This program makes a scatter plot from Iris data set. Brushing in one plot zooms in the other.
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta charset="utf-8"> | |
| <!-- Use RequireJS for module loading. --> | |
| <script src="//cdnjs.cloudflare.com/ajax/libs/require.js/2.1.14/require.js"></script> | |
| <!-- Configure AMD modules. --> | |
| <script> | |
| requirejs.config({ | |
| paths: { | |
| d3: "//d3js.org/d3.v3.min", | |
| jquery: "//code.jquery.com/jquery-2.1.1.min" | |
| } | |
| }); | |
| </script> | |
| <!-- Include CSS that styles the visualization. --> | |
| <link rel="stylesheet" href="styles.css"> | |
| <title>Scatter Plot</title> | |
| </head> | |
| <body> | |
| <!-- The visualization will be injected into this div. --> | |
| <div id="container"></div> | |
| <!-- Run the main program. --> | |
| <script src="main.js"></script> | |
| </body> | |
| </html> |
| { | |
| "sepal_length":{ | |
| "type": "Q", | |
| "label": "sepal length (cm)" | |
| }, | |
| "sepal_width": { | |
| "type": "Q", | |
| "label": "sepal width (cm)" | |
| }, | |
| "petal_length": { | |
| "type": "Q", | |
| "label": "petal length (cm)" | |
| }, | |
| "petal_width": { | |
| "type": "Q", | |
| "label": "petal width (cm)" | |
| }, | |
| "class": { | |
| "type": "N", | |
| "label": "species" | |
| } | |
| } |
| sepal_length | sepal_width | petal_length | petal_width | class | |
|---|---|---|---|---|---|
| 5.1 | 3.5 | 1.4 | 0.2 | Iris-setosa | |
| 4.9 | 3.0 | 1.4 | 0.2 | Iris-setosa | |
| 4.7 | 3.2 | 1.3 | 0.2 | Iris-setosa | |
| 4.6 | 3.1 | 1.5 | 0.2 | Iris-setosa | |
| 5.0 | 3.6 | 1.4 | 0.2 | Iris-setosa | |
| 5.4 | 3.9 | 1.7 | 0.4 | Iris-setosa | |
| 4.6 | 3.4 | 1.4 | 0.3 | Iris-setosa | |
| 5.0 | 3.4 | 1.5 | 0.2 | Iris-setosa | |
| 4.4 | 2.9 | 1.4 | 0.2 | Iris-setosa | |
| 4.9 | 3.1 | 1.5 | 0.1 | Iris-setosa | |
| 5.4 | 3.7 | 1.5 | 0.2 | Iris-setosa | |
| 4.8 | 3.4 | 1.6 | 0.2 | Iris-setosa | |
| 4.8 | 3.0 | 1.4 | 0.1 | Iris-setosa | |
| 4.3 | 3.0 | 1.1 | 0.1 | Iris-setosa | |
| 5.8 | 4.0 | 1.2 | 0.2 | Iris-setosa | |
| 5.7 | 4.4 | 1.5 | 0.4 | Iris-setosa | |
| 5.4 | 3.9 | 1.3 | 0.4 | Iris-setosa | |
| 5.1 | 3.5 | 1.4 | 0.3 | Iris-setosa | |
| 5.7 | 3.8 | 1.7 | 0.3 | Iris-setosa | |
| 5.1 | 3.8 | 1.5 | 0.3 | Iris-setosa | |
| 5.4 | 3.4 | 1.7 | 0.2 | Iris-setosa | |
| 5.1 | 3.7 | 1.5 | 0.4 | Iris-setosa | |
| 4.6 | 3.6 | 1.0 | 0.2 | Iris-setosa | |
| 5.1 | 3.3 | 1.7 | 0.5 | Iris-setosa | |
| 4.8 | 3.4 | 1.9 | 0.2 | Iris-setosa | |
| 5.0 | 3.0 | 1.6 | 0.2 | Iris-setosa | |
| 5.0 | 3.4 | 1.6 | 0.4 | Iris-setosa | |
| 5.2 | 3.5 | 1.5 | 0.2 | Iris-setosa | |
| 5.2 | 3.4 | 1.4 | 0.2 | Iris-setosa | |
| 4.7 | 3.2 | 1.6 | 0.2 | Iris-setosa | |
| 4.8 | 3.1 | 1.6 | 0.2 | Iris-setosa | |
| 5.4 | 3.4 | 1.5 | 0.4 | Iris-setosa | |
| 5.2 | 4.1 | 1.5 | 0.1 | Iris-setosa | |
| 5.5 | 4.2 | 1.4 | 0.2 | Iris-setosa | |
| 4.9 | 3.1 | 1.5 | 0.1 | Iris-setosa | |
| 5.0 | 3.2 | 1.2 | 0.2 | Iris-setosa | |
| 5.5 | 3.5 | 1.3 | 0.2 | Iris-setosa | |
| 4.9 | 3.1 | 1.5 | 0.1 | Iris-setosa | |
| 4.4 | 3.0 | 1.3 | 0.2 | Iris-setosa | |
| 5.1 | 3.4 | 1.5 | 0.2 | Iris-setosa | |
| 5.0 | 3.5 | 1.3 | 0.3 | Iris-setosa | |
| 4.5 | 2.3 | 1.3 | 0.3 | Iris-setosa | |
| 4.4 | 3.2 | 1.3 | 0.2 | Iris-setosa | |
| 5.0 | 3.5 | 1.6 | 0.6 | Iris-setosa | |
| 5.1 | 3.8 | 1.9 | 0.4 | Iris-setosa | |
| 4.8 | 3.0 | 1.4 | 0.3 | Iris-setosa | |
| 5.1 | 3.8 | 1.6 | 0.2 | Iris-setosa | |
| 4.6 | 3.2 | 1.4 | 0.2 | Iris-setosa | |
| 5.3 | 3.7 | 1.5 | 0.2 | Iris-setosa | |
| 5.0 | 3.3 | 1.4 | 0.2 | Iris-setosa | |
| 7.0 | 3.2 | 4.7 | 1.4 | Iris-versicolor | |
| 6.4 | 3.2 | 4.5 | 1.5 | Iris-versicolor | |
| 6.9 | 3.1 | 4.9 | 1.5 | Iris-versicolor | |
| 5.5 | 2.3 | 4.0 | 1.3 | Iris-versicolor | |
| 6.5 | 2.8 | 4.6 | 1.5 | Iris-versicolor | |
| 5.7 | 2.8 | 4.5 | 1.3 | Iris-versicolor | |
| 6.3 | 3.3 | 4.7 | 1.6 | Iris-versicolor | |
| 4.9 | 2.4 | 3.3 | 1.0 | Iris-versicolor | |
| 6.6 | 2.9 | 4.6 | 1.3 | Iris-versicolor | |
| 5.2 | 2.7 | 3.9 | 1.4 | Iris-versicolor | |
| 5.0 | 2.0 | 3.5 | 1.0 | Iris-versicolor | |
| 5.9 | 3.0 | 4.2 | 1.5 | Iris-versicolor | |
| 6.0 | 2.2 | 4.0 | 1.0 | Iris-versicolor | |
| 6.1 | 2.9 | 4.7 | 1.4 | Iris-versicolor | |
| 5.6 | 2.9 | 3.6 | 1.3 | Iris-versicolor | |
| 6.7 | 3.1 | 4.4 | 1.4 | Iris-versicolor | |
| 5.6 | 3.0 | 4.5 | 1.5 | Iris-versicolor | |
| 5.8 | 2.7 | 4.1 | 1.0 | Iris-versicolor | |
| 6.2 | 2.2 | 4.5 | 1.5 | Iris-versicolor | |
| 5.6 | 2.5 | 3.9 | 1.1 | Iris-versicolor | |
| 5.9 | 3.2 | 4.8 | 1.8 | Iris-versicolor | |
| 6.1 | 2.8 | 4.0 | 1.3 | Iris-versicolor | |
| 6.3 | 2.5 | 4.9 | 1.5 | Iris-versicolor | |
| 6.1 | 2.8 | 4.7 | 1.2 | Iris-versicolor | |
| 6.4 | 2.9 | 4.3 | 1.3 | Iris-versicolor | |
| 6.6 | 3.0 | 4.4 | 1.4 | Iris-versicolor | |
| 6.8 | 2.8 | 4.8 | 1.4 | Iris-versicolor | |
| 6.7 | 3.0 | 5.0 | 1.7 | Iris-versicolor | |
| 6.0 | 2.9 | 4.5 | 1.5 | Iris-versicolor | |
| 5.7 | 2.6 | 3.5 | 1.0 | Iris-versicolor | |
| 5.5 | 2.4 | 3.8 | 1.1 | Iris-versicolor | |
| 5.5 | 2.4 | 3.7 | 1.0 | Iris-versicolor | |
| 5.8 | 2.7 | 3.9 | 1.2 | Iris-versicolor | |
| 6.0 | 2.7 | 5.1 | 1.6 | Iris-versicolor | |
| 5.4 | 3.0 | 4.5 | 1.5 | Iris-versicolor | |
| 6.0 | 3.4 | 4.5 | 1.6 | Iris-versicolor | |
| 6.7 | 3.1 | 4.7 | 1.5 | Iris-versicolor | |
| 6.3 | 2.3 | 4.4 | 1.3 | Iris-versicolor | |
| 5.6 | 3.0 | 4.1 | 1.3 | Iris-versicolor | |
| 5.5 | 2.5 | 4.0 | 1.3 | Iris-versicolor | |
| 5.5 | 2.6 | 4.4 | 1.2 | Iris-versicolor | |
| 6.1 | 3.0 | 4.6 | 1.4 | Iris-versicolor | |
| 5.8 | 2.6 | 4.0 | 1.2 | Iris-versicolor | |
| 5.0 | 2.3 | 3.3 | 1.0 | Iris-versicolor | |
| 5.6 | 2.7 | 4.2 | 1.3 | Iris-versicolor | |
| 5.7 | 3.0 | 4.2 | 1.2 | Iris-versicolor | |
| 5.7 | 2.9 | 4.2 | 1.3 | Iris-versicolor | |
| 6.2 | 2.9 | 4.3 | 1.3 | Iris-versicolor | |
| 5.1 | 2.5 | 3.0 | 1.1 | Iris-versicolor | |
| 5.7 | 2.8 | 4.1 | 1.3 | Iris-versicolor | |
| 6.3 | 3.3 | 6.0 | 2.5 | Iris-virginica | |
| 5.8 | 2.7 | 5.1 | 1.9 | Iris-virginica | |
| 7.1 | 3.0 | 5.9 | 2.1 | Iris-virginica | |
| 6.3 | 2.9 | 5.6 | 1.8 | Iris-virginica | |
| 6.5 | 3.0 | 5.8 | 2.2 | Iris-virginica | |
| 7.6 | 3.0 | 6.6 | 2.1 | Iris-virginica | |
| 4.9 | 2.5 | 4.5 | 1.7 | Iris-virginica | |
| 7.3 | 2.9 | 6.3 | 1.8 | Iris-virginica | |
| 6.7 | 2.5 | 5.8 | 1.8 | Iris-virginica | |
| 7.2 | 3.6 | 6.1 | 2.5 | Iris-virginica | |
| 6.5 | 3.2 | 5.1 | 2.0 | Iris-virginica | |
| 6.4 | 2.7 | 5.3 | 1.9 | Iris-virginica | |
| 6.8 | 3.0 | 5.5 | 2.1 | Iris-virginica | |
| 5.7 | 2.5 | 5.0 | 2.0 | Iris-virginica | |
| 5.8 | 2.8 | 5.1 | 2.4 | Iris-virginica | |
| 6.4 | 3.2 | 5.3 | 2.3 | Iris-virginica | |
| 6.5 | 3.0 | 5.5 | 1.8 | Iris-virginica | |
| 7.7 | 3.8 | 6.7 | 2.2 | Iris-virginica | |
| 7.7 | 2.6 | 6.9 | 2.3 | Iris-virginica | |
| 6.0 | 2.2 | 5.0 | 1.5 | Iris-virginica | |
| 6.9 | 3.2 | 5.7 | 2.3 | Iris-virginica | |
| 5.6 | 2.8 | 4.9 | 2.0 | Iris-virginica | |
| 7.7 | 2.8 | 6.7 | 2.0 | Iris-virginica | |
| 6.3 | 2.7 | 4.9 | 1.8 | Iris-virginica | |
| 6.7 | 3.3 | 5.7 | 2.1 | Iris-virginica | |
| 7.2 | 3.2 | 6.0 | 1.8 | Iris-virginica | |
| 6.2 | 2.8 | 4.8 | 1.8 | Iris-virginica | |
| 6.1 | 3.0 | 4.9 | 1.8 | Iris-virginica | |
| 6.4 | 2.8 | 5.6 | 2.1 | Iris-virginica | |
| 7.2 | 3.0 | 5.8 | 1.6 | Iris-virginica | |
| 7.4 | 2.8 | 6.1 | 1.9 | Iris-virginica | |
| 7.9 | 3.8 | 6.4 | 2.0 | Iris-virginica | |
| 6.4 | 2.8 | 5.6 | 2.2 | Iris-virginica | |
| 6.3 | 2.8 | 5.1 | 1.5 | Iris-virginica | |
| 6.1 | 2.6 | 5.6 | 1.4 | Iris-virginica | |
| 7.7 | 3.0 | 6.1 | 2.3 | Iris-virginica | |
| 6.3 | 3.4 | 5.6 | 2.4 | Iris-virginica | |
| 6.4 | 3.1 | 5.5 | 1.8 | Iris-virginica | |
| 6.0 | 3.0 | 4.8 | 1.8 | Iris-virginica | |
| 6.9 | 3.1 | 5.4 | 2.1 | Iris-virginica | |
| 6.7 | 3.1 | 5.6 | 2.4 | Iris-virginica | |
| 6.9 | 3.1 | 5.1 | 2.3 | Iris-virginica | |
| 5.8 | 2.7 | 5.1 | 1.9 | Iris-virginica | |
| 6.8 | 3.2 | 5.9 | 2.3 | Iris-virginica | |
| 6.7 | 3.3 | 5.7 | 2.5 | Iris-virginica | |
| 6.7 | 3.0 | 5.2 | 2.3 | Iris-virginica | |
| 6.3 | 2.5 | 5.0 | 1.9 | Iris-virginica | |
| 6.5 | 3.0 | 5.2 | 2.0 | Iris-virginica | |
| 6.2 | 3.4 | 5.4 | 2.3 | Iris-virginica | |
| 5.9 | 3.0 | 5.1 | 1.8 | Iris-virginica |
| // This is the main program that sets up a scatter plot to visualize the Iris data set. | |
| // Curran Kelleher March 2015 | |
| require(["scatterPlot"], function (ScatterPlot) { | |
| // Initialize the scatter plot. | |
| var options = { | |
| // Tell the visualization which DOM element to insert itself into. | |
| container: d3.select("#container").node(), | |
| // Specify the margin and text label offsets. | |
| margin: { | |
| top: 10, | |
| right: 10, | |
| bottom: 45, | |
| left: 55 | |
| }, | |
| yAxisLabelOffset: 1.8, // Unit is CSS "em"s | |
| xAxisLabelOffset: 1.9, | |
| titleOffset: 0.3 | |
| }, | |
| scatterPlot1 = ScatterPlot(options), | |
| scatterPlot2 = ScatterPlot(options); | |
| // Fetch the column metadata. | |
| d3.json("iris-metadata.json", function (metadata) { | |
| var xColumn = "sepal_length", | |
| yColumn = "petal_length", | |
| sizeColumn = "petal_width", | |
| colorColumn = "class", | |
| xyOptions = { | |
| xColumn: xColumn, | |
| xAxisLabel: metadata[xColumn].label, | |
| yColumn: yColumn, | |
| yAxisLabel: metadata[yColumn].label | |
| }; | |
| // Use the same X and Y for all plots. | |
| scatterPlot1.set(xyOptions); | |
| scatterPlot2.set(xyOptions); | |
| // Load the data from a CSV file. | |
| d3.csv("iris.csv", function (data){ | |
| // Parse quantitative values from strings to numbers. | |
| var quantitativeColumns = Object.keys(metadata).filter(function (column){ | |
| return metadata[column].type === "Q"; | |
| }); | |
| data.forEach(function (d){ | |
| quantitativeColumns.forEach(function (column){ | |
| d[column] = parseFloat(d[column]); | |
| }); | |
| }); | |
| // Pass the data into the plots. | |
| scatterPlot1.data = data; | |
| scatterPlot2.data = data; | |
| }); | |
| // Use the first plot to zoom in the second plot. | |
| scatterPlot1.brushEnabled = true; | |
| scatterPlot1.when("brushedIntervals", function (brushedIntervals){ | |
| scatterPlot2.xDomainMin = brushedIntervals[xColumn][0]; | |
| scatterPlot2.xDomainMax = brushedIntervals[xColumn][1]; | |
| scatterPlot2.yDomainMin = brushedIntervals[yColumn][0]; | |
| scatterPlot2.yDomainMax = brushedIntervals[yColumn][1]; | |
| }); | |
| // Initialize the default brush. | |
| scatterPlot1.brushedIntervals = { | |
| "sepal_length": [ 4.82, 7.77 ], | |
| "petal_length": [ 2.84, 6.80 ] | |
| }; | |
| }); | |
| // Sets the `box` model property | |
| // based on the size of the container, | |
| function computeBoxes(){ | |
| var width = container.clientWidth, | |
| height = container.clientHeight, | |
| padding = 10, | |
| plotWidth = (width - padding * 2) / 2, | |
| plotHeight = height - padding * 2; | |
| scatterPlot1.box = { | |
| x: padding, | |
| y: padding, | |
| width: plotWidth, | |
| height: plotHeight | |
| }; | |
| scatterPlot2.box = { | |
| x: plotWidth + padding * 2, | |
| y: padding, | |
| width: plotWidth, | |
| height: plotHeight | |
| }; | |
| } | |
| // once to initialize `model.box`, and | |
| computeBoxes(); | |
| // whenever the browser window resizes in the future. | |
| window.addEventListener("resize", computeBoxes); | |
| }); |
| // A functional reactive model library. | |
| // | |
| (function(){ | |
| // The D3 conventional graph representation. | |
| // See https://github.com/mbostock/d3/wiki/Force-Layout#nodes | |
| var nodes, links, idCounter, map; | |
| function resetFlowGraph(){ | |
| nodes = []; | |
| links = []; | |
| idCounter = 0; | |
| map = {}; | |
| } | |
| function getFlowGraph(){ | |
| return { | |
| nodes: nodes, | |
| links: links | |
| }; | |
| } | |
| resetFlowGraph(); | |
| // Adds the nodes and links to the data flow graph for one | |
| // particular reactive function. | |
| function updateLambda(modelId, lambdaId, inProperties, outProperties){ | |
| var lambda = lambdaNode(lambdaId); | |
| inProperties.forEach(function(property){ | |
| link(propertyNode(modelId, property), lambda); | |
| }); | |
| outProperties.forEach(function(property){ | |
| link(lambda, propertyNode(modelId, property)); | |
| }); | |
| } | |
| function lambdaNode(id){ | |
| return getOrCreate(id, nodes, createLambda); | |
| } | |
| function createLambda(index){ | |
| return { | |
| type: "lambda", | |
| index: index | |
| }; | |
| } | |
| function propertyNode(modelId, property){ | |
| var id = modelId + "." + property; | |
| return getOrCreate(id, nodes, createPropertyNode(property)); | |
| } | |
| function createPropertyNode(property){ | |
| return function(index){ | |
| return { | |
| type: "property", | |
| index: index, | |
| property: property | |
| }; | |
| }; | |
| } | |
| function link(sourceNode, targetNode){ | |
| var source = sourceNode.index, | |
| target = targetNode.index, | |
| id = source + "-" + target; | |
| getOrCreate(id, links, createLink(source, target)); | |
| } | |
| function createLink(source, target){ | |
| return function(index){ | |
| return { | |
| source: source, | |
| target: target | |
| }; | |
| }; | |
| } | |
| function getOrCreate(id, things, createThing){ | |
| var thing = map[id]; | |
| if(!thing){ | |
| thing = map[id] = createThing(things.length); | |
| things.push(thing); | |
| } | |
| return thing; | |
| } | |
| // The constructor function, accepting default values. | |
| function Model(defaults){ | |
| // The returned public API object. | |
| var model = {}, | |
| // The internal stored values for tracked properties. { property -> value } | |
| values = {}, | |
| // The callback functions for each tracked property. { property -> [callback] } | |
| listeners = {}, | |
| // The set of tracked properties. { property -> true } | |
| trackedProperties = {}, | |
| modelId = idCounter++, | |
| changedProperties = {}; | |
| // The functional reactive "when" operator. | |
| // | |
| // * `properties` An array of property names (can also be a single property string). | |
| // * `callback` A callback function that is called: | |
| // * with property values as arguments, ordered corresponding to the properties array, | |
| // * only if all specified properties have values, | |
| // * once for initialization, | |
| // * whenever one or more specified properties change, | |
| // * on the next tick of the JavaScript event loop after properties change, | |
| // * only once as a result of one or more synchronous changes to dependency properties. | |
| function when(properties, callback, thisArg){ | |
| var lambdaId = idCounter++; | |
| // Make sure the default `this` becomes | |
| // the object you called `.on` on. | |
| thisArg = thisArg || this; | |
| // Handle either an array or a single string. | |
| properties = (properties instanceof Array) ? properties : [properties]; | |
| // This function will trigger the callback to be invoked. | |
| var triggerCallback = debounce(function (){ | |
| var args = properties.map(function(property){ | |
| return values[property]; | |
| }); | |
| if(allAreDefined(args)){ | |
| changedProperties = {}; | |
| callback.apply(thisArg, args); | |
| updateLambda(modelId, lambdaId, properties, Object.keys(changedProperties)); | |
| } | |
| }); | |
| // Trigger the callback once for initialization. | |
| triggerCallback(); | |
| // Trigger the callback whenever specified properties change. | |
| properties.forEach(function(property){ | |
| on(property, triggerCallback); | |
| }); | |
| // Return this function so it can be removed later. | |
| return triggerCallback; | |
| } | |
| // Returns a debounced version of the given function. | |
| // See http://underscorejs.org/#debounce | |
| function debounce(callback){ | |
| var queued = false; | |
| return function () { | |
| if(!queued){ | |
| queued = true; | |
| setTimeout(function () { | |
| queued = false; | |
| callback(); | |
| }, 0); | |
| } | |
| }; | |
| } | |
| // Returns true if all elements of the given array are defined, false otherwise. | |
| function allAreDefined(arr){ | |
| return !arr.some(function (d) { | |
| return typeof d === 'undefined' || d === null; | |
| }); | |
| } | |
| // Adds a change listener for a given property with Backbone-like behavior. | |
| // Similar to http://backbonejs.org/#Events-on | |
| function on(property, callback, thisArg){ | |
| // Make sure the default `this` becomes | |
| // the object you called `.on` on. | |
| thisArg = thisArg || this; | |
| getListeners(property).push(callback); | |
| track(property, thisArg); | |
| } | |
| // Gets or creates the array of listener functions for a given property. | |
| function getListeners(property){ | |
| return listeners[property] || (listeners[property] = []); | |
| } | |
| // Tracks a property if it is not already tracked. | |
| function track(property, thisArg){ | |
| if(!(property in trackedProperties)){ | |
| trackedProperties[property] = true; | |
| values[property] = model[property]; | |
| Object.defineProperty(model, property, { | |
| get: function () { return values[property]; }, | |
| set: function(newValue) { | |
| var oldValue = values[property]; | |
| values[property] = newValue; | |
| getListeners(property).forEach(function(callback){ | |
| callback.call(thisArg, newValue, oldValue); | |
| }); | |
| changedProperties[property] = true; | |
| } | |
| }); | |
| } | |
| } | |
| // Removes a listener added using `when()`. | |
| function cancel(listener){ | |
| for(var property in listeners){ | |
| off(property, listener); | |
| } | |
| } | |
| // Removes a change listener added using `on`. | |
| function off(property, callback){ | |
| listeners[property] = listeners[property].filter(function (listener) { | |
| return listener !== callback; | |
| }); | |
| } | |
| // Sets all of the given values on the model. | |
| // `newValues` is an object { property -> value }. | |
| function set(newValues){ | |
| for(var property in newValues){ | |
| model[property] = newValues[property]; | |
| } | |
| } | |
| // Transfer defaults passed into the constructor to the model. | |
| set(defaults); | |
| // Expose the public API. | |
| model.when = when; | |
| model.cancel = cancel; | |
| model.on = on; | |
| model.off = off; | |
| model.set = set; | |
| return model; | |
| } | |
| Model.getFlowGraph = getFlowGraph; | |
| Model.resetFlowGraph = resetFlowGraph; | |
| // Support AMD (RequireJS), CommonJS (Node), and browser globals. | |
| // Inspired by https://github.com/umdjs/umd | |
| if (typeof define === "function" && define.amd) { | |
| define([], function () { return Model; }); | |
| } else if (typeof exports === "object") { | |
| module.exports = Model; | |
| } else { | |
| this.Model = Model; | |
| } | |
| })(); |
| // 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 = "__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"); | |
| }); | |
| model.when("g", function (g) { | |
| // Add an SVG group to contain the marks. | |
| model.circlesG = g.append("g"); | |
| // Create a group for the brush. | |
| model.brushG = g.append("g").attr("class", "brush"); | |
| // The circles group is added first, before the brush group, | |
| // so that mouse events go to the brush rather than to the | |
| // circles, even when the mouse is on top of a circle. | |
| }); | |
| // Disable brushing by default. | |
| model.brushEnabled = false; | |
| // Set up brushing interactions to define `brushedIntervals` on the model. | |
| model.when(["brushEnabled", "xColumn", "yColumn", "xScale", "yScale"], | |
| function (brushEnabled, xColumn, yColumn, xScale, yScale) { | |
| if(brushEnabled){ | |
| var brush = d3.svg.brush(); | |
| brush.on("brush", function () { | |
| model.brushedIntervals = brushToIntervals(brush, xColumn, yColumn, xScale, yScale); | |
| }); | |
| model.brush = brush; | |
| } | |
| }); | |
| function brushToIntervals(brush, xColumn, yColumn, xScale, yScale){ | |
| var brushedIntervals = {}; | |
| if(!brush.empty() | |
| && brush.extent() !== null){ | |
| var e = brush.extent(), | |
| xMin = e[0][0], | |
| yMin = e[0][1], | |
| xMax = e[1][0], | |
| yMax = e[1][1], | |
| epsilon = 0.01; | |
| // Account for the edge case where the brush is at the | |
| // X or Y min or max. Adding a small value ensures that all | |
| // points are included when crossfilter's filterRange is used, | |
| // because filterRange provides an exclusive range, not inclusive. | |
| // See https://github.com/square/crossfilter/wiki/API-Reference#dimension_filterRange | |
| if(xMax === xScale.domain()[1]){ xMax += epsilon; } | |
| if(yMax === yScale.domain()[1]){ yMax += epsilon; } | |
| if(xMin === xScale.domain()[0]){ xMin -= epsilon; } | |
| if(yMin === yScale.domain()[0]){ yMin -= epsilon; } | |
| brushedIntervals[xColumn] = [xMin, xMax]; | |
| brushedIntervals[yColumn] = [yMin, yMax]; | |
| } else { | |
| brushedIntervals[xColumn] = [None, None]; | |
| brushedIntervals[yColumn] = [None, None]; | |
| } | |
| return brushedIntervals; | |
| } | |
| function intervalsToBrush(brushedIntervals, xColumn, yColumn){ | |
| return [ | |
| [brushedIntervals[xColumn][0], brushedIntervals[yColumn][0]], | |
| [brushedIntervals[xColumn][1], brushedIntervals[yColumn][1]] | |
| ]; | |
| } | |
| // Update the rendered brush. | |
| model.when(["brushedIntervals", "brush", "brushG", "xColumn", "yColumn", "xScale", "yScale"], | |
| function (brushedIntervals, brush, brushG, xColumn, yColumn, xScale, yScale) { | |
| // Update the scales within the brush. | |
| brush.x(xScale); | |
| brush.y(yScale); | |
| // Update the extent of the brush. | |
| brush.extent(intervalsToBrush(brushedIntervals, xColumn, yColumn)); | |
| // Render the brush onto the brush group. | |
| brushG.call(brush); | |
| }); | |
| // 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 size 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; }; | |
| } | |
| }); | |
| // Filter out points that go beyond the edges of the plot | |
| // for the case that the domain is set explicitly and is | |
| // smaller than the extent of the data. | |
| model.when(["data", "getX", "getY", "xScale", "yScale"], | |
| function(data, getX, getY, xScale, yScale){ | |
| var xMin = xScale.domain()[0], xMax = xScale.domain()[1], | |
| yMin = yScale.domain()[0], yMax = yScale.domain()[1]; | |
| model.visibleData = data.filter(function(d){ | |
| var x = getX(d), y = getY(d); | |
| return x > xMin && x < xMax && y > yMin && y < yMax; | |
| }); | |
| }); | |
| // Draw the circles of the scatter plot. | |
| model.when(["visibleData", "circlesG", "getXScaled", "getYScaled", "getSizeScaled", "getColorScaled"], | |
| function (visibleData, circlesG, getXScaled, getYScaled, getSizeScaled, getColorScaled){ | |
| var circles = circlesG.selectAll("circle").data(visibleData); | |
| 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; | |
| }; | |
| }); |
| /* Remove the default margin. */ | |
| body { | |
| margin: 0px; | |
| } | |
| /* Make the visualization container fill the page. */ | |
| #container { | |
| /* Use the default size from bl.ocks.org */ | |
| width: 960px; | |
| height: 500px; | |
| } | |
| /* Put a border around each plot. */ | |
| svg { | |
| border-style: solid; | |
| border-color: lightgray; | |
| border-width: 1px; | |
| } | |
| /* Style the visualization. Draws from http://bl.ocks.org/mbostock/3887118 */ | |
| /* Tick mark labels */ | |
| .axis .tick text { | |
| font: 8pt sans-serif; | |
| } | |
| /* Axis labels */ | |
| .axis text { | |
| font: 14pt sans-serif; | |
| } | |
| .axis path, | |
| .axis line { | |
| fill: none; | |
| stroke: #000; | |
| shape-rendering: crispEdges; | |
| } | |
| .line { | |
| fill: none; | |
| stroke: black; | |
| stroke-width: 1.5px; | |
| } | |
| .title-text { | |
| text-anchor: middle; | |
| font: 24pt sans-serif; | |
| } | |
| /* Style the brush. Draws from http://bl.ocks.org/mbostock/4343214 */ | |
| .brush .extent { | |
| stroke: gray; | |
| fill-opacity: .125; | |
| shape-rendering: crispEdges; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment