/* eslint-disable strict, no-unused-vars, object-curly-newline, func-names, one-var, no-var, no-console, prefer-arrow-callback, vars-on-top, no-shadow, prefer-destructuring, no-use-before-define, no-plusplus, prefer-template, no-mixed-operators, max-len */ /* global d3 */ /* Author: Bo Ericsson, https://www.linkedin.com/in/boeric00/ Inspiration from numerous examples by Mike Bostock, http://bl.ocks.org/mbostock, and example by Andy Aiken, http://blog.scottlogic.com/2014/09/19/interactive.html */ 'use strict'; function realTimeChartMulti() { var version = '0.1.0', datum, data, maxSeconds = 300, pixelsPerSecond = 10, svgWidth = 700, svgHeight = 300, margin = { top: 20, bottom: 20, left: 100, right: 30, topNav: 10, bottomNav: 20 }, dimension = { chartTitle: 20, xAxis: 20, yAxis: 20, xTitle: 20, yTitle: 20, navChart: 70 }, maxY = 100, minY = 0, chartTitle, yTitle, xTitle, drawXAxis = true, drawYAxis = true, drawNavChart = true, border, selection, barId = 0, yDomain = [], debug = false, barWidth = 5, halted = false, x, y, xNav, yNav, width, height, widthNav, heightNav, xAxisG, yAxisG, xAxis, yAxis, svg; // create the chart var chart = function (s) { selection = s; if (selection === undefined) { console.error('selection is undefined'); return; } // process titles chartTitle = chartTitle || ''; xTitle = xTitle || ''; yTitle = yTitle || ''; // compute component dimensions var chartTitleDim = chartTitle === '' ? 0 : dimension.chartTitle, xTitleDim = xTitle === '' ? 0 : dimension.xTitle, yTitleDim = yTitle === '' ? 0 : dimension.yTitle, xAxisDim = !drawXAxis ? 0 : dimension.xAxis, yAxisDim = !drawYAxis ? 0 : dimension.yAxis, navChartDim = !drawNavChart ? 0 : dimension.navChart; // compute dimension of main and nav charts, and offsets var marginTop = margin.top + chartTitleDim; height = svgHeight - marginTop - margin.bottom - chartTitleDim - xTitleDim - xAxisDim - navChartDim + 30; heightNav = navChartDim - margin.topNav - margin.bottomNav; var marginTopNav = svgHeight - margin.bottom - heightNav - margin.topNav; width = svgWidth - margin.left - margin.right; widthNav = width; // append the svg svg = selection.append('svg') .attr('width', svgWidth) .attr('height', svgHeight) .style('border', function (d) { if (border) return '1px solid lightgray'; return null; }); // create main group and translate var main = svg.append('g') .attr('transform', 'translate (' + margin.left + ',' + marginTop + ')'); // define clip-path main.append('defs').append('clipPath') .attr('id', 'myClip') .append('rect') .attr('x', 0) .attr('y', 0) .attr('width', width) .attr('height', height); // create chart background main.append('rect') .attr('x', 0) .attr('y', 0) .attr('width', width) .attr('height', height) .style('fill', '#f5f5f5'); // note that two groups are created here, the latter assigned to barG; // the former will contain a clip path to constrain objects to the chart area; // no equivalent clip path is created for the nav chart as the data itself // is clipped to the full time domain var barG = main.append('g') .attr('class', 'barGroup') .attr('transform', 'translate(0, 0)') .attr('clip-path', 'url(#myClip') .append('g'); // add group for x axis xAxisG = main.append('g') .attr('class', 'x axis') .attr('transform', 'translate(0,' + height + ')'); // add group for y axis yAxisG = main.append('g') .attr('class', 'y axis'); // in x axis group, add x axis title xAxisG.append('text') .attr('class', 'title') .attr('x', width / 2) .attr('y', 25) .attr('dy', '.71em') .text(function (d) { var text = xTitle === undefined ? '' : xTitle; return text; }); // in y axis group, add y axis title yAxisG.append('text') .attr('class', 'title') .attr('transform', 'rotate(-90)') .attr('x', -height / 2) .attr('y', -margin.left + 15) // -35 .attr('dy', '.71em') .text(function (d) { var text = yTitle === undefined ? '' : yTitle; return text; }); // in main group, add chart title main.append('text') .attr('class', 'chartTitle') .attr('x', width / 2) .attr('y', -20) .attr('dy', '.71em') .text(function (d) { var text = chartTitle === undefined ? '' : chartTitle; return text; }); // define main chart scales x = d3.time.scale().range([0, width]); y = d3.scale.ordinal().domain(yDomain).rangeRoundPoints([height, 0], 1); // define main chart axis xAxis = d3.svg.axis().orient('bottom'); yAxis = d3.svg.axis().orient('left'); // add nav chart var nav = svg.append('g') .attr('transform', 'translate (' + margin.left + ',' + marginTopNav + ')'); // add nav background nav.append('rect') .attr('x', 0) .attr('y', 0) .attr('width', width) .attr('height', heightNav) .style('fill', '#F5F5F5') .style('shape-rendering', 'crispEdges') .attr('transform', 'translate(0, 0)'); // add group to data items var navG = nav.append('g') .attr('class', 'nav'); // add group to hold nav x axis // please note that a clip path has yet to be added here (tbd) var xAxisGNav = nav.append('g') .attr('class', 'x axis') .attr('transform', 'translate(0,' + heightNav + ')'); // define nav chart scales xNav = d3.time.scale().range([0, widthNav]); yNav = d3.scale.ordinal().domain(yDomain).rangeRoundPoints([heightNav, 0], 1); // define nav axis var xAxisNav = d3.svg.axis().orient('bottom'); // compute initial time domains... var ts = new Date().getTime(); // first, the full time domain var endTime = new Date(ts); var startTime = new Date(endTime.getTime() - maxSeconds * 1000); var interval = endTime.getTime() - startTime.getTime(); // then the viewport time domain (what's visible in the main chart and the viewport in the nav chart) var endTimeViewport = new Date(ts); var startTimeViewport = new Date(endTime.getTime() - width / pixelsPerSecond * 1000); var intervalViewport = endTimeViewport.getTime() - startTimeViewport.getTime(); var offsetViewport = startTimeViewport.getTime() - startTime.getTime(); // set the scale domains for main and nav charts x.domain([startTimeViewport, endTimeViewport]); xNav.domain([startTime, endTime]); // update axis with modified scale xAxis.scale(x)(xAxisG); yAxis.scale(y)(yAxisG); xAxisNav.scale(xNav)(xAxisGNav); // create brush (moveable, changable rectangle that determines the time domain of main chart) var viewport = d3.svg.brush() .x(xNav) .extent([startTimeViewport, endTimeViewport]) .on('brush', function () { // get the current time extent of viewport var extent = viewport.extent(); startTimeViewport = extent[0]; endTimeViewport = extent[1]; // compute viewport extent in milliseconds intervalViewport = endTimeViewport.getTime() - startTimeViewport.getTime(); offsetViewport = startTimeViewport.getTime() - startTime.getTime(); // handle invisible viewport if (intervalViewport === 0) { intervalViewport = maxSeconds * 1000; offsetViewport = 0; } // update the x domain of the main chart x.domain(viewport.empty() ? xNav.domain() : extent); // update the x axis of the main chart xAxis.scale(x)(xAxisG); // update display refresh(); }); // create group and assign to brush var viewportG = nav.append('g') .attr('class', 'viewport') .call(viewport) .selectAll('rect') .attr('height', heightNav); // initial invocation; update display data = []; refresh(); // function to refresh the viz upon changes of the time domain // (which happens constantly), or after arrival of new data, or at init function refresh() { // process data to remove too late data items data = data.filter(function (d) { if (d.time.getTime() > startTime.getTime()) return true; return false; }); // determine number of categories var categoryCount = yDomain.length; if (debug) console.log('yDomain', yDomain); // here we bind the new data to the main chart // note: no key function is used here; therefore the data binding is // by index, which effectivly means that available DOM elements // are associated with each item in the available data array, from // first to last index; if the new data array contains fewer elements // than the existing DOM elements, the LAST DOM elements are removed; // basically, for each step, the data items "walks" leftward (each data // item occupying the next DOM element to the left); // This data binding is very different from one that is done with a key // function; in such a case, a data item stays "resident" in the DOM // element, and such DOM element (with data) would be moved left, until // the x position is to the left of the chart, where the item would be // exited var updateSel = barG.selectAll('.bar') .data(data); // remove items updateSel.exit().remove(); // add items updateSel.enter() .append(function (d) { if (debug) { console.log('d', JSON.stringify(d)); } if (d.type === undefined) console.error(JSON.stringify(d)); var type = d.type || 'circle'; var node = document.createElementNS('http://www.w3.org/2000/svg', type); return node; }) .attr('class', 'bar') .attr('id', function () { return 'bar-' + barId++; }); // update items; added items are now part of the update selection updateSel .attr('x', function (d) { var retVal = null; switch (getTagName(this)) { case 'rect': var size = d.size || 6; retVal = Math.round(x(d.time) - size / 2); break; default: } return retVal; }) .attr('y', function (d) { var retVal = null; switch (getTagName(this)) { case 'rect': var size = d.size || 6; retVal = y(d.category) - size / 2; break; default: } return retVal; }) .attr('cx', function (d) { var retVal = null; switch (getTagName(this)) { case 'circle': retVal = Math.round(x(d.time)); break; default: } return retVal; }) .attr('cy', function (d) { var retVal = null; switch (getTagName(this)) { case 'circle': retVal = y(d.category); break; default: } return retVal; }) .attr('r', function (d) { var retVal = null; switch (getTagName(this)) { case 'circle': retVal = d.size / 2; break; default: } return retVal; }) .attr('width', function (d) { var retVal = null; switch (getTagName(this)) { case 'rect': retVal = d.size; break; default: } return retVal; }) .attr('height', function (d) { var retVal = null; switch (getTagName(this)) { case 'rect': retVal = d.size; break; default: } return retVal; }) .style('fill', function (d) { return d.color || 'black'; }) // .style('stroke', 'orange') // .style('stroke-width', '1px') // .style('stroke-opacity', 0.8) .style('fill-opacity', function (d) { return d.opacity || 1; }); // create update selection for the nav chart, by applying data var updateSelNav = navG.selectAll('circle') .data(data); // remove items updateSelNav.exit().remove(); // add items updateSelNav.enter().append('circle') .attr('r', 1) .attr('fill', 'black'); // added items now part of update selection; set coordinates of points updateSelNav .attr('cx', function (d) { return Math.round(xNav(d.time)); }) .attr('cy', function (d) { return yNav(d.category); }); } // end refreshChart function function getTagName(that) { var tagName = d3.select(that).node().tagName; return (tagName); } // function to keep the chart 'moving' through time (right to left) setInterval(function () { if (halted) return; // get current viewport extent var extent = viewport.empty() ? xNav.domain() : viewport.extent(); var interval = extent[1].getTime() - extent[0].getTime(); var offset = extent[0].getTime() - xNav.domain()[0].getTime(); // compute new nav extents endTime = new Date(); startTime = new Date(endTime.getTime() - maxSeconds * 1000); // compute new viewport extents startTimeViewport = new Date(startTime.getTime() + offset); endTimeViewport = new Date(startTimeViewport.getTime() + interval); viewport.extent([startTimeViewport, endTimeViewport]); // update scales x.domain([startTimeViewport, endTimeViewport]); xNav.domain([startTime, endTime]); // update axis xAxis.scale(x)(xAxisG); xAxisNav.scale(xNav)(xAxisGNav); // refresh svg refresh(); }, 200); // end setInterval function return chart; }; // end chart function // chart getters/setters // new data item (this most recent item will appear // on the right side of the chart, and begin moving left) chart.datum = function (_) { if (arguments.length === 0) return datum; datum = _; data.push(datum); return chart; }; // svg width chart.width = function (_) { if (arguments.length === 0) return svgWidth; svgWidth = _; return chart; }; // svg height chart.height = function (_) { if (arguments.length === 0) return svgHeight; svgHeight = _; return chart; }; // svg border chart.border = function (_) { if (arguments.length === 0) return border; border = _; return chart; }; // chart title chart.title = function (_) { if (arguments.length === 0) return chartTitle; chartTitle = _; return chart; }; // x axis title chart.xTitle = function (_) { if (arguments.length === 0) return xTitle; xTitle = _; return chart; }; // y axis title chart.yTitle = function (_) { if (arguments.length === 0) return yTitle; yTitle = _; return chart; }; // yItems (can be dynamically added after chart construction) chart.yDomain = function (_) { if (arguments.length === 0) return yDomain; yDomain = _; if (svg) { // update the y ordinal scale y = d3.scale.ordinal().domain(yDomain).rangeRoundPoints([height, 0], 1); // update the y axis yAxis.scale(y)(yAxisG); // update the y ordinal scale for the nav chart yNav = d3.scale.ordinal().domain(yDomain).rangeRoundPoints([heightNav, 0], 1); } return chart; }; // debug chart.debug = function (_) { if (arguments.length === 0) return debug; debug = _; return chart; }; // halt chart.halt = function (_) { if (arguments.length === 0) return halted; halted = _; return chart; }; // version chart.version = version; return chart; } // end realTimeChart function