An example set of coordinated views that use Crossfilter and Chiasm.
The idea with this is to introduce a general Crossfilter component that can be set up to work with any visualization components in Chiasm.
Draws from:
An example set of coordinated views that use Crossfilter and Chiasm.
The idea with this is to introduce a general Crossfilter component that can be set up to work with any visualization components in Chiasm.
Draws from:
| // This is an example Chaism plugin that uses D3 to make a bar chart. | |
| // Draws from this Bar Chart example http://bl.ocks.org/mbostock/3885304 | |
| function BarChart() { | |
| var my = ChiasmComponent({ | |
| margin: { | |
| left: 20, | |
| top: 40, | |
| right: 20, | |
| bottom: 20 | |
| }, | |
| yColumn: Model.None, | |
| xColumn: Model.None, | |
| // These properties adjust spacing between bars. | |
| // The names correspond to the arguments passed to | |
| // d3.scale.ordinal.rangeRoundBands(interval[, padding[, outerPadding]]) | |
| // https://github.com/mbostock/d3/wiki/Ordinal-Scales#ordinal_rangeRoundBands | |
| barPadding: 0.1, | |
| barOuterPadding: 0.1, | |
| fill: "#a3a3a3", | |
| stroke: "none", | |
| strokeWidth: "1px", | |
| brushEnabled: false, | |
| brushIntervalX: Model.None, | |
| title: "", | |
| titleSize: "1.5em", | |
| titleOffset: "-0.3em" | |
| }); | |
| var yScale = d3.scale.linear(); | |
| // This scale is for the bars to use. | |
| var xScale = d3.scale.ordinal(); | |
| // This scale is for the brush to use. | |
| var xScaleLinear = d3.scale.linear(); | |
| var brush = d3.svg.brush() | |
| .x(xScaleLinear) | |
| .on("brush", onBrush); | |
| my.el = document.createElement("div"); | |
| var svg = d3.select(my.el).append("svg"); | |
| var g = svg.append("g"); | |
| var titleText = g.append("text"); | |
| var barsG = g.append("g"); | |
| var brushG = g.append("g").attr("class", "brush"); | |
| function onBrush() { | |
| my.brushIntervalX = brush.empty() ? Model.None : brush.extent(); | |
| } | |
| my.when("title", titleText.text, titleText); | |
| my.when("titleSize", function (titleSize){ | |
| titleText.style("font-size", titleSize); | |
| }); | |
| my.when("titleOffset", function (titleOffset){ | |
| titleText.attr("dy", titleOffset); | |
| }); | |
| // Respond to changes in size and margin. | |
| // Inspired by D3 margin convention from http://bl.ocks.org/mbostock/3019563 | |
| my.when(["box", "margin"], function(box, margin){ | |
| my.innerBox = { | |
| width: box.width - margin.left - margin.right, | |
| height: box.height - margin.top - margin.bottom | |
| }; | |
| svg | |
| .attr("width", box.width) | |
| .attr("height", box.height); | |
| g.attr("transform", "translate(" + margin.left + "," + margin.top + ")"); | |
| }); | |
| my.when(["data", "xColumn", "innerBox", "barPadding", "barOuterPadding"], | |
| function (data, xColumn, innerBox, barPadding, barOuterPadding){ | |
| var xAccessor = function (d){ return d[xColumn]; }; | |
| xScale | |
| .domain(data.map(xAccessor)) | |
| .rangeRoundBands([0, innerBox.width], barPadding, barOuterPadding); | |
| xScaleLinear | |
| .domain(d3.extent(data, xAccessor)) | |
| .range([0, innerBox.width]); | |
| my.x = function(d) { return xScale(xAccessor(d)); } | |
| my.width = xScale.rangeBand(); | |
| }); | |
| my.when(["data", "yColumn", "innerBox"], | |
| function (data, yColumn, innerBox){ | |
| var yAccessor = function (d){ return d[yColumn]; }; | |
| yScale | |
| .domain([0, d3.max(data, yAccessor)]) | |
| .range([innerBox.height, 0]); | |
| my.y = function(d) { return yScale(yAccessor(d)); }; | |
| my.height = function(d) { return innerBox.height - my.y(d); }; | |
| }); | |
| my.when(["data", "x", "y", "width", "height", "fill", "stroke", "strokeWidth"], | |
| function (data, x, y, width, height, fill, stroke, strokeWidth){ | |
| var bars = barsG.selectAll("rect").data(data); | |
| bars.enter().append("rect"); | |
| bars | |
| .attr("x", x) | |
| .attr("width", width) | |
| .attr("y", y) | |
| .attr("height", height) | |
| .attr("fill", fill) | |
| .attr("stroke", stroke) | |
| .attr("stroke-width", strokeWidth); | |
| bars.exit().remove(); | |
| }); | |
| my.when(["brushIntervalX", "innerBox", "x", "y"], | |
| function (brushIntervalX, innerBox){ | |
| if(brushIntervalX !== Model.None){ | |
| //brush.extent(parseDates(brushIntervalX)); | |
| // Uncomment this to see what the brush interval is as you drag. | |
| //console.log(brushIntervalX.map(function (date){ | |
| // return date.toUTCString(); | |
| //})); | |
| } | |
| brushG.call(brush); | |
| brushG.selectAll("rect") | |
| .attr("y", 0) | |
| .attr("height", innerBox.height); | |
| }); | |
| return my; | |
| } |
| // This function defines a Chiasm component that exposes a Crossfilter instance | |
| // to visualizations via the Chaism configuration. | |
| function ChiasmCrossfilter() { | |
| var my = new ChiasmComponent({ | |
| groups: Model.None | |
| }); | |
| var listeners = []; | |
| my.when(["data", "groups"], function (data, groups){ | |
| if(groups !== Model.None) { | |
| var cf = crossfilter(data); | |
| var updateFunctions = []; | |
| listeners.forEach(my.cancel); | |
| listeners = Object.keys(groups).map(function (groupName){ | |
| var group = groups[groupName]; | |
| var dimension = group.dimension; | |
| var cfDimension = cf.dimension(function (d){ return d[dimension]; }); | |
| // Generate an aggregate function by parsing the "aggregation" config option. | |
| var aggregate; | |
| if(group.aggregation === "day"){ | |
| aggregate = d3.time.day; | |
| } else if(group.aggregation.indexOf("floor") === 0){ | |
| var interval = parseInt(group.aggregation.substr(6)); | |
| aggregate = function(d) { | |
| return Math.floor(d / interval) * interval; | |
| }; | |
| } | |
| var cfGroup = cfDimension.group(aggregate); | |
| var updateMyGroup = function (){ | |
| my[groupName] = cfGroup.all(); | |
| }; | |
| updateFunctions.push(updateMyGroup); | |
| updateMyGroup(); | |
| return my.when(dimension + "Filter", function (extent){ | |
| if(extent !== Model.None){ | |
| cfDimension.filterRange(extent); | |
| } else { | |
| cfDimension.filterAll(); | |
| } | |
| updateFunctions.forEach(function (updateFunction){ | |
| if(updateFunction !== updateMyGroup){ | |
| updateFunction(); | |
| } | |
| }); | |
| }); | |
| }); | |
| } | |
| }); | |
| return my; | |
| } |
| function ChiasmCSVLoader (){ | |
| var my = ChiasmComponent({ | |
| path: Model.None | |
| }); | |
| var parseFunctions = { | |
| number: parseFloat | |
| }; | |
| function generateColumnParsers(metadata) { | |
| if("columns" in metadata){ | |
| return metadata.columns | |
| .filter(function (column){ | |
| return column.type !== "string"; | |
| }) | |
| .map(function (column){ | |
| var parse = parseFunctions[column.type]; | |
| var name = column.name; | |
| return function (d){ | |
| d[name] = parse(d[name]); | |
| } | |
| }); | |
| } else { | |
| return []; | |
| } | |
| } | |
| my.when("path", function (path){ | |
| if(path !== Model.None){ | |
| d3.json(path + ".json", function(error, metadata) { | |
| var columnParsers = generateColumnParsers(metadata); | |
| var numColumns = columnParsers.length; | |
| function type (d){ | |
| // Old school for loop as an optimization. | |
| for(var i = 0; i < numColumns; i++){ | |
| // Each column parser function mutates the row object, | |
| // replacing the column property string with its parsed variant. | |
| columnParsers[i](d); | |
| } | |
| return d; | |
| } | |
| d3.csv(path + ".csv", type, function(error, data) { | |
| my.data = data; | |
| }); | |
| }); | |
| } | |
| }); | |
| return my; | |
| } |
| { | |
| "columns": [ | |
| { "name": "date", "type": "string" }, | |
| { "name": "delay", "type": "number" }, | |
| { "name": "distance", "type": "number" }, | |
| { "name": "origin", "type": "string" }, | |
| { "name": "destination", "type": "string" } | |
| ] | |
| } |
| // This does custom data preprocessing for the flight data. | |
| // Modified from Crossfilter example code: https://github.com/square/crossfilter/blob/gh-pages/index.html#L231 | |
| function FlightsPreprocessor() { | |
| var my = new ChiasmComponent(); | |
| my.when("dataIn", function (dataIn){ | |
| my.dataOut = dataIn.map(function (d){ | |
| d.date = parseDate(d.date); | |
| d.hour = d.date.getHours() + d.date.getMinutes() / 60; | |
| d.delay = Math.max(-60, Math.min(149, d.delay)); | |
| d.distance = Math.min(1999, d.distance); | |
| return d; | |
| }); | |
| }); | |
| function parseDate(d) { | |
| return new Date(2001, | |
| d.substring(0, 2) - 1, | |
| d.substring(2, 4), | |
| d.substring(4, 6), | |
| d.substring(6, 8)); | |
| } | |
| return my; | |
| } |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta charset="utf-8"> | |
| <title>Chiasm Crossfilter Integration</title> | |
| <!-- A functional reactive model library. See github.com/curran/model --> | |
| <script src="http://curran.github.io/model/cdn/model-v0.2.4.js"></script> | |
| <!-- The common base for Chiasm components (depends on Model.js). --> | |
| <script src="http://chiasm-project.github.io/chiasm-component/chiasm-component-v0.2.1.js"></script> | |
| <!-- This script defines the BarChart component. --> | |
| <script src="barChart.js"> </script> | |
| <!-- Load Crossfilter and the Crossfilter Chiasm component. --> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/crossfilter/1.3.12/crossfilter.min.js"></script> | |
| <script src="chiasm-crossfilter.js"></script> | |
| <!-- This plugin loads CSV data sets. --> | |
| <script src="chiasm-csv-loader.js"></script> | |
| <!-- This does custom data preprocessing for the flight data. --> | |
| <script src="flights-preprocessor.js"></script> | |
| <!-- Chiasm.js depends on Model.js, Lodash.js, D3.js. --> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.10.1/lodash.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js"></script> | |
| <!-- Chiasm.js and plugins. See github.com/chiasm-project --> | |
| <script src="http://chiasm-project.github.io/chiasm/chiasm-v0.2.0.js"></script> | |
| <script src="http://chiasm-project.github.io/chiasm-layout/chiasm-layout-v0.2.2.js"></script> | |
| <script src="http://chiasm-project.github.io/chiasm-links/chiasm-links-v0.2.1.js"></script> | |
| <!-- Make the Chiasm container fill the page and have a 20px black border. --> | |
| <style> | |
| body { | |
| background-color: black; | |
| } | |
| #chiasm-container { | |
| background-color: white; | |
| position: fixed; | |
| left: 20px; | |
| right: 20px; | |
| top: 20px; | |
| bottom: 20px; | |
| } | |
| /* Style the brush. Draws from http://bl.ocks.org/mbostock/4343214 */ | |
| .brush .extent { | |
| stroke: black; | |
| fill-opacity: .2; | |
| shape-rendering: crispEdges; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Chiasm component DOM elements will be injected into this div. --> | |
| <div id="chiasm-container"></div> | |
| <!-- This is the main program that sets up the Chiasm application. --> | |
| <script> | |
| // Create a new Chiasm instance. | |
| var chiasm = new Chiasm(); | |
| // Register plugins that the configuration can access. | |
| chiasm.plugins.layout = ChiasmLayout; | |
| chiasm.plugins.links = ChiasmLinks; | |
| chiasm.plugins.barChart = BarChart; | |
| chiasm.plugins.csvLoader = ChiasmCSVLoader; | |
| chiasm.plugins.flightsPreprocessor = FlightsPreprocessor; | |
| chiasm.plugins.crossfilter = ChiasmCrossfilter; | |
| // Set the Chaism configuration. | |
| chiasm.setConfig({ | |
| "layout": { | |
| "plugin": "layout", | |
| "state": { | |
| "containerSelector": "#chiasm-container", | |
| "layout": { | |
| "orientation": "vertical", | |
| "children": [ | |
| { | |
| "orientation": "horizontal", | |
| "children": [ | |
| "hour-chart", | |
| "delay-chart", | |
| "distance-chart" | |
| ] | |
| }, | |
| "date-chart" | |
| ] | |
| }, | |
| "sizes": { | |
| "distance-chart": { | |
| "size": 1.5 | |
| } | |
| } | |
| } | |
| }, | |
| "hour-chart": { | |
| "plugin": "barChart", | |
| "state": { | |
| "title": "Time of Day", | |
| "yColumn": "value", | |
| "xColumn": "key" | |
| } | |
| }, | |
| "delay-chart": { | |
| "plugin": "barChart", | |
| "state": { | |
| "title": "Arrival Delay (min.)", | |
| "yColumn": "value", | |
| "xColumn": "key" | |
| } | |
| }, | |
| "distance-chart": { | |
| "plugin": "barChart", | |
| "state": { | |
| "title": "Distance (mi.)", | |
| "yColumn": "value", | |
| "xColumn": "key" | |
| } | |
| }, | |
| "date-chart": { | |
| "plugin": "barChart", | |
| "state": { | |
| "title": "Date", | |
| "yColumn": "value", | |
| "xColumn": "key" | |
| } | |
| }, | |
| "flights-dataset": { | |
| "plugin": "csvLoader", | |
| "state": { | |
| // for development | |
| //"path": "../ca04856ca6afbc563957/flights-3m" | |
| // for production | |
| "path": "http://bl.ocks.org/curran/raw/ca04856ca6afbc563957/flights-3m" | |
| } | |
| }, | |
| "flights-preprocessor": { | |
| "plugin": "flightsPreprocessor" | |
| }, | |
| "flights-crossfilter": { | |
| "plugin": "crossfilter", | |
| "state": { | |
| "groups": { | |
| "dates": { | |
| "dimension": "date", | |
| "aggregation": "day" | |
| }, | |
| "hours": { | |
| "dimension": "hour", | |
| "aggregation": "floor 1" | |
| }, | |
| "delays": { | |
| "dimension": "delay", | |
| "aggregation": "floor 10" | |
| }, | |
| "distances": { | |
| "dimension": "distance", | |
| "aggregation": "floor 50" | |
| } | |
| } | |
| } | |
| }, | |
| "links": { | |
| "plugin": "links", | |
| "state": { | |
| "bindings": [ | |
| "flights-dataset.data -> flights-preprocessor.dataIn", | |
| "flights-preprocessor.dataOut -> flights-crossfilter.data", | |
| "flights-crossfilter.hours -> hour-chart.data", | |
| "flights-crossfilter.delays -> delay-chart.data", | |
| "flights-crossfilter.distances -> distance-chart.data", | |
| "flights-crossfilter.dates -> date-chart.data", | |
| "hour-chart.brushIntervalX -> flights-crossfilter.hourFilter", | |
| "delay-chart.brushIntervalX -> flights-crossfilter.delayFilter", | |
| "distance-chart.brushIntervalX -> flights-crossfilter.distanceFilter", | |
| "date-chart.brushIntervalX -> flights-crossfilter.dateFilter" | |
| ] | |
| } | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> |
| The MIT License (MIT) | |
| Copyright (c) 2015 Curran Kelleher | |
| Permission is hereby granted, free of charge, to any person obtaining a copy | |
| of this software and associated documentation files (the "Software"), to deal | |
| in the Software without restriction, including without limitation the rights | |
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| copies of the Software, and to permit persons to whom the Software is | |
| furnished to do so, subject to the following conditions: | |
| The above copyright notice and this permission notice shall be included in all | |
| copies or substantial portions of the Software. | |
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| SOFTWARE. | |