Skip to content

Instantly share code, notes, and snippets.

@biovisualize
Last active August 29, 2015 14:02
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save biovisualize/f84111e34edc6d594216 to your computer and use it in GitHub Desktop.
Save biovisualize/f84111e34edc6d594216 to your computer and use it in GitHub Desktop.
10K points line chart with progressive rendering

10K points in an interactive line chart using the excellent Kay Chang's (AKA Syntagmatic) progressive rendering.

It also uses some nice tricks like throttling the brush move (from underscore) and bisecting the data points to find the hovered dots. The reactive hover and tooltip clearly shows that the UI is not blocked by the rendering. You can even hover the points before they are rendered!

var Tooltip = function () {
var config = {
container: null,
templateSelector: '#linechart-tooltip',
bound: null,
pos: [0, 0]
};
var data = {
header: null,
text: null
};
var cache = {
rootSelection: null
};
var exports = {};
function init() {
var rootNode = config.container;
rootNode.innerHTML = '<div class="tooltip-container"></div>';
cache.rootSelection = d3.select(rootNode).style({opacity: 0, 'pointer-events': 'none'});
}
exports.show = function() {
var tooltipContainer = cache.rootSelection;
tooltipContainer.html(data.text);
var tooltipSize = tooltipContainer.node().getBoundingClientRect();
var shouldBeLeft = false;
if (config.bound) {
var bounds = config.bound.getBoundingClientRect();
shouldBeLeft = (config.pos.x < (bounds.width / 2) && config.pos.x > tooltipSize.width) || config.pos.x > bounds.width - tooltipSize.width;
}
tooltipContainer.classed('left', shouldBeLeft).classed('right', !shouldBeLeft);
var tooltipPosX = shouldBeLeft ? config.pos.x - tooltipSize.width : config.pos.x;
tooltipContainer.interrupt().style({opacity: 1, left: tooltipPosX + 'px', top: config.pos.y - tooltipSize.height/2 + 'px'});
return this;
};
exports.hide = function(){
cache.rootSelection.transition().duration(500).style({opacity: 0});
return this;
};
exports.setConfig = function (_newConfig) {
chartUtils.override(_newConfig, config);
if (!cache.rootSelection) {init();}
return this;
};
exports.setData = function (_newData) {
data = _newData;
return this;
};
return exports;
};
var chartUtils = (function () {
var exports = {};
exports.override = function (_objA, _objB) { for (var x in _objA) {if (x in _objB) {_objB[x] = _objA[x];}} };
exports.cloneJSON = function(_obj){ return JSON.parse(JSON.stringify(_obj)); };
exports.deepExtend = function extend(destination, source) {
for (var property in source) {
if (source[property] && source[property].constructor &&
source[property].constructor === Object) {
destination[property] = destination[property] || {};
extend(destination[property], source[property]);
} else {
destination[property] = source[property];
}
}
return destination;
};
//DOMParser shim for Safari
(function(DOMParser) {
"use strict";
var DOMParser_proto = DOMParser.prototype,
real_parseFromString = DOMParser_proto.parseFromString;
try {
if ((new DOMParser()).parseFromString("", "text/html")) {
return;
}
} catch (ex) {}
DOMParser_proto.parseFromString = function(markup, type) {
if (/^\s*text\/html\s*(?:;|$)/i.test(type)) {
var doc = document.implementation.createHTMLDocument("");
if (markup.toLowerCase().indexOf('<!doctype') > -1) {
doc.documentElement.innerHTML = markup;
}
else {
doc.body.innerHTML = markup;
}
return doc;
} else {
return real_parseFromString.apply(this, arguments);
}
};
}(DOMParser));
exports.throttle = function(func, wait, options) {
var context, args, result;
var timeout = null;
var previous = 0;
options = options || {};
var later = function() {
previous = options.leading === false ? 0 : new Date();
timeout = null;
result = func.apply(context, args);
context = args = null;
};
return function() {
var now = new Date();
if (!previous && options.leading === false) {previous = now;}
var remaining = wait - (now - previous);
context = this;
args = arguments;
if (remaining <= 0) {
clearTimeout(timeout);
timeout = null;
previous = now;
result = func.apply(context, args);
context = args = null;
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining);
}
return result;
};
};
return exports;
})();
<!DOCTYPE html>
<head lang="en">
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<script type="text/javascript" src="http://d3js.org/d3.v3.min.js"></script>
<script type="text/javascript" src="render-queue.js"></script><!DOCTYPE html>
<script type="text/javascript" src="streaming-line-chart.js"></script>
<script type="text/javascript" src="chart-utils.js"></script>
<script type="text/javascript" src="chart-tooltip.js"></script>
<link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
<script type="text/template" id="line-chart-template">
<div>
<svg xmlns="http://www.w3.org/2000/svg" class="bg">
<g class="chart-group">
<g class="background"><rect class="panel-bg" /></g>
<g class="axis-y axis-y2"></g>
<g class="axis-y axis-y1"></g>
<g class="axis-x"></g>
<rect class="axis-x-bg" />
</g>
</svg>
<canvas class="geometry"></canvas>
<svg xmlns="http://www.w3.org/2000/svg" class="axes">
<g class="chart-group">
<g class="axis-x"></g>
<rect class="axis-y-bg" />
<g class="axis-y axis-y2"></g>
<g class="axis-y axis-y1"></g>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" class="interaction">
<g class="hover-group"><line class="hover-guide-x"/></g>
<g class="brush-group"></g>
</svg>
</div>
</script>
<div class="boundary-chart-container" id="new-line-chart">
<div class="boundary-chart" id="scrubber-line-chart"></div>
<div class="boundary-chart" id="main-line-chart"></div>
<div class="boundary-chart" id="bar-chart"></div>
<div class="boundary-chart-tooltip"></div>
</div>
<script>
// data
//-----------------------------------------------------
var pointCount = 10000;
var startDate = new Date();
var timeSeries = d3.range(pointCount).map(function (d, i) {
startDate.setUTCSeconds(startDate.getUTCSeconds() + 1);
return d3.time.format.iso.parse(startDate.toISOString());
});
var pointSeriesGenerator = function (_min, _max) {
return d3.range(pointCount).map(function (d, i) {
return ~~(Math.random() * (_max - _min) + _min) / 100;
});
};
var colors = d3.scale.category20().range();
var dataset = [
{x: timeSeries, y: pointSeriesGenerator(10, 30), y2: pointSeriesGenerator(25, 40), name: 'line A', color: colors[0]},
{x: timeSeries, y: pointSeriesGenerator(5, 15), y2: pointSeriesGenerator(30, 35), name: 'line B', color: colors[1]},
{x: timeSeries, y: pointSeriesGenerator(10, 12), y2: pointSeriesGenerator(20, 35), name: 'line C', color: colors[2]},
{x: timeSeries, y: pointSeriesGenerator(30, 50), y2: pointSeriesGenerator(25, 30), name: 'line D', color: colors[3]},
{x: timeSeries, y: pointSeriesGenerator(2, 5), y2: pointSeriesGenerator(30, 40), name: 'line E', color: colors[4]},
{x: timeSeries, y: pointSeriesGenerator(10, 12), y2: pointSeriesGenerator(30, 40), name: 'line F', color: colors[5]}
];
// line charts
//-----------------------------------------------------
var lineChartNode = document.querySelector('#new-line-chart');
var mainLineChartNode = lineChartNode.querySelector('#main-line-chart');
var scrubberLineChartNode = lineChartNode.querySelector('#scrubber-line-chart');
var barChartNode = lineChartNode.querySelector('#bar-chart');
var scrubberHeight = 100;
var barChartHeight = 50;
var mainLineChart = LineChart().setConfig({
width: lineChartNode.clientWidth,
height: lineChartNode.clientHeight - scrubberHeight,
container: mainLineChartNode,
suggestedYTicks: 4,
suffix: 'Gbps',
resolution: 'second',
progressiveRenderingRate: 100
});
var scrubberLineChart = LineChart().setConfig({
width: lineChartNode.clientWidth,
height: scrubberHeight,
container: scrubberLineChartNode,
suggestedYTicks: 4,
useBrush: true,
resolution: 'second',
showAxisY: false,
brushThrottleWaitDuration: 300
});
// tooltip
//-----------------------------------------------------
var tooltip = Tooltip().setConfig({
container: lineChartNode.querySelector('.boundary-chart-tooltip'),
bound: mainLineChart.getCanvasNode()
});
var tooltipOffset = mainLineChartNode.getBoundingClientRect().top;
function tooltipOnDotHover(pos, d){
tooltip.setConfig({pos: {x: pos.x + 10, y: pos.y + tooltipOffset - 10}})
.setData({text: d.name + ' : ' + d.closestY})
.show();
}
function tooltipOnMouseOut(){
tooltip.hide();
}
// bind events
//-----------------------------------------------------
mainLineChart
.on('dotHover', tooltipOnDotHover)
.on('dotMouseOut', tooltipOnMouseOut);
// sample info for sizing the brush on init
var lastTimeSeriesPointIdx = timeSeries.length-1;
var brushSizeInSample = 1000;
scrubberLineChart
.on('brushChange', function(brushExtent){
mainLineChart.setZoom(brushExtent);
})
.setBrushSelection([timeSeries[lastTimeSeriesPointIdx - brushSizeInSample], timeSeries[lastTimeSeriesPointIdx]]);
// set data
//-----------------------------------------------------
scrubberLineChart.setData(dataset);
mainLineChart.setData(dataset).setZoom(scrubberLineChart.getBrushExtent());
</script>
</body></html>
var renderQueue = (function(func) {
var _queue = [], // data to be rendered
_rate = 1000, // number of calls per frame
_invalidate = function() {}, // invalidate last render queue
_clear = function() {}; // clearing function
var rq = function(data) {
if (data) rq.data(data);
_invalidate();
_clear();
rq.render();
};
rq.render = function() {
var valid = true;
_invalidate = rq.invalidate = function() {
valid = false;
};
function doFrame() {
if (!valid) return true;
var chunk = _queue.splice(0,_rate);
chunk.map(func);
timer_frame(doFrame);
}
doFrame();
};
rq.data = function(data) {
_invalidate();
_queue = data.slice(0); // creates a copy of the data
return rq;
};
rq.add = function(data) {
_queue = _queue.concat(data);
};
rq.rate = function(value) {
if (!arguments.length) return _rate;
_rate = value;
return rq;
};
rq.remaining = function() {
return _queue.length;
};
// clear the canvas
rq.clear = function(func) {
if (!arguments.length) {
_clear();
return rq;
}
_clear = func;
return rq;
};
rq.invalidate = _invalidate;
var timer_frame = window.requestAnimationFrame
|| window.webkitRequestAnimationFrame
|| window.mozRequestAnimationFrame
|| window.oRequestAnimationFrame
|| window.msRequestAnimationFrame
|| function(callback) { setTimeout(callback, 17); };
return rq;
});
var LineChart = function () {
var config = {
width: null,
height: null,
margin: {top: 0, right: 0, bottom: 0, left: 0},
container: null,
showTicksX: true,
showTicksY: true,
useBrush: false,
suggestedXTicks: null,
suggestedYTicks: null,
timeFormat: d3.time.format('%H:%M:%S'),
axisXHeight: 20,
isMirror: false,
dotSize: 4,
suffix: '',
resolution: 'minute',
stripeCount: 4,
tickFormatY: null,
labelYOffset: 10,
axisYStartsAtZero: true,
showStripes: true,
geometryType: 'line',
showAxisX: true,
showAxisY: true,
showLabelsX: true,
showLabelsY: true,
progressiveRenderingRate: 300,
brushThrottleWaitDuration: 150
};
var cache = {
scaledData: [],
bgSvg: null,
axesSvg: null,
geometryCanvas: null,
resolutionConfigs: null,
scaleX: null,
scaleY: null,
isMirror: false,
axisXHeight: null
};
var resolutionConfigs = {
second: { dividerMillis: 60*1000, multiplier: 60, dateFunc: 'setSeconds', d3DateFunc: d3.time.minutes},
minute: { dividerMillis: 60*60*1000, multiplier: 60, dateFunc: 'setMinutes', d3DateFunc: d3.time.hours},
hour: { dividerMillis: 24*60*60*1000, multiplier: 24, dateFunc: 'setHours', d3DateFunc: d3.time.days}
};
var brush = d3.svg.brush(), brushExtent;
var data = [];
var dispatch = d3.dispatch('brushChange', 'brushDragStart', 'brushDragMove', 'brushDragEnd', 'dotHover', 'dotMouseOut', 'dotClick', 'chartHover', 'chartOut', 'chartEnter');
var exports = {};
var queues = [];
function init() {
// Template
/////////////////////////////
var container = config.container;
var template = d3.select('#line-chart-template').text();
var templateDOM = new DOMParser().parseFromString(template, 'text/html');
var doc = container.insertBefore(container.ownerDocument.importNode(templateDOM.body.children[0], true), container.firstChild);
var root = d3.select(doc);
cache.bgSvg = root.select('svg.bg');
cache.axesSvg = root.select('svg.axes');
cache.interactionSvg = root.select('svg.interaction');
cache.geometryCanvas = root.select('canvas.geometry');
root.selectAll('svg, canvas').style({position: 'absolute'});
// Scales
/////////////////////////////
cache.scaleX = d3.time.scale.utc();
cache.scaleY = d3.scale.linear();
// Hovering
/////////////////////////////
if(!config.useBrush) {setupHovering();}
return this;
}
function setupHovering() {
var hoverGroupSelection = cache.interactionSvg.select('.hover-group');
cache.interactionSvg
.on('mousemove', function () {
if(!data || data.length === 0) {return;}
var mouseX = d3.mouse(cache.geometryCanvas.node())[0];
injectClosestPointsFromX(mouseX);
hoverGroupSelection.style({visibility: 'visible'});
if (typeof data[0].closestY !== 'undefined') {
exports.displayHoveredDots();
dispatch.chartHover(data);
}
else {
exports.hideHoveredDots();
}
exports.displayVerticalGuide(mouseX);
})
.on('mouseenter', function () {
dispatch.chartEnter();
})
.on('mouseout', function () {
if(!cache.interactionSvg.node().contains(d3.event.toElement)) {
hoverGroupSelection.style({visibility: 'hidden'});
dispatch.chartOut();
}
})
.select('.hover-group');
}
function injectClosestPointsFromX(fromPointX) {
data.forEach(function (d) {
if(typeof d.scaledX === 'undefined') {return;}
var halfInterval = (d.scaledX[1] - d.scaledX[0]) * 0.5;
var closestIndex = d3.bisect(d.scaledX, fromPointX - halfInterval);
d.closestX = d.x[closestIndex];
d.closestY = d.y[closestIndex];
if (typeof d.closestY !== 'undefined'){
d.closestScaledX = d.scaledX[closestIndex];
}
d.closestScaledY = d.scaledY[closestIndex];
if (cache.isMirror) {
d.closestY2 = d.y2[closestIndex];
d.closestScaledY2 = d.scaledY2[closestIndex];
}
});
return data;
}
function render() {
prepareContainers();
if (data.length === 0) {return;}
cache.isMirror = !!data[0].y2 || config.isMirror;
if(config.showAxisY) {renderAxisY();}
if(config.showAxisX) {renderAxisX();}
if(config.showStripes) {showStripes();}
if(config.geometryType === 'line') {renderLineGeometry();}
else if(config.geometryType === 'bar') {renderBarGeometry();}
setupBrush();
}
function prepareContainers(){
// Calculate sizes
/////////////////////////////
cache.axisXHeight = (!config.showAxisX || !config.showLabelsX) ? 0 : config.axisXHeight;
var containerBBox = config.container.getBoundingClientRect();
if (!config.width) {config.width = containerBBox.width;}
if (!config.height) {config.height = containerBBox.height;}
cache.chartW = config.width - config.margin.right - config.margin.left;
cache.chartH = config.height - config.margin.top - config.margin.bottom - cache.axisXHeight;
cache.scaleX.range([0, cache.chartW]);
// Containers
/////////////////////////////
cache.bgSvg.style({height: config.height + 'px', width: config.width + 'px'})
.selectAll('.chart-group')
.attr({transform: 'translate(' + [config.margin.left, config.margin.top] + ')'});
cache.axesSvg.style({height: config.height + 'px', width: config.width + 'px'});
cache.interactionSvg.style({height: config.height + 'px', width: config.width + 'px'});
// Background
/////////////////////////////
cache.bgSvg.select('.panel-bg').attr({width: cache.chartW, height: cache.chartH});
cache.bgSvg.select('.axis-x-bg').attr({width: cache.chartW, height: cache.axisXHeight, y: cache.chartH});
cache.resolutionConfig = resolutionConfigs[config.resolution];
}
function renderAxisY(){
// Y axis
/////////////////////////////
if (cache.isMirror) {cache.scaleY.range([cache.chartH / 2, 0]);}
else {cache.scaleY.range([cache.chartH, 0]);}
var axisContainerY = cache.axesSvg.select('.axis-y1');
var bgYSelection = cache.bgSvg.select('.axis-y1');
var axisY = d3.svg.axis().scale(cache.scaleY).orient('left').tickSize(0);
function renderAxisPart(axisContainerY, bgYSelection, axisY){
var ticksY = [].concat(config.suggestedYTicks); // make sure it's an array
if (ticksY[0]) {axisY.ticks.apply(null, ticksY);}
// labels
if(config.showLabelsY){
axisContainerY.call(axisY);
var texts = axisContainerY.selectAll('text').attr({transform: 'translate(' + config.labelYOffset + ',0)'})
.style({'text-anchor': 'start'})
.text(function(d){ return parseFloat(d); })
.filter(function(d, i){ return i === 0; }).text(function(){ return this.innerHTML + ' ' + config.suffix; });
if(config.tickFormatY) {texts.text(config.tickFormatY);}
axisContainerY.selectAll('line').remove();
}
// grid lines
if (config.showTicksY) {
bgYSelection.call(axisY);
bgYSelection.selectAll('text').text(null);
bgYSelection.selectAll('line').attr({x1: cache.chartW})
.classed('grid-line y', true);
}
}
renderAxisPart(axisContainerY, bgYSelection, axisY);
// Y2 axis
/////////////////////////////
if (cache.isMirror) {
var axisContainerY2 = cache.axesSvg.select('.axis-y2');
var bgY2Selection = cache.bgSvg.select('.axis-y2');
cache.scaleY.range([cache.chartH / 2, cache.chartH]);
renderAxisPart(axisContainerY2, bgY2Selection, axisY);
}
else {cache.axesSvg.select('.axis-y2').selectAll('*').remove();}
// Axis background
function findMaxLabelWidth(selection){
var labels = [], labelW;
selection.each(function(){
labels.push(this.getBoundingClientRect().width);
});
return d3.max(labels);
}
if (config.showTicksY) {
var labels = cache.axesSvg.selectAll('.axis-y1 text, .axis-y2 text');
var maxLabelW = findMaxLabelWidth(labels);
var axisYBg = cache.axesSvg.select('.axis-y-bg');
axisYBg.attr({width: maxLabelW + config.labelYOffset, height: cache.chartH, y: config.margin.top});
}
}
function renderAxisX(){
// X axis
/////////////////////////////
var axisXSelection = cache.axesSvg.select('.axis-x');
axisXSelection.attr({transform: 'translate(' + [0, cache.chartH] + ')'});
var axisX = d3.svg.axis().scale(cache.scaleX).orient('bottom').tickSize(cache.axisXHeight);
// labels
if(config.showLabelsX){
if (typeof config.timeFormat === 'function') {
axisX.tickFormat(function (d) { return config.timeFormat(new Date(d)); });
}
var ticksX = [];
if(config.suggestedXTicks){
ticksX = [].concat(config.suggestedXTicks); // make sure it's an array
}
else if (config.resolution){
ticksX = [cache.resolutionConfig.d3DateFunc, 1];
}
if (ticksX[0]) {axisX.ticks.apply(null, ticksX);}
axisXSelection.call(axisX);
axisXSelection.selectAll('text').attr({transform: function(){
return 'translate(3, -' + (cache.axisXHeight/2 + this.getBBox().height / 2) + ')';
}});
axisXSelection.selectAll('line').remove();
}
// ticks
if(config.showTicksX){
var bgXSelection = cache.bgSvg.select('.axis-x');
bgXSelection.attr({transform: 'translate(' + [0, cache.chartH] + ')'});
bgXSelection.call(axisX);
bgXSelection.selectAll('text').text(null);
bgXSelection.selectAll('line').attr({y1: -cache.chartH})
.classed('grid-line x', true);
}
}
function showStripes(){
// Stripes
/////////////////////////////
if(data.length > 0 && data[0].x.length > 0 && cache.resolutionConfig){
var stripCountMultiplier = config.stripeCount / 2;
var stripeCount = Math.round(Math.abs((data[0].x[data[0].x.length - 1].getTime() - data[0].x[0].getTime()) /
(cache.resolutionConfig.dividerMillis))) * stripCountMultiplier;
var discretizedDates = d3.range(stripeCount + 1).map(function(d, i){
return new Date(new Date(data[0].x[0])[cache.resolutionConfig.dateFunc](i * cache.resolutionConfig.multiplier/stripCountMultiplier));
});
var tickSpacing = cache.scaleX(discretizedDates[1]) - cache.scaleX(discretizedDates[0]);
var stripesSelection = cache.bgSvg.select('.background').selectAll('rect.stripe').data(discretizedDates);
stripesSelection.enter().append('rect').attr({'class': 'stripe'});
stripesSelection
.attr({
x: function(d){ return cache.scaleX(d); },
y: 0,
width: isNaN(tickSpacing)? 0 : tickSpacing/2,
height: cache.chartH
})
.style({stroke: 'none'});
stripesSelection.exit().remove();
}
}
function renderBarGeometry(){
// Bar geometry
/////////////////////////////
if (cache.isMirror) {cache.scaleY.range([cache.chartH / 2, 0]);}
else {cache.scaleY.range([cache.chartH, 0]);}
cache.geometryCanvas.attr({
width: cache.chartW,
height: cache.chartH
})
.style({
top: config.margin.top + 'px',
left: config.margin.left + 'px'
});
var ctx = cache.geometryCanvas.node().getContext('2d');
ctx.globalCompositeOperation = "source-over";
ctx.translate(0.5, 0.5);
var i, j, lineData, scaledData;
lineData = data[0];
ctx.fillStyle = lineData.color || 'silver';
var barW = cache.scaleX(lineData.x[1]) - cache.scaleX(lineData.x[0]);
barW *= 0.5;
for (j = 0; j < lineData.x.length; j++) {
scaledData = {x: cache.scaleX(lineData.x[j]), y: cache.scaleY(lineData.y[j])};
ctx.fillRect(scaledData.x - barW/2, scaledData.y, barW, cache.chartH);
}
}
function renderLineGeometry(){
// Line geometry
/////////////////////////////
// setTimeout(function(){
cache.geometryCanvas.attr({
width: cache.chartW,
height: cache.chartH
})
.style({
top: config.margin.top + 'px',
left: config.margin.left + 'px'
});
var ctx = cache.geometryCanvas.node().getContext('2d');
ctx.globalCompositeOperation = "source-over";
ctx.translate(0.5, 0.5);
ctx.lineWidth = 1.5;
if (cache.isMirror) {cache.scaleY.range([cache.chartH / 2, 0]);}
else {cache.scaleY.range([cache.chartH, 0]);}
var i, j, lineData, scaledData, lineDataZipped, queue;
function renderLineSegment(scaledData){
ctx.strokeStyle = scaledData[4];
ctx.beginPath();
ctx.moveTo(scaledData[2], scaledData[3]);
ctx.lineTo(scaledData[0], scaledData[1]);
ctx.stroke();
}
for (i = 0; i < data.length * 2; i++){
queues.push(renderQueue(renderLineSegment).rate(config.progressiveRenderingRate));
}
for (i = 0; i < data.length; i++) {
lineData = data[i];
lineData.scaledX = [];
lineData.scaledY = [];
for (j = 0; j < lineData.x.length; j++) {
scaledData = {x: cache.scaleX(lineData.x[j]), y: cache.scaleY(lineData.y[j])};
lineData.scaledX.push(scaledData.x);
lineData.scaledY.push(scaledData.y);
}
lineDataZipped = d3.zip(lineData.scaledX, lineData.scaledY, lineData.scaledX.slice(1), lineData.scaledY.slice(1));
lineDataZipped.forEach(function(d, i){ d.push(lineData.color); });
lineDataZipped.reverse();
queues[i](lineDataZipped);
}
if (cache.isMirror) {
cache.scaleY.range([cache.chartH / 2, cache.chartH]);
for (i = 0; i < data.length; i++) {
lineData = data[i];
lineData.scaledY2 = [];
for (j = 0; j < lineData.x.length; j++) {
scaledData = {x: lineData.scaledX[j], y: cache.scaleY(lineData.y2[j])};
lineData.scaledY2.push(scaledData.y);
}
lineDataZipped = d3.zip(lineData.scaledX, lineData.scaledY2, lineData.scaledX.slice(1), lineData.scaledY2.slice(1));
lineDataZipped.forEach(function(d, i){ d.push(lineData.color); });
lineDataZipped.reverse();
ctx.strokeStyle = lineData.color || 'silver';
ctx.beginPath();
queues[i + data.length](lineDataZipped);
ctx.stroke();
}
}
// }, 0);
}
function setupBrush(){
// Brush
/////////////////////////////
var brushChange = chartUtils.throttle(dispatch.brushChange, config.brushThrottleWaitDuration, {trailing: false});
var brushDragMove = chartUtils.throttle(dispatch.brushDragMove, config.brushThrottleWaitDuration, {trailing: false});
if (config.useBrush && data.length > 0 && data[0].x.length) {
brush.x(cache.scaleX)
.extent(brushExtent || d3.extent(data[0].x))
.on("brush", function () {
brushChange(brushExtent);
if (!d3.event.sourceEvent) {return;} // only on manual brush resize
brushExtent = brush.extent();
brushDragMove(brushExtent);
})
.on("brushstart", function(){ dispatch.brushDragStart(); })
.on("brushend", function(){ dispatch.brushDragEnd(); });
cache.interactionSvg.select('.brush-group')
.call(brush)
.selectAll('rect')
.attr({height: cache.chartH + cache.axisXHeight, y: 0});
}
}
function prepareScales (_extentX, _extentY) {
if (_extentX) {cache.scaleX.domain(_extentX);}
if (_extentY) {
var extent = config.axisYStartsAtZero ? [0, _extentY[1]] : _extentY ;
cache.scaleY.domain(extent);
}
}
exports.setConfig = function (_newConfig) {
chartUtils.override(_newConfig, config);
if (!cache.geometryCanvas) {init();}
return this;
};
exports.setZoom = function (_newExtent) {
// data[0].filter(function(d, i){ return d.x.getTime() > _newExtent[0].getTime() && d.x.getTime() > _newExtent[1].getTime(); });
prepareScales(_newExtent);
render();
return this;
};
exports.displayHoveredDots = function () {
var hoverData = data.map(function (d, i) {
return {
x: d.closestScaledX,
y: d.closestScaledY + config.margin.top,
originalData: d,
idx: i
};
});
if (cache.isMirror) {
var hoverData2 = data.map(function (d, i) {
return {
x: d.closestScaledX,
y: d.closestScaledY2 + config.margin.top,
originalData: d,
idx: i
};
});
hoverData = hoverData.concat(hoverData2);
}
var hoveredDotsSelection = cache.interactionSvg.select('.hover-group').selectAll('circle.hovered-dots')
.data(hoverData);
hoveredDotsSelection.enter().append('circle').attr({'class': 'hovered-dots'})
.on('mousemove', onDotsMouseEnter)
.on('mouseout', function (d) { dispatch.dotMouseOut(d.originalData); })
.on('click', function (d) { dispatch.dotClick(d.originalData); });
hoveredDotsSelection.filter(function(d, i){ return !isNaN(d.y); })
.style({
fill: function (d) { return d.originalData.color || 'silver'; }
})
.attr({
r: config.dotSize,
cx: function (d) { return d.x; },
cy: function (d) { return d.y; }
});
hoveredDotsSelection.exit().remove();
return this;
};
exports.hideHoveredDots = function () {
cache.interactionSvg.select('.hover-group').selectAll('circle.hovered-dots').remove();
};
function onDotsMouseEnter(d) {
var dotPos = {x: d.x, y: d.y};
dispatch.dotHover(dotPos, d.originalData);
}
exports.displayVerticalGuide = function (mouseX) {
cache.interactionSvg.select('line.hover-guide-x')
.attr({x1: mouseX, x2: mouseX, y1: 0, y2: cache.chartH})
.style({'pointer-events': 'none'});
return this;
};
exports.setBrushSelection = function (_brushSelectionExtent) {
if (brush) {
brushExtent = _brushSelectionExtent.map(function (d) { return new Date(d); });
render();
// dispatch.brushChange(brushExtent);
}
return this;
};
exports.brushIsFarRight = function () {
if(brush.extent()) {return brush.extent()[1].getTime() === cache.scaleX.domain()[1].getTime();}
};
exports.getBrushExtent = function () {
if(brush.extent()) {return brush.extent();}
};
exports.refresh = function () {
render();
return this;
};
exports.setData = function (_newData) {
data = chartUtils.deepExtend([], _newData);
function computeExtent(_data, _axis) {
var max = Number.MIN_VALUE, min = Number.MAX_VALUE, i, j, len, len2, datum;
for (i = 0, len = _data.length; i < len; i++) {
for (j = 0, len2 = _data[i].x.length; j < len2; j++) {
datum = _data[i][_axis][j];
if (datum > max) {max = datum;}
if (datum < min) {min = datum;}
}
}
return [min, max];
}
var extentX = computeExtent(data, 'x');
var extentY = computeExtent(data, 'y');
if(!!data[0] && !!data[0].y2) {
var extentY2 = computeExtent(data, 'y2');
extentY = [Math.min(extentY[0], extentY2[0]), Math.max(extentY[1], extentY2[1])];
}
prepareScales(extentX, extentY);
render();
return this;
};
exports.getSvgNode = function () {
if(cache.bgSvg) {
return cache.bgSvg.node();
}
return null;
};
exports.getCanvasNode = function () {
if(cache.geometryCanvas) {
return cache.geometryCanvas.node();
}
return null;
};
d3.rebind(exports, dispatch, "on");
return exports;
};
/* position and size */
#new-line-chart{
top: 50px;
left: 100px;
height: 400px;
width: 800px;
position: absolute;
}
#main-line-chart{
top: 100px;
}
#scrubber-line-chart{
top: 0px;
height: 100px;
}
#bar-chart{
top: 400px;
height: 100px;
}
div {
position: absolute;
}
/* style */
#new-line-chart{
box-sizing: border-box;
border: #eee 1px solid;
}
.boundary-chart path {
fill: none;
stroke: none;
}
.boundary-chart text {
fill: silver;
font-weight: lighter;
font-size: 12px;
}
.boundary-chart .domain {
fill: none;
stroke: none;
}
.boundary-chart .grid-line {
stroke: #eee;
stroke-width: 1px;
}
.boundary-chart .grid-line.y {
stroke: #eee;
stroke-width: 1px;
}
.boundary-chart .axis-x {
font-size: 1.2rem;
}
.boundary-chart .axis-x .tick text {
}
.boundary-chart .axis-y {
font-size: 1.1rem;
}
.boundary-chart .axis-x-bg, .boundary-chart .axis-y-bg {
fill: rgba(220, 220, 220, .5);
}
.boundary-chart .extent {
fill: rgba(200, 200, 200, .5);
stroke: rgba(255, 255, 255, .5);
}
.boundary-chart .stripe {
fill: rgb(250, 250, 250);
}
.boundary-chart .panel-bg {
fill: white;
}
.boundary-chart .hovered-dots {
stroke: silver;
stroke-width: 2px;
}
.boundary-chart .hover-guide-x {
stroke: silver;
stroke-width: 2px;
}
.boundary-chart-tooltip{
background: silver;
color: #eee;
border: white solid 1px;
padding: 5px;
border-radius: 10px 10px 10px 0px;
margin-top: -55px;
margin-left: -8px;
}
.boundary-chart-tooltip.left{
background: silver;
color: #eee;
border: white solid 1px;
padding: 5px;
border-radius: 10px 10px 0px 10px;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment