Skip to content

Instantly share code, notes, and snippets.

@curran
Last active April 27, 2016 12:21
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save curran/19d42e98ce25291eb45d to your computer and use it in GitHub Desktop.
Save curran/19d42e98ce25291eb45d to your computer and use it in GitHub Desktop.
Data Canvas Part 8 - Zooming

This program makes a scatter plot (with lines) and line chart from data in the Data Canvas - Sense Your City API and makes use of the Chiasm visualization runtime engine.

The line chart shows the temperature for all cities with available data. The program constantly fetches more data going back in time, in invervals of 24 hours with a resolution of 10 minutes.

You can use the bottom line chart to zoom in with the top line chart. This approach is inspired by the D3 Focus+Context Via Brushing example. Brushing in the top line chart causes data for only the brushed region to be shown in the scatter plot. This allows you to focus on specific regions of time.

Draws from

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<!--
A data visualization editor.
Curran Kelleher March 2015
-->
<title>Visualization Editor</title>
<link rel="stylesheet" href="//curran.github.io/cdn/inlet/inlet.css">
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/codemirror/5.0.0/codemirror.css">
<link rel="stylesheet" href="styles.css">
<style>
</style>
</head>
<body>
<!-- The container for the runtime environment. -->
<div id="container"></div>
<!-- The gear icon in the upper right. -->
<!-- Image from http://simpleicon.com/gear-12.html -->
<img id="gear" src="gear.png">
<!-- Use RequireJS for module loading. -->
<script src="//cdnjs.cloudflare.com/ajax/libs/require.js/2.1.16/require.min.js"></script>
<!-- Configure AMD modules. -->
<script src="requireJSConfig.js"></script>
<!-- Run the main program. -->
<script src="main.js"></script>
</body>
</html>
// A reusable line chart module.
// Draws from D3 line chart example http://bl.ocks.org/mbostock/3883245
// Curran Kelleher March 2015
define(["reactivis", "d3", "model"], function (reactivis, d3, Model) {
// A representation for an optional Model property that is not specified.
// This allows the "when" approach to support optional properties.
// Inspired by Scala"s Option type.
// See http://alvinalexander.com/scala/using-scala-option-some-none-idiom-function-java-null
var None = reactivis.None;
// The constructor function, accepting default values.
return function LineChart(runtime) {
// Create a Model instance for the line chart.
// This will serve as the line chart's public API.
var model = Model({
publicProperties: [
"xColumn",
"yColumn",
"xAxisLabel",
"yAxisLabel",
"xAxisLabelOffset",
"yAxisLabelOffset"
],
container: runtime.div
});
reactivis.svg(model);
reactivis.title(model);
reactivis.margin(model);
reactivis.color(model);
// Append a mouse target for intercepting mouse hover events.
model.enableHoverLine = false;
model.when(["enableHoverLine", "g"], function(enableHoverLine, g){
if(enableHoverLine){
model.mouseTarget = g.append("rect")
.attr("x", 0)
.attr("y", 0)
.style("fill", "none")
.style("pointer-events", "all");
model.selectedXLine = g.append("line")
.attr("class", "hover-line")
}
});
model.when(["mouseTarget", "xScale"], function(mouseTarget, xScale){
mouseTarget.on("mousemove", function () {
var mouseX = d3.mouse(mouseTarget.node())[0];
model.selectedX = xScale.invert(mouseX);
});
});
model.when(["selectedX", "selectedXLine", "xScale", "height"],
function (selectedX, selectedXLine, xScale, height) {
var xPixel = xScale(selectedX);
selectedXLine
.attr("x1", xPixel)
.attr("x2", xPixel)
.attr("y1", 0)
.attr("y2", height);
});
model.when(["mouseTarget", "width"], function (mouseTarget, width) {
mouseTarget.attr("width", width);
});
model.when(["mouseTarget", "height"], function (mouseTarget, height) {
mouseTarget.attr("height", height);
});
// Disable brushing by default.
model.brushEnabled = false;
// Set up brushing interactions to define `brushedIntervals` on the model.
model.when(["brushEnabled", "brushG"], function (brushEnabled, brushG){
if(brushEnabled){
var brush = d3.svg.brush();
brush.on("brush", function () {
model.brushFilter = brush.empty() ? model.xScale.domain() : brush.extent();
});
model.brush = brush;
}
});
model.when(["brush", "brushG", "height", "xScale"], function (brush, brushG, height, xScale){
brush.x(xScale);
// This line causes the brush to move in response to scale changes.
brush.extent(brush.extent());
brushG.call(brush)
.selectAll("rect")
.attr("y", 0)
.attr("height", height);
});
// Generate a function for getting the X value.
model.when(["data", "xColumn"], function (data, xColumn) {
model.getX = function (d) { return d[xColumn]; };
});
// Compute the domain of the X attribute.
model.fixedXDomain = None;
model.when(["data", "getX", "fixedXDomain"], function (data, getX, fixedXDomain) {
if(fixedXDomain === None){
model.xDomain = d3.extent(data, getX);
} else {
model.xDomain = fixedXDomain;
}
});
// Compute the X scale.
model.when(["data", "xDomain", "width"], function (data, xDomain, width) {
model.xScale = d3.time.scale().domain(xDomain).range([0, width]);
});
// Generate a function for getting the scaled X value.
model.when(["data", "xScale", "getX"], function (data, xScale, getX) {
model.getXScaled = function (d) { return xScale(getX(d)); };
});
// Set up the X axis.
model.when("g", function (g) {
model.xAxisG = g.append("g").attr("class", "x axis");
model.xAxisText = model.xAxisG.append("text").style("text-anchor", "middle");
});
// Move the X axis label based on its specified offset.
model.when(["xAxisText", "xAxisLabelOffset"], function (xAxisText, xAxisLabelOffset){
xAxisText.attr("dy", xAxisLabelOffset + "em");
});
// Update the X axis transform when height changes.
model.when(["xAxisG", "height"], function (xAxisG, height) {
xAxisG.attr("transform", "translate(0," + height + ")");
});
// Center the X axis label when width changes.
model.when(["xAxisText", "width"], function (xAxisText, width) {
xAxisText.attr("x", width / 2);
});
// Update the X axis based on the X scale.
model.when(["xAxisG", "xScale"], function (xAxisG, xScale) {
xAxisG.call(d3.svg.axis().orient("bottom").scale(xScale));
});
// Update X axis label.
model.when(["xAxisText", "xAxisLabel"], function (xAxisText, xAxisLabel) {
xAxisText.text(xAxisLabel);
});
// Generate a function for getting the Y value.
model.when(["data", "yColumn"], function (data, yColumn) {
model.getY = function (d) { return d[yColumn]; };
});
// Compute the domain of the Y attribute.
model.when(["data", "getY"], function (data, getY) {
model.yDomain = d3.extent(data, getY);
});
// Compute the Y scale.
model.when(["data", "yDomain", "height"], function (data, yDomain, height) {
model.yScale = d3.scale.linear()
.domain(yDomain)
.range([height, 0]);
});
// Generate a function for getting the scaled Y value.
model.when(["data", "yScale", "getY"], function (data, yScale, getY) {
model.getYScaled = function (d) { return yScale(getY(d)); };
});
// Set up the Y axis.
model.when("g", function (g) {
model.yAxisG = g.append("g").attr("class", "y axis");
model.yAxisText = model.yAxisG.append("text")
.style("text-anchor", "middle")
.attr("transform", "rotate(-90)")
.attr("y", 0);
});
// Move the Y axis label based on its specified offset.
model.when(["yAxisText", "yAxisLabelOffset"], function (yAxisText, yAxisLabelOffset){
yAxisText.attr("dy", "-" + yAxisLabelOffset + "em");
});
// Center the Y axis label when height changes.
model.when(["yAxisText", "height"], function (yAxisText, height) {
yAxisText.attr("x", -height / 2);
});
// Update Y axis label.
model.when(["yAxisText", "yAxisLabel"], function (yAxisText, yAxisLabel) {
yAxisText.text(yAxisLabel);
});
model.yAxis = d3.svg.axis().orient("left");
// Update the Y axis based on the Y scale.
model.when(["yAxis", "yAxisG", "yScale", "height"], function (yAxis, yAxisG, yScale, height) {
var pixelsPerTick = 15;
yAxis.scale(yScale).ticks(height / pixelsPerTick);
yAxisG.call(yAxis);
});
model.when("g", function (g) {
// Add an SVG group to contain the lines.
model.lineG = g.append("g");
// Create a group for the brush.
model.brushG = g.append("g").attr("class", "brush");
// The circles group is added first, before the brush group,
// so that mouse events go to the brush rather than to the
// circles, even when the mouse is on top of a circle.
});
// Draw the lines.
model.lineColumn = None;
model.when(["lineG", "data", "lineColumn", "getXScaled", "getYScaled", "colorScale"],
function (lineG, data, lineColumn, getXScaled, getYScaled, colorScale){
var linesData = d3.nest()
.key(function(d){
if(lineColumn !== None){
return d[lineColumn]; // Have multiple lines.
} else {
return "X";// have only a single line.
}
})
.entries(data),
line = d3.svg.line().x(getXScaled).y(getYScaled),
lines = lineG.selectAll(".line").data(linesData);
lines.enter().append("path").attr("class", "line");
lines
.attr("d", function(d){ return line(d.values); })
.style("stroke", function(d){ return colorScale(d.key); });
lines.exit().remove();
});
return model;
};
});
// This program loads the configuration file called "visConfig.json".
require(["d3", "model", "chiasm/runtime", "./senseYourCityData", "./scatterPlotWithLines", "./lineChart"],
function (d3, Model, Runtime, senseYourCityData, scatterPlotWithLines, lineChart) {
// Instantiate the Chiasm runtime within the container.
var runtime = Runtime(document.getElementById("container"));
// Attach the data API plugin to the runtime.
runtime.plugins.senseYourCityData = senseYourCityData;
// Attach the customized scatter plot to the runtime as a plugin.
runtime.plugins.scatterPlotWithLines = scatterPlotWithLines;
// Attach the customized line chart to the runtime as a plugin.
runtime.plugins.lineChart = lineChart;
// Load the visualization configuration.
d3.json("visConfig.json", function (err, config) {
runtime.config = config;
});
// Toggle visibility of the configuration editor.
d3.select("#gear").on("click", function(){
runtime.getComponent("editor", function(editor){
if(editor.size != "0px"){
editor.size = "0px";
} else {
editor.size = "325px";
}
});
});
});
// This is the RequireJS configuration that sets up module paths.
//
// This file is documented here:
// http://requirejs.org/docs/api.html#config
//
// Curran Kelleher March 2015
(function(){
// Use a fixed version of Chiasm, which provides the visualization runtime.
var chiasmPath = "//curran.github.io/cdn/chiasm-v0.1.4/client/src";
// Here's how to can use a local development version
// if this Gist is cloned into a sibling directory to the chiasm repo.
//var chiasmPath = "../../chiasm/client/src";
requirejs.config({
// Set up the Chiasm package.
// https://github.com/curran/chiasm
packages: [{
name: "chiasm",
location: chiasmPath + "/core"
}],
// Set up paths for Bower dependencies.
// Uses github.com/curran/cdn
paths: {
// AJAX library.
// http://jquery.com/
jquery: "//code.jquery.com/jquery-2.1.1.min",
// Visualization library.
// http://d3js.org/
d3: "//curran.github.io/cdn/d3-v3.5.5/d3.min",
// Reactive model library.
// https://github.com/curran/model
model: "//curran.github.io/cdn/model-v0.2.0/dist/model",
// Functional programming utilities.
// http://benmccormick.org/2014/11/12/underscore-vs-lodash/
lodash: "//cdnjs.cloudflare.com/ajax/libs/lodash.js/3.5.0/lodash.min",
// Asynchronous control flow.
// https://github.com/caolan/async
async: "//cdnjs.cloudflare.com/ajax/libs/async/0.9.0/async",
// Syntax-highlighted text editor for code.
// http://codemirror.net/
codemirror: "//curran.github.io/cdn/codemirror-v5.0.0",
// Provides interactive color picker and slider for CodeMirror.
// http://github.com/enjalot/Inlet.git
inlet: "//curran.github.io/cdn/inlet/inlet",
// Provides miltidimensional filtering.
// http://square.github.io/crossfilter/
crossfilter: "//cdnjs.cloudflare.com/ajax/libs/crossfilter/1.3.11/crossfilter.min",
// Configure paths for plugins loaded at runtime.
plugins: chiasmPath + "/plugins",
// Configure path for reactivis
reactivis: chiasmPath + "/plugins/reactivis"
}
});
})();
// A reusable scatter plot module.
// Curran Kelleher March 2015
define(["d3", "model", "reactivis"], function (d3, Model, reactivis) {
// A representation for an optional Model property that is not specified.
// This allows the "when" approach to support optional properties.
// Inspired by Scala's Option type.
// See http://alvinalexander.com/scala/using-scala-option-some-none-idiom-function-java-null
var None = "__none__";
// The constructor function, accepting default values.
return function ScatterPlot(runtime) {
// Create a Model instance for the visualization.
// This will serve as its public API.
var model = Model({
container: runtime.div
});
reactivis.svg(model);
reactivis.color(model);
model.when("g", function (g) {
// Add an SVG group to contain the marks.
model.circlesG = g.append("g");
});
// Adjust the SVG group translation based on the margin.
model.when(["g", "margin"], function (g, margin) {
g.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
});
// Create the title text element.
model.when("g", function (g){
model.titleText = g.append("text").attr("class", "title-text");
});
// Center the title text when width changes.
model.when(["titleText", "width"], function (titleText, width) {
titleText.attr("x", width / 2);
});
// Update the title text based on the `title` property.
model.when(["titleText", "title"], function (titleText, title){
titleText.text(title);
});
// Update the title text offset.
model.when(["titleText", "titleOffset"], function (titleText, titleOffset){
titleText.attr("dy", titleOffset + "em");
});
// Compute the inner box from the outer box and margin.
// See Margin Convention http://bl.ocks.org/mbostock/3019563
model.when(["box", "margin"], function (box, margin) {
model.width = box.width - margin.left - margin.right;
model.height = box.height - margin.top - margin.bottom;
});
// Generate a function for getting the X value.
model.when(["data", "xColumn"], function (data, xColumn) {
model.getX = function (d) { return d[xColumn]; };
});
// Compute the domain of the X attribute.
// Allow the API client to optionally specify fixed min and max values.
model.xDomainMin = None;
model.xDomainMax = None;
model.when(["data", "getX", "xDomainMin", "xDomainMax"],
function (data, getX, xDomainMin, xDomainMax) {
if(xDomainMin === None && xDomainMax === None){
model.xDomain = d3.extent(data, getX);
} else {
if(xDomainMin === None){
xDomainMin = d3.min(data, getX);
}
if(xDomainMax === None){
xDomainMax = d3.max(data, getX);
}
model.xDomain = [xDomainMin, xDomainMax]
}
});
// Compute the X scale.
model.when(["xDomain", "width"], function (xDomain, width) {
model.xScale = d3.scale.linear().domain(xDomain).range([0, width]);
});
// Generate a function for getting the scaled X value.
model.when(["data", "xScale", "getX"], function (data, xScale, getX) {
model.getXScaled = function (d) { return xScale(getX(d)); };
});
// Set up the X axis.
model.when("g", function (g) {
model.xAxisG = g.append("g").attr("class", "x axis");
model.xAxisText = model.xAxisG.append("text").style("text-anchor", "middle");
});
// Move the X axis label based on its specified offset.
model.when(["xAxisText", "xAxisLabelOffset"], function (xAxisText, xAxisLabelOffset){
xAxisText.attr("dy", xAxisLabelOffset + "em");
});
// Update the X axis transform when height changes.
model.when(["xAxisG", "height"], function (xAxisG, height) {
xAxisG.attr("transform", "translate(0," + height + ")");
});
// Center the X axis label when width changes.
model.when(["xAxisText", "width"], function (xAxisText, width) {
xAxisText.attr("x", width / 2);
});
// Update the X axis based on the X scale.
model.when(["xAxisG", "xScale"], function (xAxisG, xScale) {
xAxisG.call(d3.svg.axis().orient("bottom").scale(xScale));
});
// Update X axis label.
model.when(["xAxisText", "xAxisLabel"], function (xAxisText, xAxisLabel) {
xAxisText.text(xAxisLabel);
});
// Generate a function for getting the Y value.
model.when(["data", "yColumn"], function (data, yColumn) {
model.getY = function (d) { return d[yColumn]; };
});
// Compute the domain of the Y attribute.
// Allow the API client to optionally specify fixed min and max values.
model.yDomainMin = None;
model.yDomainMax = None;
model.when(["data", "getY", "yDomainMin", "yDomainMax"],
function (data, getY, yDomainMin, yDomainMax) {
if(yDomainMin === None && yDomainMax === None){
model.yDomain = d3.extent(data, getY);
} else {
if(yDomainMin === None){
yDomainMin = d3.min(data, getY);
}
if(yDomainMax === None){
yDomainMax = d3.max(data, getY);
}
model.yDomain = [yDomainMin, yDomainMax]
}
});
// Compute the Y scale.
model.when(["data", "yDomain", "height"], function (data, yDomain, height) {
model.yScale = d3.scale.linear().domain(yDomain).range([height, 0]);
});
// Generate a function for getting the scaled Y value.
model.when(["data", "yScale", "getY"], function (data, yScale, getY) {
model.getYScaled = function (d) { return yScale(getY(d)); };
});
// Set up the Y axis.
model.when("g", function (g) {
model.yAxisG = g.append("g").attr("class", "y axis");
model.yAxisText = model.yAxisG.append("text")
.style("text-anchor", "middle")
.attr("transform", "rotate(-90)")
.attr("y", 0);
});
// Move the Y axis label based on its specified offset.
model.when(["yAxisText", "yAxisLabelOffset"], function (yAxisText, yAxisLabelOffset){
yAxisText.attr("dy", "-" + yAxisLabelOffset + "em")
});
// Center the Y axis label when height changes.
model.when(["yAxisText", "height"], function (yAxisText, height) {
yAxisText.attr("x", -height / 2);
});
// Update Y axis label.
model.when(["yAxisText", "yAxisLabel"], function (yAxisText, yAxisLabel) {
yAxisText.text(yAxisLabel);
});
// Update the Y axis based on the Y scale.
model.when(["yAxisG", "yScale"], function (yAxisG, yScale) {
yAxisG.call(d3.svg.axis().orient("left").scale(yScale));
});
// Allow the API client to optionally specify a size column.
model.sizeColumn = None;
// The default radius of circles in pixels.
model.sizeDefault = 2;
// The min and max circle radius in pixels.
model.sizeMin = 0.5;
model.sizeMax = 6;
// Set up the size scale.
model.when(["sizeColumn", "data", "sizeDefault", "sizeMin", "sizeMax"],
function (sizeColumn, data, sizeDefault, sizeMin, sizeMax){
if(sizeColumn !== None){
var getSize = function (d){ return d[sizeColumn] },
sizeScale = d3.scale.linear()
.domain(d3.extent(data, getSize))
.range([sizeMin, sizeMax]);
model.getSizeScaled = function (d){ return sizeScale(getSize(d)); };
} else {
model.getSizeScaled = function (d){ return sizeDefault; };
}
});
// Filter out points that go beyond the edges of the plot
// for the case that the domain is set explicitly and is
// smaller than the extent of the data.
model.when(["data", "getX", "getY", "xScale", "yScale"],
function(data, getX, getY, xScale, yScale){
var xMin = xScale.domain()[0], xMax = xScale.domain()[1],
yMin = yScale.domain()[0], yMax = yScale.domain()[1];
model.visibleData = data.filter(function(d){
var x = getX(d), y = getY(d);
return x > xMin && x < xMax && y > yMin && y < yMax;
});
});
// Draw the circles of the scatter plot.
//model.when(["visibleData", "circlesG", "getXScaled", "getYScaled", "getSizeScaled", "getColorScaled"],
// function (visibleData, circlesG, getXScaled, getYScaled, getSizeScaled, getColorScaled){
// var circles = circlesG.selectAll("circle").data(visibleData);
// circles.enter().append("circle");
// circles
// .attr("cx", getXScaled)
// .attr("cy", getYScaled)
// .attr("r", getSizeScaled)
// .attr("fill", getColorScaled);
// circles.exit().remove();
//});
// Add an SVG group to contain the lines.
model.when("g", function (g) {
model.lineG = g.append("g");
});
// Draw the lines.
model.lineColumn = None;
model.when(["lineG", "data", "lineColumn", "getXScaled", "getYScaled", "colorScale"],
function (lineG, data, lineColumn, getXScaled, getYScaled, colorScale){
var linesData = d3.nest()
.key(function(d){
if(lineColumn !== None){
return d[lineColumn]; // Have multiple lines.
} else {
return "X";// have only a single line.
}
})
.entries(data),
line = d3.svg.line().x(getXScaled).y(getYScaled),
lines = lineG.selectAll(".line").data(linesData);
lines.enter().append("path").attr("class", "line");
lines
.attr("d", function(d){ return line(d.values); })
.style("stroke", function(d){ return colorScale(d.key); });
lines.exit().remove();
});
return model;
};
});
// A Chiasm plugin for loading data from the Data Canvas Sense Your City API.
// http://map.datacanvas.org/#!/data
define(["model", "jquery", "lodash", "async"], function (Model, $, _, async){
return function (runtime) {
var model = Model(),
// See API documentation at http://map.datacanvas.org/#!/data
API_URL = "http://sensor-api.localdata.com/api/v1/aggregations.csv",
// List of all cities with available data.
cities = ["San Francisco", "Bangalore", "Boston", "Geneva", "Rio de Janeiro", "Shanghai", "Singapore"],
// The default parameters to pass into the API.
defaultParams = {
// Use averaging as the aggregation operator.
op: "mean",
// Include all fields.
fields: "temperature,light,airquality_raw,sound,humidity,dust",
// Get data for every 10 minutes.
resolution: "10m",
}
// Fetches the latest data for a given city.
function getDataForCity(city, from, before, callback){
var params = _.extend({
from: from,
before: before,
"over.city": city
}, defaultParams);
// Use jQuery to fetch the data.
// jQuery is used here rather than D3 because of its nice parameter syntax.
$.get(API_URL, params, function(csv) {
// Parse the CSV string.
callback(null, d3.csv.parse(csv, function(d){
// Parse ISO date strings into Date objects.
d.timestamp = new Date(d.timestamp);
// Parse strings into Numbers for numeric fields.
d.temperature = +d.temperature;
d.light = +d.light
d.airquality_raw = +d.airquality_raw
d.sound = +d.sound
d.humidity = +d.humidity
d.dust = +d.dust
return d;
}));
});
};
// Fetches the current temperature across all cities.
function getData(from, before, callback){
async.map(cities, function(city, callback){
getDataForCity(city, from, before, callback);
}, function(err, results){
callback(err, _.flatten(results));
});
}
// Fetch the data and expose it to the model.
// 1 day in milliseconds.
var msInterval = 1000 * 60 * 60 * 24,
msTime = Date.now(),
from = new Date(msTime - msInterval).toISOString(),
before = new Date(msTime).toISOString();
getData(from, before, function(err, data){
model.data = data;
from = new Date(msTime - msInterval * 2).toISOString(),
before = new Date(msTime - msInterval).toISOString();
getData(from, before, function(err, data){
model.data = data;
});
});
var intervalCount = 0,
intervalCountMax = 80;
async.whilst(
function () { return intervalCount < intervalCountMax; },
function (callback) {
from = new Date(msTime - msInterval * (intervalCount + 1)).toISOString(),
before = new Date(msTime - msInterval * intervalCount).toISOString();
getData(from, before, function(err, data){
if(model.data){
model.data = _.sortBy(_.union(model.data, data), "timestamp");
console.log(model.data.length + " records over " + intervalCount + " days");
} else {
model.data = data;
}
callback();
});
intervalCount++;
}, function(err){
console.log("Done fetching data");
}
);
return model;
};
});
/* Style the axes and labels for visualizations.
Curran Kelleher March 2015 */
/* Make the container fill the page. */
body {
background-color: black;
}
#container {
position: fixed;
left: 0px;
right: 0px;
top: 0px;
bottom: 0px;
}
#gear {
position: fixed;
right: 0px;
top: 0px;
background-color: gray;
border-radius: 20px;
}
#gear:hover {
background-color: lightgray;
}
#gear:active {
background-color: gray;
}
/* Tick mark labels */
.axis .tick text {
font: 8pt sans-serif;
fill: white;
}
/* Axis labels */
.axis text {
font: 14pt sans-serif;
fill: white;
}
/* Lines within axes. */
.axis path,
.axis line {
fill: none;
stroke: gray;
shape-rendering: crispEdges;
}
/* Lines within the line chart. */
.line {
fill: none;
stroke-width: 1px;
}
/* Style the title text at the top of the visualization. */
.title-text {
text-anchor: middle;
font: 20pt sans-serif;
fill: white;
}
/* Style the brush. Draws from http://bl.ocks.org/mbostock/4560481 */
.brush .extent {
fill-opacity: .2;
stroke: gray;
shape-rendering: crispEdges;
fill: white;
}
{
"layout": {
"plugin": "layout",
"state": {
"layout": {
"orientation": "horizontal",
"children": [
"editor",
{
"orientation": "vertical",
"children": [
{
"orientation": "horizontal",
"children": [
"lineChartFocus",
"scatterPlot"
]
},
"lineChartContext"
]
}
]
}
}
},
"editor": {
"plugin": "configEditor",
"state": {
"size": "0px"
}
},
"color": {
"plugin": "colorScale",
"state": {
"domain": [
"San Francisco",
"Bangalore",
"Boston",
"Geneva",
"Rio de Janeiro",
"Shanghai",
"Singapore"
],
"range": [
"#927400",
"#cc0006",
"#db008e",
"#8a3dff",
"#006aff",
"#00979b",
"#009100"
]
}
},
"scatterPlot": {
"plugin": "scatterPlotWithLines",
"state": {
"colorColumn": "city",
"lineColumn": "city",
"xColumn": "humidity",
"xAxisLabel": "Humidity",
"yColumn": "temperature",
"yAxisLabel": "Temperature (°C)",
"margin": {
"top": 32,
"right": 2,
"bottom": 45,
"left": 47
},
"xAxisLabelOffset": 2.128,
"yAxisLabelOffset": 1.4,
"colorDefault": "#059e00",
"title": "Temperature by Humidity",
"titleOffset": -0.1617408,
"xDomainMin": 7,
"xDomainMax": 80,
"yDomainMin": -11,
"yDomainMax": 43
}
},
"lineChartFocus": {
"plugin": "lineChart",
"state": {
"brushEnabled": true,
"lineColumn": "city",
"colorColumn": "city",
"xColumn": "timestamp",
"yColumn": "temperature",
"margin": {
"top": 8,
"right": 2,
"bottom": 40,
"left": 47
},
"xAxisLabelOffset": 1.9,
"yAxisLabelOffset": 1.4
}
},
"lineChartContext": {
"plugin": "lineChart",
"state": {
"brushEnabled": true,
"lineColumn": "city",
"colorColumn": "city",
"xColumn": "timestamp",
"xAxisLabel": "Time",
"yColumn": "temperature",
"margin": {
"top": 8,
"right": 2,
"bottom": 40,
"left": 47
},
"xAxisLabelOffset": 1.9,
"yAxisLabelOffset": 1.4,
"size": 0.288
}
},
"senseYourCityData": {
"plugin": "senseYourCityData"
},
"crossfilterFocus": {
"plugin": "crossfilter",
"state": {
"dimensions": [
"timestamp"
]
}
},
"crossfilterContext": {
"plugin": "crossfilter",
"state": {
"dimensions": [
"timestamp"
]
}
},
"links": {
"plugin": "links",
"state": {
"bindings": [
"senseYourCityData.data -> lineChartContext.data",
"senseYourCityData.data -> crossfilterContext.data",
"lineChartContext.brushFilter -> crossfilterContext.timestampFilter",
"crossfilterContext.timestampTop -> crossfilterFocus.data",
"crossfilterFocus.data -> lineChartFocus.data",
"lineChartFocus.brushFilter -> crossfilterFocus.timestampFilter",
"crossfilterFocus.timestampTop -> scatterPlot.data",
"color.domain -> lineChartFocus.colorDomain",
"color.range -> lineChartFocus.colorRange",
"color.domain -> lineChartContext.colorDomain",
"color.range -> lineChartContext.colorRange",
"color.domain -> scatterPlot.colorDomain",
"color.range -> scatterPlot.colorRange"
]
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment