// A reusable scatter plot module. // Curran Kelleher March 2015 define(["d3", "model"], function (d3, Model) { // A representation for an optional Model property that is not specified. // This allows the "when" approach to support optional properties. // Inspired by Scala's Option type. // See http://alvinalexander.com/scala/using-scala-option-some-none-idiom-function-java-null var None = Model.None; // The constructor function, accepting default values. return function ScatterPlot(defaults) { // Create a Model instance for the visualization. // This will serve as its public API. var model = Model(); // Create an SVG element from the container DOM element. model.when("container", function (container) { model.svg = d3.select(container).append("svg") // Use CSS `position: absolute;` // so setting `left` and `top` later will // position the SVG relative to the container div. .style("position", "absolute"); }); // Adjust the size of the SVG based on the `box` property. model.when(["svg", "box"], function (svg, box) { // Set the CSS `left` and `top` properties // to move the SVG to `(box.x, box.y)` // relative to the container div. svg .style("left", box.x + "px") .style("top", box.y + "px") .attr("width", box.width) .attr("height", box.height); }); // Create an SVG group that will contain the visualization. model.when("svg", function (svg) { model.g = svg.append("g"); }); // Adjust the SVG group translation based on the margin. model.when(["g", "margin"], function (g, margin) { g.attr("transform", "translate(" + margin.left + "," + margin.top + ")"); }); // Create the title text element. model.when("g", function (g){ model.titleText = g.append("text").attr("class", "title-text"); }); // Center the title text when width changes. model.when(["titleText", "width"], function (titleText, width) { titleText.attr("x", width / 2); }); // Update the title text based on the `title` property. model.when(["titleText", "title"], function (titleText, title){ titleText.text(title); }); // Update the title text offset. model.when(["titleText", "titleOffset"], function (titleText, titleOffset){ titleText.attr("dy", titleOffset + "em"); }); // Compute the inner box from the outer box and margin. // See Margin Convention http://bl.ocks.org/mbostock/3019563 model.when(["box", "margin"], function (box, margin) { model.width = box.width - margin.left - margin.right; model.height = box.height - margin.top - margin.bottom; }); // Generate a function for getting the X value. model.when(["data", "xColumn"], function (data, xColumn) { model.getX = function (d) { return d[xColumn]; }; }); // Compute the domain of the X attribute. // Allow the API client to optionally specify fixed min and max values. model.xDomainMin = None; model.xDomainMax = None; model.when(["data", "getX", "xDomainMin", "xDomainMax"], function (data, getX, xDomainMin, xDomainMax) { if(xDomainMin === None && xDomainMax === None){ model.xDomain = d3.extent(data, getX); } else { if(xDomainMin === None){ xDomainMin = d3.min(data, getX); } if(xDomainMax === None){ xDomainMax = d3.max(data, getX); } model.xDomain = [xDomainMin, xDomainMax] } }); // Compute the X scale. model.when(["xDomain", "width"], function (xDomain, width) { model.xScale = d3.scale.linear().domain(xDomain).range([0, width]); }); // Generate a function for getting the scaled X value. model.when(["data", "xScale", "getX"], function (data, xScale, getX) { model.getXScaled = function (d) { return xScale(getX(d)); }; }); // Set up the X axis. model.when("g", function (g) { model.xAxisG = g.append("g").attr("class", "x axis"); model.xAxisText = model.xAxisG.append("text").style("text-anchor", "middle"); }); // Move the X axis label based on its specified offset. model.when(["xAxisText", "xAxisLabelOffset"], function (xAxisText, xAxisLabelOffset){ xAxisText.attr("dy", xAxisLabelOffset + "em"); }); // Update the X axis transform when height changes. model.when(["xAxisG", "height"], function (xAxisG, height) { xAxisG.attr("transform", "translate(0," + height + ")"); }); // Center the X axis label when width changes. model.when(["xAxisText", "width"], function (xAxisText, width) { xAxisText.attr("x", width / 2); }); // Update the X axis based on the X scale. model.when(["xAxisG", "xScale"], function (xAxisG, xScale) { xAxisG.call(d3.svg.axis().orient("bottom").scale(xScale)); }); // Update X axis label. model.when(["xAxisText", "xAxisLabel"], function (xAxisText, xAxisLabel) { xAxisText.text(xAxisLabel); }); // Generate a function for getting the Y value. model.when(["data", "yColumn"], function (data, yColumn) { model.getY = function (d) { return d[yColumn]; }; }); // Compute the domain of the Y attribute. // Allow the API client to optionally specify fixed min and max values. model.yDomainMin = None; model.yDomainMax = None; model.when(["data", "getY", "yDomainMin", "yDomainMax"], function (data, getY, yDomainMin, yDomainMax) { if(yDomainMin === None && yDomainMax === None){ model.yDomain = d3.extent(data, getY); } else { if(yDomainMin === None){ yDomainMin = d3.min(data, getY); } if(yDomainMax === None){ yDomainMax = d3.max(data, getY); } model.yDomain = [yDomainMin, yDomainMax] } }); // Compute the Y scale. model.when(["data", "yDomain", "height"], function (data, yDomain, height) { model.yScale = d3.scale.linear().domain(yDomain).range([height, 0]); }); // Generate a function for getting the scaled Y value. model.when(["data", "yScale", "getY"], function (data, yScale, getY) { model.getYScaled = function (d) { return yScale(getY(d)); }; }); // Set up the Y axis. model.when("g", function (g) { model.yAxisG = g.append("g").attr("class", "y axis"); model.yAxisText = model.yAxisG.append("text") .style("text-anchor", "middle") .attr("transform", "rotate(-90)") .attr("y", 0); }); // Move the Y axis label based on its specified offset. model.when(["yAxisText", "yAxisLabelOffset"], function (yAxisText, yAxisLabelOffset){ yAxisText.attr("dy", "-" + yAxisLabelOffset + "em") }); // Center the Y axis label when height changes. model.when(["yAxisText", "height"], function (yAxisText, height) { yAxisText.attr("x", -height / 2); }); // Update Y axis label. model.when(["yAxisText", "yAxisLabel"], function (yAxisText, yAxisLabel) { yAxisText.text(yAxisLabel); }); // Update the Y axis based on the Y scale. model.when(["yAxisG", "yScale"], function (yAxisG, yScale) { yAxisG.call(d3.svg.axis().orient("left").scale(yScale)); }); // Allow the API client to optionally specify a size column. model.sizeColumn = None; // The default radius of circles in pixels. model.sizeDefault = 2; // The min and max circle radius in pixels. model.sizeMin = 0.5; model.sizeMax = 6; // Set up the size scale. model.when(["sizeColumn", "data", "sizeDefault", "sizeMin", "sizeMax"], function (sizeColumn, data, sizeDefault, sizeMin, sizeMax){ if(sizeColumn !== None){ var getSize = function (d){ return d[sizeColumn] }, sizeScale = d3.scale.linear() .domain(d3.extent(data, getSize)) .range([sizeMin, sizeMax]); model.getSizeScaled = function (d){ return sizeScale(getSize(d)); }; } else { model.getSizeScaled = function (d){ return sizeDefault; }; } }); // Allow the API client to optionally specify a color column. model.colorColumn = None; model.colorRange = None; // The default color of circles (CSS color string). model.colorDefault = "black"; // Set up the color scale. model.when(["colorColumn", "data", "colorDefault", "colorRange"], function (colorColumn, data, colorDefault, colorRange){ if(colorColumn !== None && colorRange !== None){ var getColor = function (d){ return d[colorColumn] }, colorScale = d3.scale.ordinal() .domain(data.map(getColor)) .range(colorRange); model.getColorScaled = function (d){ return colorScale(getColor(d)); }; } else { model.getColorScaled = function (d){ return colorDefault; }; } }); // Add an SVG group to contain the marks. model.when("g", function (g) { model.circlesG = g.append("g"); }); // Draw the circles of the scatter plot. model.when(["data", "circlesG", "getXScaled", "getYScaled", "getSizeScaled", "getColorScaled"], function (data, circlesG, getXScaled, getYScaled, getSizeScaled, getColorScaled){ var circles = circlesG.selectAll("circle").data(data); circles.enter().append("circle"); circles .attr("cx", getXScaled) .attr("cy", getYScaled) .attr("r", getSizeScaled) .attr("fill", getColorScaled); circles.exit().remove(); }); // Set defaults at the end so they override optional properties set to None. model.set(defaults); return model; }; });