├── .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 | [](https://npmjs.org/package/reactive-vis)
6 | [](https://npmjs.org/package/reactive-vis)
7 | [](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 |
--------------------------------------------------------------------------------