├── .gitignore ├── .travis.yml ├── LICENSE ├── README-FUTURE.md ├── README.md ├── package.json ├── rollup.config.js ├── src ├── components │ ├── circle.js │ └── scatter.js ├── index.js └── mixins │ ├── column.js │ ├── data.js │ ├── margin.js │ ├── resize.js │ ├── scale.js │ └── svg.js └── test ├── components ├── circleTest.js └── scatterTest.js ├── composition └── nested.js ├── mixins ├── columnTest.js ├── dataTest.js ├── marginTest.js ├── resizeTest.js ├── scaleTest.js └── svgTest.js ├── test.js └── visual ├── index.html └── scatterPlotData.csv /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4.4.5" 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Curran Kelleher 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README-FUTURE.md: -------------------------------------------------------------------------------- 1 | This document is essentially a requirements document that outlines the goals and scope of the project. It is written in such a way that parts of the document can be move into the actual README once they are implemented. 2 | 3 | # reactive-vis FUTURE! 4 | 5 | This library supports the creation of interactive data visualizations. A layered approach is taken wherein visual components can be built up from smaller ones, and visual components can be composed in various ways. 6 | 7 | ## Simplifying Assumptions 8 | 9 | * SVG Only (No Canvas, No WebGL) 10 | 11 | ## Composition 12 | 13 | * Adjacent - Visualizations may be placed next to one another within the same SVG element. The placement should be able to follow a nested box layout. 14 | * Layered - It should be possible to place a visual component on top of another in a layered manner. Examples: 15 | * Color legend on top of a stacked bar chart in the upper right corner. 16 | * Area chart behind a bar chart 17 | * Nested - Hybrid visualizations in which visual components are nested within other visual components should be trivial to assemble. Examples: 18 | * Facet (small multiples in X and/or Y) 19 | * Scatter plot of pie charts 20 | * Treemap of line charts 21 | 22 | ## Dynamism 23 | 24 | * Interactions 25 | * Brushing 26 | * Quadtree 27 | * R-Tree 28 | * Hovering 29 | * Voronoi Overlay 30 | * Pan 31 | * Zoom 32 | * Linked Views 33 | * Linked Highlighting 34 | * Focus + Context 35 | * Crossfilter 36 | * Transitions 37 | * Updates in configuration should manifest as transitions where possible. 38 | * It should be possible to customize the transitions (e.g. bounce in bars on enter). 39 | 40 | # Visual Components 41 | 42 | Visual components include both marks and "chart types". 43 | 44 | ## Foundational Components 45 | 46 | * Circle 47 | * Rectangle 48 | * Scatter Plot 49 | * Bar Chart 50 | * Stacked 51 | * Grouped 52 | * Horizontal variations (Bar Chart, Stacked, Grouped) 53 | * Pie Chart 54 | * Line Chart 55 | * Variant with multiple lines 56 | * Variant with spline interpolation 57 | * Area Chart 58 | * Stacked Area Chart 59 | * Streamgraph 60 | * Treemap 61 | 62 | ## Add-on Components 63 | 64 | The following components should be implemented as separate packages that build upon this library. 65 | 66 | * Sankey 67 | * Chord 68 | * Parallel Coordinates 69 | * Choropleth 70 | * Tree (tidy) 71 | * Box Plot 72 | 73 | ## Extensibility 74 | 75 | Visual components should be extensible without requiring modification of the original code. Something like inheritance should be possible. The following should be accessible: 76 | 77 | * Visual marks should be accessible for appending additional marks or adding interactions. 78 | * e.g. adding text labels to each bar in a bar chart 79 | * Scales should be inspectable. 80 | * e.g. for using in a color legend 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # reactive-vis 2 | 3 | A library for reactive data visualizations. 4 | 5 | [![NPM](https://nodei.co/npm/reactive-vis.png)](https://npmjs.org/package/reactive-vis) 6 | [![NPM](https://nodei.co/npm-dl/reactive-vis.png?months=3)](https://npmjs.org/package/reactive-vis) 7 | [![Build Status](https://travis-ci.org/datavis-tech/reactive-vis.svg?branch=master)](https://travis-ci.org/datavis-tech/reactive-vis) 8 | 9 | This project is about encapsulating dynamic behaviors that are common to many data visualization types. It is intended to be the foundation for authoring reusable interactive data visualization components that can easily be extended and composed. 10 | 11 |

12 | 13 |
14 | The reactive-vis stack for interactive data visualizations. 15 |
16 | reactive-model | 17 | D3 18 |

19 | 20 | ## API Reference 21 | 22 | Each function is a "reactive mixin", meaning that it can be invoked using model.call. For all functions, the first argument *model* is an instance of [reactive-model](https://github.com/datavis-tech/reactive-model). Each function adds new properties and reactive functions to the specified *model*. 23 | 24 | Note that names for nodes in the data flow graph diagrams follow the convention that `camelCase` names are properties with values, and `dash-separated` names reactive functions that have side effects but no returned value (typically these are DOM manipulations). Hyphenated names serve only to document what the reactive function does, they are never actually assigned values. 25 | 26 | * [SVG](#svg) 27 | * [Margin](#margin) 28 | * [Data](#data) 29 | 30 | ### SVG 31 | 32 | # ReactiveVis.SVG(model) 33 | 34 |

35 | 36 |

37 | 38 | Properties added: 39 | 40 | * *model*.**svg** An SVG DOM element. This may be set any number of times. This will be the root of the visualization DOM tree, and will be accessed by other mixins. 41 | * *model*.**width** An integer representing the width (in pixels) of the SVG element. The default value is 960 (the default width of examples on [bl.ocks.org](http://bl.ocks.org/)). 42 | * *model*.**height** An integer representing the width (in pixels) of the SVG element. The default value is 500 (the default height of examples on [bl.ocks.org](http://bl.ocks.org/). 43 | 44 | Reactive functions: 45 | 46 | * **svg-width** Sets the `width` attribute of the SVG element based on the value of model.*width*. 47 | * **svg-height** Sets the `height` attribute of the SVG element based on the value of model.*height*. 48 | 49 | ### Margin 50 | 51 | # ReactiveVis.Margin(model) 52 | 53 | Encapsulates margins. Inspired by [D3 Margin Convention](https://bl.ocks.org/mbostock/3019563). Depends on [SVG](#svg). 54 | 55 |

56 | 57 |

58 | 59 | Properties added: 60 | 61 | * *model*.**marginRight** The right side margin (in pixels). 62 | * *model*.**marginLeft** The left side margin (in pixels). 63 | * *model*.**marginTop** The top side margin (in pixels). 64 | * *model*.**marginBottom** The bottom side margin (in pixels). 65 | * *model*.**innerWidth** The width of the inner rectangle, after margins have been applied. This is computed and updated based on *model*.**marginRight**, *model*.**marginLeft**, and *model*.**width**. 66 | * *model*.**innerHeight** The height of the inner rectangle, after margins have been applied. This is computed and updated based on *model*.**marginTop**, *model*.**marginBottom**, and *model*.**height**. 67 | * *model*.**g** An SVG `` element, appended as a child to *model*.**svg**. 68 | 69 | Reactive functions: 70 | 71 | * **g-transform** Computes and updates the `transform` attribute of *model*.**g** based on *model*.**marginTop** and *model*.**marginLeft**. 72 | 73 | ### Data 74 | 75 | # ReactiveVis.Data(model) 76 | 77 | Properties added: 78 | 79 | * *model*.**data** This property accepts the input data for the component. Typically this is expected to be an array of objects (e.g. the output from parsing a CSV file using [d3-dsv](https://github.com/d3/d3-dsv)). 80 | 81 | ## Related Work 82 | 83 | This project is similar to: 84 | 85 | * [Vega](https://github.com/vega/vega) 86 | * [C3](http://c3js.org/) 87 | * [Dimple](http://dimplejs.org/) 88 | * [NVD3](http://nvd3.org/) 89 | 90 | The build tooling for this project draws from: 91 | 92 | * [D3 Custom Bundle II](http://bl.ocks.org/mbostock/97557a39b4bfc8229786c8bccb54074d) 93 | * [rollup-starter-project](https://github.com/rollup/rollup-starter-project) 94 | 95 | Previous initiatives that feed into this work: 96 | 97 | * [chiasm-charts](https://github.com/chiasm-project/chiasm-charts) 98 | * [Model.js](https://github.com/curran/model) 99 | * [Reactive Data Visualizations (pdf)](https://github.com/curran/portfolio/raw/gh-pages/2015/reactiveVisualizationsPaper.pdf) 100 | 101 |

102 | 103 | 104 | 105 |

106 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reactive-vis", 3 | "version": "0.2.1", 4 | "description": "Reusable reactive data flows for data visualization.", 5 | "main": "index.js", 6 | "scripts": { 7 | "pretest": "rollup -c", 8 | "test": "mocha" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/curran/reactive-vis.git" 13 | }, 14 | "keywords": [ 15 | "visualization", 16 | "data", 17 | "reactive", 18 | "dataflow", 19 | "frp" 20 | ], 21 | "author": "Curran Kelleher", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/curran/reactive-vis/issues" 25 | }, 26 | "homepage": "https://github.com/curran/reactive-vis", 27 | "dependencies": { 28 | "d3-array": "^1.0.0", 29 | "d3-scale": "^0.9.2", 30 | "d3-selection": "^0.9.0", 31 | "d3-transition": "^1.0.0", 32 | "reactive-model": "0.12.0" 33 | }, 34 | "devDependencies": { 35 | "graph-diagrams": "^0.5.0", 36 | "jsdom": "9", 37 | "mocha": "^2.5.3", 38 | "rollup": "^0.31.2", 39 | "rollup-plugin-commonjs": "^3.0.0", 40 | "rollup-plugin-node-resolve": "^1.7.0", 41 | "source-map-support": "^0.4.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import npm from "rollup-plugin-node-resolve"; 2 | import commonjs from "rollup-plugin-commonjs"; 3 | 4 | export default { 5 | entry: "src/index.js", 6 | format: "umd", 7 | sourceMap: "inline", 8 | moduleName: "ReactiveVis", 9 | plugins: [ 10 | npm({ jsnext: true }), 11 | commonjs() 12 | ], 13 | dest: "build/reactive-vis.js" 14 | }; 15 | -------------------------------------------------------------------------------- /src/components/circle.js: -------------------------------------------------------------------------------- 1 | import ReactiveModel from "reactive-model"; 2 | import SVG from "../mixins/svg"; 3 | import Data from "../mixins/data"; 4 | 5 | export default function Circle(){ 6 | return ReactiveModel() 7 | .call(SVG) 8 | .call(Data) 9 | 10 | ("circle", function (svgSelection, data){ 11 | 12 | var circle = svgSelection.selectAll(".reactive-vis-circle") 13 | .data(data); 14 | 15 | return circle.enter().append("circle") 16 | .attr("class", "reactive-vis-circle") 17 | .merge(circle); 18 | 19 | }, "svgSelection, data"); 20 | } 21 | -------------------------------------------------------------------------------- /src/components/scatter.js: -------------------------------------------------------------------------------- 1 | import ReactiveModel from "reactive-model"; 2 | import SVG from "../mixins/svg"; 3 | import Data from "../mixins/data"; 4 | import Margin from "../mixins/margin"; 5 | import Column from "../mixins/column"; 6 | import Scale from "../mixins/scale"; 7 | 8 | import { extent, max } from "d3-array"; 9 | import { scaleLinear, scaleSqrt } from "d3-scale"; 10 | 11 | import { local } from "d3-selection"; 12 | 13 | export default function Circle(){ 14 | return ReactiveModel() 15 | .call(SVG) 16 | .call(Margin) 17 | .call(Data) 18 | 19 | .call(Column, "x") 20 | .call(Column, "y") 21 | .call(Column, "size") 22 | 23 | // The x scale. 24 | ("xDomain", function (data, accessor){ 25 | return extent(data, accessor); 26 | }, "data, xAccessor") 27 | ("xRange", function (innerWidth){ 28 | return [0, innerWidth]; 29 | }, "innerWidth") 30 | .call(Scale, "x", scaleLinear) 31 | 32 | // The y scale. 33 | ("yDomain", function (data, accessor){ 34 | return extent(data, accessor); 35 | }, "data, yAccessor") 36 | ("yRange", function (innerHeight){ 37 | return [innerHeight, 0]; 38 | }, "innerHeight") 39 | .call(Scale, "y", scaleLinear) 40 | 41 | // The size scale. 42 | ("sizeMax", 20) 43 | ("sizeDomain", function (data, accessor){ 44 | return [0, max(data, accessor)]; 45 | }, "data, sizeAccessor") 46 | ("sizeRange", function (sizeMax){ 47 | return [0, sizeMax]; 48 | }, "sizeMax") 49 | .call(Scale, "size", scaleSqrt) 50 | 51 | // This is the single SVG group for the scatter layer. 52 | ("scatterLayer", function (g){ 53 | 54 | var scatterLayer = g.selectAll(".reactive-vis-scatter-layer") 55 | .data([1]); 56 | 57 | return scatterLayer.enter().append("g") 58 | .attr("class", "reactive-vis-scatter-layer") 59 | .merge(scatterLayer); 60 | 61 | }, "g") 62 | 63 | // This is the selection of many g elements, corresponding to the data. 64 | ("marks", function (scatterLayer, data, xScaled, yScaled, sizeScaled){ 65 | 66 | var scatter = scatterLayer.selectAll(".reactive-vis-scatter-mark") 67 | .data(data); 68 | 69 | scatter.exit().remove(); 70 | 71 | var sizeLocal = local(); 72 | 73 | var marks = scatter.enter().append("g") 74 | .attr("class", "reactive-vis-scatter-mark") 75 | .merge(scatter) 76 | .attr("transform", function (d){ 77 | return "translate(" + xScaled(d) + "," + yScaled(d) + ")"; 78 | }) 79 | .each(function(d) { 80 | sizeLocal.set(this, sizeScaled(d)); 81 | }); 82 | 83 | marks.sizeLocal = sizeLocal; 84 | 85 | return marks; 86 | }, "scatterLayer, data, xScaled, yScaled, sizeScaled"); 87 | } 88 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import ReactiveModel from "reactive-model"; 2 | 3 | import "d3-transition"; 4 | 5 | // Mixins 6 | import SVG from "./mixins/svg"; 7 | import Margin from "./mixins/margin"; 8 | import Data from "./mixins/data"; 9 | import Column from "./mixins/column"; 10 | import Scale from "./mixins/scale"; 11 | import Resize from "./mixins/resize"; 12 | 13 | // Components 14 | import Circle from "./components/circle"; 15 | import Scatter from "./components/scatter"; 16 | 17 | // This needs to be used to construct ReactiveModel instances, 18 | // because it is the version that's bundled within ReactiveVis. 19 | // If an externally loaded instance of the ReactiveModel module 20 | // (from NPM) is used, the "digest" function won't work correctly, 21 | // because the two modules have different data flow graphs. 22 | export var Model = ReactiveModel; 23 | 24 | // Expose digest at this level for convenience. 25 | export var digest = ReactiveModel.digest; 26 | 27 | export { 28 | SVG, 29 | Margin, 30 | Data, 31 | Column, 32 | Scale, 33 | Resize, 34 | 35 | Circle, 36 | Scatter 37 | }; 38 | -------------------------------------------------------------------------------- /src/mixins/column.js: -------------------------------------------------------------------------------- 1 | export default function Column (my, name){ 2 | my(name + "Column") 3 | (name + "Accessor", function (column){ 4 | return function (d){ return d[column]; }; 5 | }, name + "Column"); 6 | } 7 | -------------------------------------------------------------------------------- /src/mixins/data.js: -------------------------------------------------------------------------------- 1 | export default function Data (my){ 2 | my("data"); 3 | } 4 | -------------------------------------------------------------------------------- /src/mixins/margin.js: -------------------------------------------------------------------------------- 1 | import { select } from "d3-selection"; 2 | 3 | // Encapsulates the margin convention. 4 | export default function Margin(my){ 5 | my("marginTop", 50) 6 | ("marginBottom", 50) 7 | ("marginLeft", 50) 8 | ("marginRight", 50) 9 | 10 | ("innerWidth", function (width, marginLeft, marginRight){ 11 | return width - marginLeft - marginRight; 12 | }, "width, marginLeft, marginRight") 13 | 14 | ("innerHeight", function (height, marginTop, marginBottom){ 15 | return height - marginTop - marginBottom; 16 | }, "height, marginTop, marginBottom") 17 | 18 | ("g", function (svgSelection){ 19 | 20 | var g = svgSelection.selectAll(".reactive-vis-margin-g") 21 | .data([1]); 22 | 23 | return g.enter().append("g") 24 | .attr("class", "reactive-vis-margin-g") 25 | .merge(g); 26 | 27 | }, "svgSelection") 28 | 29 | ("g-transform", function (g, marginLeft, marginTop){ 30 | g.attr("transform", "translate(" + marginLeft + "," + marginTop + ")"); 31 | }, "g, marginLeft, marginTop"); 32 | } 33 | -------------------------------------------------------------------------------- /src/mixins/resize.js: -------------------------------------------------------------------------------- 1 | // Respond to resize by setting width and height from DOM element. 2 | export default function Resize(my, el){ 3 | 4 | function resize(){ 5 | my.width(el.clientWidth) 6 | .height(el.clientHeight); 7 | } 8 | 9 | // Set initial size. 10 | resize(); 11 | 12 | // Guard so unit tests don't break in NodeJS environment. 13 | if(typeof window !== "undefined"){ 14 | 15 | // Update size when the browser window is resized. 16 | window.addEventListener("resize", resize); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/mixins/scale.js: -------------------------------------------------------------------------------- 1 | export default function Scale (my, name, scaleConstructor){ 2 | 3 | var scale = scaleConstructor(); 4 | 5 | my (name + "Scale", function(domain, range){ 6 | return scale 7 | .domain(domain) 8 | .range(range); 9 | }, [name + "Domain", name + "Range"]); 10 | 11 | my(name + "Scaled", function(scale, accessor){ 12 | return function (d){ 13 | return scale(accessor(d)); 14 | }; 15 | }, [name + "Scale", name + "Accessor"]); 16 | } 17 | -------------------------------------------------------------------------------- /src/mixins/svg.js: -------------------------------------------------------------------------------- 1 | import { select } from "d3-selection"; 2 | 3 | // Resizes the SVG container. 4 | export default function SVG (my){ 5 | my("svg") 6 | ("width", 960) 7 | ("height", 500) 8 | 9 | ("svgSelection", select, "svg") 10 | 11 | ("svg-width", function (svgSelection, width){ 12 | svgSelection.attr("width", width); 13 | }, "svgSelection, width") 14 | 15 | ("svg-height", function (svgSelection, height){ 16 | svgSelection.attr("height", height); 17 | }, "svgSelection, height"); 18 | } 19 | -------------------------------------------------------------------------------- /test/components/circleTest.js: -------------------------------------------------------------------------------- 1 | var ReactiveVis = require("../../build/reactive-vis.js"); 2 | var assert = require("assert"); 3 | 4 | module.exports = function (common){ 5 | var output = common.output; 6 | var createSVG = common.createSVG; 7 | 8 | describe("Circle", function (){ 9 | 10 | var exampleData = [{ 11 | "sepal_length": 5.1, 12 | "sepal_width": 3.5, 13 | "petal_length": 1.4, 14 | "petal_width": 0.2, 15 | "species": "setosa" 16 | }]; 17 | 18 | it("Should use SVG mixin.", function (){ 19 | var svg = createSVG(); 20 | var circle = ReactiveVis.Circle() 21 | .svg(svg); 22 | 23 | circle 24 | .width(100) 25 | .height(200); 26 | 27 | ReactiveVis.digest(); 28 | 29 | assert.equal(svg.getAttribute("width"), 100); 30 | assert.equal(svg.getAttribute("height"), 200); 31 | 32 | circle.destroy(); 33 | }); 34 | 35 | it("Should use Data mixin.", function (){ 36 | var circle = ReactiveVis.Circle() 37 | .data(exampleData); 38 | assert.deepEqual(circle.data(), exampleData); 39 | }); 40 | 41 | it("Should append a circle element", function (){ 42 | var svg = createSVG(); 43 | var circle = ReactiveVis.Circle() 44 | .svg(svg) 45 | .data(exampleData); 46 | 47 | ReactiveVis.digest(); 48 | 49 | assert.equal(svg.children.length, 1); 50 | assert.equal(svg.children[0].tagName, "circle"); 51 | }); 52 | 53 | }); 54 | }; 55 | -------------------------------------------------------------------------------- /test/components/scatterTest.js: -------------------------------------------------------------------------------- 1 | var ReactiveVis = require("../../build/reactive-vis.js"); 2 | var assert = require("assert"); 3 | 4 | module.exports = function (common){ 5 | var output = common.output; 6 | var createSVG = common.createSVG; 7 | 8 | describe("Scatter", function (){ 9 | 10 | var exampleData = [ 11 | { "sepal_length": 5.1, "sepal_width": 3.5, "petal_length": 1.4, "petal_width": 0.2, "species": "setosa" }, 12 | { "sepal_length": 5.2, "sepal_width": 2.7, "petal_length": 3.9, "petal_width": 1.4, "species": "versicolor" }, 13 | { "sepal_length": 6.3, "sepal_width": 3.4, "petal_length": 5.6, "petal_width": 2.4, "species": "virginica" }, 14 | { "sepal_length": 6.4, "sepal_width": 3.1, "petal_length": 5.5, "petal_width": 1.8, "species": "virginica" } 15 | ]; 16 | 17 | it("Should append scatterLayer.", function (){ 18 | var scatter = ReactiveVis.Scatter() 19 | .svg(createSVG()) 20 | .data(exampleData); 21 | 22 | ReactiveVis.digest(); 23 | 24 | var g = scatter.g().node(); 25 | var scatterLayer = scatter.scatterLayer().node(); 26 | 27 | assert.equal(g.children.length, 1); 28 | assert.equal(g.children[0].tagName, "g"); 29 | assert.equal(g.children[0], scatterLayer); 30 | 31 | scatter.destroy(); 32 | }); 33 | 34 | it("Should update scatterLayer.", function (){ 35 | var svg = createSVG(); 36 | var scatter = ReactiveVis.Scatter() 37 | .data(exampleData); 38 | 39 | scatter.svg(svg); 40 | ReactiveVis.digest(); 41 | var g = scatter.g().node(); 42 | var scatterLayer = scatter.scatterLayer().node(); 43 | assert.equal(g.children.length, 1); 44 | assert.equal(g.children[0].tagName, "g"); 45 | assert.equal(g.children[0], scatterLayer); 46 | 47 | scatter.svg(svg); 48 | ReactiveVis.digest(); 49 | assert.equal(g.children.length, 1); 50 | assert.equal(g.children[0].tagName, "g"); 51 | assert.equal(g.children[0], scatterLayer); 52 | 53 | scatter.destroy(); 54 | }); 55 | 56 | it("Should append g elements for each data entry.", function (){ 57 | var scatter = ReactiveVis.Scatter() 58 | .svg(createSVG()) 59 | .data(exampleData); 60 | 61 | scatter 62 | .xColumn("sepal_length") 63 | .yColumn("sepal_width") 64 | 65 | // TODO remove this when it becomes optional. 66 | .sizeColumn("petal_length"); 67 | 68 | ReactiveVis.digest(); 69 | 70 | var g = scatter.scatterLayer().node(); 71 | 72 | assert.equal(g.children.length, 4); 73 | assert.equal(g.children[0].tagName, "g"); 74 | scatter.destroy(); 75 | }); 76 | 77 | it("Should exit g elements.", function (){ 78 | var scatter = ReactiveVis.Scatter() 79 | .svg(createSVG()) 80 | .data(exampleData); 81 | 82 | scatter 83 | .xColumn("sepal_length") 84 | .yColumn("sepal_width") 85 | .sizeColumn("petal_length"); 86 | 87 | ReactiveVis.digest(); 88 | 89 | scatter.data(exampleData.slice(0, 3)); 90 | ReactiveVis.digest(); 91 | var g = scatter.scatterLayer().node(); 92 | assert.equal(g.children.length, 3); 93 | assert.equal(g.children[0].tagName, "g"); 94 | scatter.destroy(); 95 | }); 96 | 97 | it("Should set g transforms for each data entry.", function (){ 98 | var scatter = ReactiveVis.Scatter() 99 | .svg(createSVG()) 100 | .data(exampleData); 101 | 102 | scatter 103 | .xColumn("sepal_length") 104 | .yColumn("sepal_width") 105 | .sizeColumn("petal_length"); 106 | 107 | ReactiveVis.digest(); 108 | 109 | var g = scatter.scatterLayer().node(); 110 | 111 | assert.equal(g.children[0].getAttribute("transform"), "translate(0,0)"); 112 | assert.equal(g.children[1].getAttribute("transform"), "translate(66.15384615384647,400)"); 113 | 114 | scatter.destroy(); 115 | }); 116 | 117 | it("Should expose size using local.", function (){ 118 | var scatter = ReactiveVis.Scatter() 119 | .svg(createSVG()) 120 | .data(exampleData); 121 | 122 | scatter 123 | .xColumn("sepal_length") 124 | .yColumn("sepal_width") 125 | .sizeColumn("petal_length"); 126 | 127 | ReactiveVis.digest(); 128 | 129 | var marks = scatter.marks(); 130 | marks.each(function (d, i){ 131 | var size = marks.sizeLocal.get(this); 132 | assert.equal(size, scatter.sizeScaled()(d)); 133 | }); 134 | 135 | //assert.equal(g.children[1].getAttribute("transform"), "translate(66.15384615384647,400)"); 136 | 137 | scatter.destroy(); 138 | }); 139 | 140 | }); 141 | }; 142 | -------------------------------------------------------------------------------- /test/composition/nested.js: -------------------------------------------------------------------------------- 1 | var ReactiveVis = require("../../build/reactive-vis.js"); 2 | var assert = require("assert"); 3 | 4 | module.exports = function (common){ 5 | var output = common.output; 6 | var createSVG = common.createSVG; 7 | 8 | describe("Nested", function (){ 9 | 10 | var exampleData = [ 11 | { "sepal_length": 5.1, "sepal_width": 3.5, "petal_length": 1.4, "petal_width": 0.2, "species": "setosa" }, 12 | { "sepal_length": 5.2, "sepal_width": 2.7, "petal_length": 3.9, "petal_width": 1.4, "species": "versicolor" }, 13 | { "sepal_length": 6.3, "sepal_width": 3.4, "petal_length": 5.6, "petal_width": 2.4, "species": "virginica" }, 14 | { "sepal_length": 6.4, "sepal_width": 3.1, "petal_length": 5.5, "petal_width": 1.8, "species": "virginica" } 15 | ]; 16 | 17 | it("Should nest Circle in Scatter.", function (){ 18 | 19 | var svg = createSVG(); 20 | 21 | var circle = ReactiveVis.Circle(); 22 | 23 | var scatter = ReactiveVis.Scatter() 24 | .svg(svg) 25 | .data(exampleData); 26 | 27 | scatter 28 | .xColumn("sepal_length") 29 | .yColumn("sepal_width") 30 | .sizeColumn("petal_length"); 31 | 32 | scatter(function (marks){ 33 | marks.each(function (d){ 34 | var size = marks.sizeLocal.get(this); 35 | circle 36 | .data([d]) 37 | .width(size) 38 | .height(size) 39 | .svg(this) 40 | .digest(); 41 | }); 42 | }, "marks"); 43 | 44 | ReactiveVis.digest(); 45 | 46 | assert.equal(svg.children.length, 1); 47 | assert.equal(svg.children[0].tagName, "g"); 48 | assert.equal(svg.children[0].getAttribute("class"), "reactive-vis-margin-g"); 49 | 50 | var g = svg.children[0]; 51 | assert.equal(g.children.length, 1); 52 | assert.equal(g.children[0].tagName, "g"); 53 | assert.equal(g.children[0].getAttribute("class"), "reactive-vis-scatter-layer"); 54 | 55 | var scatterLayer = g.children[0]; 56 | assert.equal(scatterLayer.children.length, 4); 57 | assert.equal(scatterLayer.children[0].tagName, "g"); 58 | assert.equal(scatterLayer.children[0].getAttribute("class"), "reactive-vis-scatter-mark"); 59 | assert.equal(scatterLayer.children[0].getAttribute("transform"), "translate(0,0)"); 60 | 61 | assert.equal(scatterLayer.children[1].tagName, "g"); 62 | assert.equal(scatterLayer.children[1].getAttribute("class"), "reactive-vis-scatter-mark"); 63 | assert.equal(scatterLayer.children[1].getAttribute("transform"), "translate(66.15384615384647,400)"); 64 | 65 | var mark0 = scatterLayer.children[0]; 66 | assert.equal(mark0.children.length, 1); 67 | assert.equal(mark0.children[0].tagName, "circle"); 68 | assert.equal(mark0.children[0].getAttribute("class"), "reactive-vis-circle"); 69 | 70 | // This can be used to output the SVG. 71 | //console.log(svg.outerHTML); 72 | 73 | 74 | }); 75 | }); 76 | }; 77 | -------------------------------------------------------------------------------- /test/mixins/columnTest.js: -------------------------------------------------------------------------------- 1 | var ReactiveVis = require("../../build/reactive-vis.js"); 2 | var assert = require("assert"); 3 | 4 | module.exports = function (common){ 5 | var output = common.output; 6 | 7 | describe("Column", function(){ 8 | 9 | it("Should create a column property with name prefix.", function(){ 10 | var my = ReactiveVis.Model() 11 | .call(ReactiveVis.Data) 12 | .call(ReactiveVis.Column, "x"); 13 | 14 | my.xColumn("foo"); 15 | assert.equal(my.xColumn(), "foo"); 16 | 17 | output("column"); 18 | my.destroy(); 19 | }); 20 | 21 | it("Should create an accessor property with name prefix.", function(){ 22 | var my = ReactiveVis.Model() 23 | .call(ReactiveVis.Data) 24 | .call(ReactiveVis.Column, "x"); 25 | 26 | my.xColumn("foo"); 27 | ReactiveVis.digest(); 28 | assert.equal(my.xAccessor()({ foo: "bar" }), "bar"); 29 | 30 | my.destroy(); 31 | }); 32 | }); 33 | }; 34 | -------------------------------------------------------------------------------- /test/mixins/dataTest.js: -------------------------------------------------------------------------------- 1 | var ReactiveVis = require("../../build/reactive-vis.js"); 2 | var assert = require("assert"); 3 | 4 | module.exports = function (common){ 5 | var output = common.output; 6 | 7 | describe("Data", function(){ 8 | it("Should create a property called 'data'", function(){ 9 | var my = ReactiveVis.Model() 10 | .call(ReactiveVis.Data); 11 | 12 | my.data([1, 2, 3]); 13 | assert.deepEqual(my.data(), [1, 2, 3]); 14 | 15 | output("data"); 16 | my.destroy(); 17 | }); 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /test/mixins/marginTest.js: -------------------------------------------------------------------------------- 1 | var ReactiveVis = require("../../build/reactive-vis.js"); 2 | var assert = require("assert"); 3 | 4 | module.exports = function (common){ 5 | var output = common.output; 6 | var createSVG = common.createSVG; 7 | 8 | describe("Margin", function(){ 9 | 10 | it("Should compute innerWidth and innerHeight", function(){ 11 | var my = ReactiveVis.Model() 12 | .call(ReactiveVis.SVG) 13 | .call(ReactiveVis.Margin) 14 | .svg(createSVG()); 15 | 16 | ReactiveVis.digest(); 17 | assert(my.innerWidth(), my.width() - my.width() - my.marginLeft() - my.marginRight()); 18 | assert(my.innerHeight(), my.height() - my.marginTop() - my.marginBottom()); 19 | 20 | my 21 | .width(100) 22 | .height(200) 23 | .marginTop(10) 24 | .marginBottom(11) 25 | .marginLeft(12) 26 | .marginRight(13) 27 | 28 | ReactiveVis.digest(); 29 | assert(my.innerWidth(), my.width() - my.width() - my.marginLeft() - my.marginRight()); 30 | assert(my.innerHeight(), my.height() - my.marginTop() - my.marginBottom()); 31 | 32 | output("margin"); 33 | my.destroy(); 34 | }); 35 | 36 | it("Should append g to svg", function(){ 37 | var my = new ReactiveVis.Model() 38 | .call(ReactiveVis.SVG) 39 | .call(ReactiveVis.Margin) 40 | .svg(createSVG()); 41 | 42 | ReactiveVis.digest(); 43 | assert.equal(my.g().node().tagName, "g"); 44 | assert(my.innerHeight(), my.height() - my.marginTop() - my.marginBottom()); 45 | 46 | my 47 | .width(100) 48 | .height(200) 49 | .marginTop(10) 50 | .marginBottom(11) 51 | .marginLeft(12) 52 | .marginRight(13) 53 | 54 | ReactiveVis.digest(); 55 | assert(my.innerWidth(), my.width() - my.width() - my.marginLeft() - my.marginRight()); 56 | assert(my.innerHeight(), my.height() - my.marginTop() - my.marginBottom()); 57 | }); 58 | 59 | it("Should set g transform", function(){ 60 | var my = new ReactiveVis.Model() 61 | .call(ReactiveVis.SVG) 62 | .call(ReactiveVis.Margin) 63 | .svg(createSVG()); 64 | 65 | ReactiveVis.digest(); 66 | assert.equal(my.g().attr("transform"), "translate(50,50)"); 67 | 68 | my.marginTop(10).marginLeft(12); 69 | 70 | ReactiveVis.digest(); 71 | assert.equal(my.g().attr("transform"), "translate(12,10)"); 72 | }); 73 | 74 | it("Should select existing g if already on svg", function(){ 75 | 76 | var svg = createSVG(); 77 | 78 | var my = new ReactiveVis.Model() 79 | .call(ReactiveVis.SVG) 80 | .call(ReactiveVis.Margin); 81 | 82 | my.svg(svg); 83 | ReactiveVis.digest(); 84 | assert.equal(svg.children.length, 1); 85 | 86 | my.svg(svg); 87 | ReactiveVis.digest(); 88 | assert.equal(svg.children.length, 1); 89 | 90 | // Covers the .merge line in "g" definition. 91 | my.marginTop(10).marginLeft(12); 92 | ReactiveVis.digest(); 93 | assert.equal(my.g().attr("transform"), "translate(12,10)"); 94 | 95 | }); 96 | }); 97 | }; 98 | -------------------------------------------------------------------------------- /test/mixins/resizeTest.js: -------------------------------------------------------------------------------- 1 | var ReactiveVis = require("../../build/reactive-vis.js"); 2 | var assert = require("assert"); 3 | var jsdom = require("jsdom"); 4 | var d3 = require("d3-selection"); 5 | 6 | module.exports = function (common){ 7 | var createSVG = common.createSVG; 8 | describe("Resize", function(){ 9 | 10 | it("Should set width and height from clientWidth and clientHeight", function(){ 11 | 12 | var container = d3.select(jsdom.jsdom().body) 13 | .append("div") 14 | .node(); 15 | 16 | container.clientWidth = 234; 17 | container.clientHeight = 567; 18 | 19 | var my = ReactiveVis.Model() 20 | .call(ReactiveVis.SVG) 21 | .call(ReactiveVis.Resize, container); 22 | 23 | assert.equal(my.width(), container.clientWidth); 24 | assert.equal(my.height(), container.clientHeight); 25 | 26 | my.destroy(); 27 | }); 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /test/mixins/scaleTest.js: -------------------------------------------------------------------------------- 1 | var ReactiveVis = require("../../build/reactive-vis.js"); 2 | var assert = require("assert"); 3 | var d3 = require("d3-scale"); 4 | 5 | module.exports = function (common){ 6 | var output = common.output; 7 | 8 | describe("Scale", function(){ 9 | 10 | it("Should create a property with name prefix.", function(){ 11 | var my = ReactiveVis.Model() 12 | .call(ReactiveVis.Data) 13 | .call(ReactiveVis.Column, "x") 14 | 15 | ("xDomain", [5, 10]) 16 | ("xRange", [0, 100]) 17 | .call(ReactiveVis.Scale, "x", d3.scaleLinear) 18 | 19 | ReactiveVis.digest(); 20 | 21 | assert.equal(typeof my.xScale(), "function"); 22 | assert.equal(my.xScale()(7), 40); 23 | 24 | my.destroy(); 25 | }); 26 | 27 | it("Should create a (name + \"Scaled\") property.", function(){ 28 | var my = ReactiveVis.Model() 29 | .call(ReactiveVis.Data) 30 | .call(ReactiveVis.Column, "x") 31 | 32 | ("xDomain", [5, 10]) 33 | ("xRange", [0, 100]) 34 | .call(ReactiveVis.Scale, "x", d3.scaleLinear) 35 | 36 | my.xColumn("foo"); 37 | 38 | ReactiveVis.digest(); 39 | 40 | assert.equal(typeof my.xScaled(), "function"); 41 | assert.equal(my.xScaled()({ foo: 7 }), 40); 42 | 43 | my.destroy(); 44 | }); 45 | 46 | it("Should support sqrt scale.", function(){ 47 | var my = ReactiveVis.Model() 48 | .call(ReactiveVis.Data) 49 | .call(ReactiveVis.Column, "x") 50 | 51 | ("xDomain", [0, 16]) 52 | ("xRange", [0, 256]) 53 | .call(ReactiveVis.Scale, "x", d3.scaleSqrt); 54 | 55 | my.xColumn("foo"); 56 | 57 | ReactiveVis.digest(); 58 | 59 | assert.equal(typeof my.xScale(), "function"); 60 | assert.equal(my.xScale()(4), 128); 61 | assert.equal(my.xScaled()({ foo: 4 }), 128); 62 | 63 | my.destroy(); 64 | }); 65 | 66 | }); 67 | }; 68 | -------------------------------------------------------------------------------- /test/mixins/svgTest.js: -------------------------------------------------------------------------------- 1 | var ReactiveVis = require("../../build/reactive-vis.js"); 2 | var assert = require("assert"); 3 | 4 | module.exports = function (common){ 5 | var output = common.output; 6 | var createSVG = common.createSVG; 7 | 8 | describe("SVG", function(){ 9 | it("Should set width and height", function(){ 10 | var svg = createSVG(); 11 | 12 | var my = ReactiveVis.Model() 13 | .call(ReactiveVis.SVG) 14 | .svg(svg) 15 | 16 | ReactiveVis.digest(); 17 | assert.equal(svg.getAttribute("width"), 960); 18 | assert.equal(svg.getAttribute("height"), 500); 19 | 20 | my 21 | .width(100) 22 | .height(200); 23 | 24 | ReactiveVis.digest(); 25 | assert.equal(svg.getAttribute("width"), 100); 26 | assert.equal(svg.getAttribute("height"), 200); 27 | 28 | output("svg"); 29 | my.destroy(); 30 | }); 31 | 32 | it("Should expose svgSelection (with .transition).", function(){ 33 | var svg = createSVG(); 34 | 35 | var my = ReactiveVis.Model() 36 | .call(ReactiveVis.SVG) 37 | .svg(svg) 38 | 39 | ReactiveVis.digest(); 40 | assert.equal(typeof my.svgSelection, "function"); 41 | 42 | my.destroy(); 43 | }); 44 | }); 45 | }; 46 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var d3 = require("d3-selection"); 2 | var ReactiveVis = require("../build/reactive-vis.js"); 3 | var jsdom = require("jsdom"); 4 | var fs = require("fs"); 5 | 6 | // This causes correct line numbers to be shown 7 | // when errors from source files occur in tests. 8 | require("source-map-support").install(); 9 | 10 | // If outputGraph = true, writes graph files to ../graph-diagrams for visualization. 11 | var outputGraph = require("graph-diagrams")({ 12 | outputGraphs: false, 13 | project: "reactive-vis" 14 | }); 15 | 16 | // These are utilities available to all tests. 17 | var common = { 18 | 19 | // Convenience function to output graphs for visualization. 20 | output: function (name){ 21 | outputGraph(ReactiveVis.Model.serializeGraph(), name); 22 | }, 23 | 24 | // Creates and returns an SVG element using jsdom. 25 | createSVG: function createSVG(){ 26 | return d3.select(jsdom.jsdom().body) 27 | .append("svg") 28 | .node(); 29 | } 30 | }; 31 | 32 | describe("Mixins", function (){ 33 | runTests("mixins"); 34 | }); 35 | 36 | describe("Components", function (){ 37 | runTests("components"); 38 | }); 39 | 40 | describe("Composition", function (){ 41 | runTests("composition"); 42 | }); 43 | 44 | function runTests(subdirectory){ 45 | fs.readdirSync("test/" + subdirectory).forEach(function (testFile){ 46 | if(testFile.indexOf(".swp") === -1){ 47 | require("./" + subdirectory + "/" + testFile)(common); 48 | } 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /test/visual/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Reactive-Vis Visual Test 7 | 8 | 9 | 10 | 11 | 22 | 23 | 24 | 25 |
26 | 27 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /test/visual/scatterPlotData.csv: -------------------------------------------------------------------------------- 1 | sepal_length,sepal_width,petal_length,petal_width,class 2 | 5.0,3.4,1.5,0.2,setosa 3 | 4.8,3.4,1.6,0.2,setosa 4 | 5.1,3.5,1.4,0.3,setosa 5 | 5.4,3.4,1.7,0.2,setosa 6 | 5.0,3.4,1.6,0.4,setosa 7 | 4.4,3.0,1.3,0.2,setosa 8 | 5.0,3.5,1.3,0.3,setosa 9 | 4.6,3.2,1.4,0.2,setosa 10 | 6.9,3.1,4.9,1.5,versicolor 11 | 5.9,3.0,4.2,1.5,versicolor 12 | 5.8,2.7,4.1,1.0,versicolor 13 | 6.1,2.8,4.0,1.3,versicolor 14 | 5.7,2.6,3.5,1.0,versicolor 15 | 5.5,2.4,3.8,1.1,versicolor 16 | 5.6,3.0,4.1,1.3,versicolor 17 | 5.8,2.6,4.0,1.2,versicolor 18 | 5.1,2.5,3.0,1.1,versicolor 19 | 5.8,2.7,5.1,1.9,virginica 20 | 7.3,2.9,6.3,1.8,virginica 21 | 6.8,3.0,5.5,2.1,virginica 22 | 6.0,2.2,5.0,1.5,virginica 23 | 5.6,2.8,4.9,2.0,virginica 24 | 6.3,2.8,5.1,1.5,virginica 25 | 6.9,3.1,5.4,2.1,virginica 26 | 6.2,3.4,5.4,2.3,virginica 27 | --------------------------------------------------------------------------------