import * as d3 from 'd3'; import { transition } from 'd3-transition'; var defaultColors = [ '#a6cee3', '#ff7f00', '#b2df8a', '#1f78b4', '#fdbf6f', '#33a02c', '#cab2d6', '#6a3d9a', '#fb9a99', '#e31a1c', '#ffff99', '#b15928' ]; // utils function functorkey(v) { return typeof v === 'function' ? v : function(d) { return d[v]; }; } function functorkeyscale(v, scale) { var f = typeof v === 'function' ? v : function(d) { return d[v]; }; return function(d) { return scale(f(d)); }; } function keyNotNull(k) { return function(d) { return d.hasOwnProperty(k) && d[k] !== null && !isNaN(d[k]); }; } function fk(v) { return function(d) { return d[v]; }; } function main() { // default var height = 480; var width = 600; var drawerHeight = 80; var drawerTopMargin = 10; var margin = { top: 10, bottom: 20, left: 30, right: 10 }; var series = []; var yscale = d3.scaleLinear(); var xscale = d3.scaleTime(); yscale.label = ''; xscale.label = ''; var brush = d3.brushX(); var svg, container, serieContainer, annotationsContainer, drawerContainer, mousevline; var fullxscale, tooltipDiv; yscale.setformat = function(n) { return n.toLocaleString(); }; xscale.setformat = xscale.tickFormat(); // default tool tip function var _tipFunction = function(date, series) { var spans = '' + series .filter(function(d) { console.log('DDD:', d); return d.item !== undefined && d.item !== null; }) .map(function(d) { return ( '' + '' ); }) .join('') + '
' + d.options.label + ' ' + yscale.setformat(d.item[d.aes.y]) + '
'; return '

' + xscale.setformat(d3.timeDay(date)) + '

' + spans; }; function createLines(serie) { // https://github.com/d3/d3-shape/blob/master/README.md#curves var aes = serie.aes; if (!serie.options.interpolate) { serie.options.interpolate = 'linear'; } else { // translate curvenames serie.options.interpolate = serie.options.interpolate == 'monotone' ? 'monotoneX' : serie.options.interpolate == 'step-after' ? 'stepAfter' : serie.options.interpolate == 'step-before' ? 'stepBefore' : serie.options.interpolate; } // to uppercase for d3 curve name var curveName = 'curve' + serie.options.interpolate[0].toUpperCase() + serie.options.interpolate.slice(1); serie.interpolationFunction = d3[curveName] || d3.curveLinear; var line = d3 .line() .x(functorkeyscale(aes.x, xscale)) .y(functorkeyscale(aes.y, yscale)) .curve(serie.interpolationFunction) .defined(keyNotNull(aes.y)); serie.line = line; serie.options.label = serie.options.label || serie.options.name || serie.aes.label || serie.aes.y; if (aes.ci_up && aes.ci_down) { var ciArea = d3 .area() .x(functorkeyscale(aes.x, xscale)) .y0(functorkeyscale(aes.ci_down, yscale)) .y1(functorkeyscale(aes.ci_up, yscale)) .curve(serie.interpolationFunction); serie.ciArea = ciArea; } if (aes.diff) { serie.diffAreas = [ d3 .area() .x(functorkeyscale(aes.x, xscale)) .y0(functorkeyscale(aes.y, yscale)) .y1(function(d) { if (d[aes.y] > d[aes.diff]) return yscale(d[aes.diff]); return yscale(d[aes.y]); }) .curve(serie.interpolationFunction), d3 .area() .x(functorkeyscale(aes.x, xscale)) .y1(functorkeyscale(aes.y, yscale)) .y0(function(d) { if (d[aes.y] < d[aes.diff]) return yscale(d[aes.diff]); return yscale(d[aes.y]); }) .curve(serie.interpolationFunction) ]; } serie.find = function(date) { var bisect = d3.bisector(fk(aes.x)).left; var i = bisect(serie.data, date) - 1; if (i == -1) { return null; } // look to far after serie is defined if ( i == serie.data.length - 1 && serie.data.length > 1 && Number(date) - Number(serie.data[i][aes.x]) > Number(serie.data[i][aes.x]) - Number(serie.data[i - 1][aes.x]) ) { return null; } return serie.data[i]; }; } function drawSerie(serie) { if (!serie.linepath) { console.log(series); const sorted = [...series[0].data]; sorted.sort((a, b) => (a.n > b.n ? 1 : -1)); const min = sorted[0].n; const max = sorted[sorted.length - 1].n; console.log('max:', max); console.log('min:', min); const midean = (max + min) / 2; console.log('midean:', midean); var linepath = serieContainer .append('path') .datum(serie.data.filter((e) => e.n <= midean)) .attr('class', 'd3_timeseries line') .attr('d', serie.line) // .attr('stroke', serie.options.color) .attr('stroke', 'red') .attr('stroke-linecap', 'round') .attr('stroke-width', serie.options.width || 1.5) .attr('fill', 'none'); if (serie.options.dashed) { if (serie.options.dashed == true || serie.options.dashed == 'dashed') { serie['stroke-dasharray'] = '5,5'; } else if (serie.options.dashed == 'long') { serie['stroke-dasharray'] = '10,10'; } else if (serie.options.dashed == 'dot') { serie['stroke-dasharray'] = '2,4'; } else { serie['stroke-dasharray'] = serie.options.dashed; } linepath.attr('stroke-dasharray', serie['stroke-dasharray']); } serie.linepath = linepath; // serie.hotLine = hotLine; if (serie.ciArea) { serie.cipath = serieContainer .insert('path', ':first-child') .datum(serie.data) .attr('class', 'd3_timeseries ci-area') .attr('d', serie.ciArea) .attr('stroke', 'none') .attr('fill', serie.options.color) .attr('opacity', serie.options.ci_opacity || 0.3); } if (serie.diffAreas) { serie.diffpaths = serie.diffAreas.map(function(area, i) { var c = (serie.options.diff_colors ? serie.options.diff_colors : ['green', 'red'])[i]; return serieContainer .insert('path', function() { return linepath.node(); }) .datum(serie.data) .attr('class', 'd3_timeseries diff-area') .attr('d', area) .attr('stroke', 'none') .attr('fill', c) .attr('opacity', serie.options.diff_opacity || 0.5); }); } } else { serie.linepath.attr('d', serie.line); serie.linepath.attr('d', serie.hotLine); if (serie.ciArea) { serie.cipath.attr('d', serie.ciArea); } if (serie.diffAreas) { serie.diffpaths[0].attr('d', serie.diffAreas[0]); serie.diffpaths[1].attr('d', serie.diffAreas[1]); } } } function updatefocusRing(xdate) { var s = annotationsContainer.selectAll('circle.d3_timeseries.focusring'); if (xdate == null) { s = s.data([]); } else { s = s.data( series .map(function(s) { return { x: xdate, item: s.find(xdate), aes: s.aes, color: s.options.color }; }) .filter(function(d) { return ( d.item !== undefined && d.item !== null && d.item[d.aes.y] !== null && !isNaN(d.item[d.aes.y]) ); }) ); } const t = transition().duration(50); /* .attr('cx', function(d) { console.log('aDDD:', d); return xscale(d.item[d.aes.n]); }) .attr('cy', function(d) { return yscale(d.item[d.aes.date]); }); */ s.transition(t); s.enter() .append('circle') .attr('class', 'd3_timeseries focusring') .attr('fill', 'none') .attr('stroke-width', 2) .attr('r', 5) .attr('stroke', fk('color')); s.exit().remove(); } function updateTip(xdate) { if (xdate == null) { tooltipDiv.style('opacity', 0); } else { var s = series.map(function(s) { return { item: s.find(xdate), aes: s.aes, options: s.options }; }); tooltipDiv .style('opacity', 0.9) .style('left', margin.left + 5 + xscale(xdate) + 'px') .style('top', '0px') .html(_tipFunction(xdate, s)); } } function drawMiniDrawer() { var smallyscale = yscale.copy().range([drawerHeight - drawerTopMargin, 0]); var serie = series[0]; var line = d3 .line() .x(functorkeyscale(serie.aes.x, fullxscale)) .y(functorkeyscale(serie.aes.y, smallyscale)) .curve(serie.interpolationFunction) .defined(keyNotNull(serie.aes.y)); var linepath = drawerContainer .insert('path', ':first-child') .datum(serie.data) .attr('class', 'd3_timeseries.line') .attr('transform', 'translate(0,' + drawerTopMargin + ')') .attr('d', line) .attr('stroke', serie.options.color) .attr('stroke-width', serie.options.width || 1.5) .attr('fill', 'none'); if (serie.hasOwnProperty('stroke-dasharray')) { linepath.attr('stroke-dasharray', serie['stroke-dasharray']); } } function mouseMove() { var x = d3.pointer(container.node())[0]; x = xscale.invert(x); mousevline.datum({ x: x, visible: true }); mousevline.update(); updatefocusRing(x); updateTip(x); } function mouseOut() { mousevline.datum({ x: null, visible: false }); mousevline.update(); updatefocusRing(null); updateTip(null); } var chart = function(elem) { // compute mins max on all series series = series.map(function(s) { var extent = d3.extent(s.data.map(functorkey(s.aes.y))); s.min = extent[0]; s.max = extent[1]; extent = d3.extent(s.data.map(functorkey(s.aes.x))); s.dateMin = extent[0]; s.dateMax = extent[1]; return s; }); // set scales yscale .range([height - margin.top - margin.bottom - drawerHeight - drawerTopMargin, 0]) .domain([d3.min(series.map(fk('min'))), d3.max(series.map(fk('max')))]) .nice(); xscale .range([0, width - margin.left - margin.right]) .domain([d3.min(series.map(fk('dateMin'))), d3.max(series.map(fk('dateMax')))]) .nice(); // if user specify domain if (yscale.fixedomain) { // for showing 0 : // chart.addSerie(...) // .yscale.domain([0]) if (yscale.fixedomain.length == 1) { yscale.fixedomain.push(yscale.domain()[1]); } yscale.domain(yscale.fixedomain); } if (xscale.fixedomain) { xscale.domain(yscale.fixedomain); } fullxscale = xscale.copy(); // create svg svg = d3.select(elem).append('svg').attr('width', width).attr('height', height); // clipping for scrolling in focus area svg .append('defs') .append('clipPath') .attr('id', 'clip') .append('rect') .attr('width', width - margin.left - margin.right) .attr('height', height - margin.bottom - drawerHeight - drawerTopMargin) .attr('y', -margin.top); // container for focus area container = svg .insert('g', 'rect.mouse-catch') .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')') .attr('clip-path', 'url(#clip)'); serieContainer = container.append('g'); annotationsContainer = container.append('g'); // mini container at the bottom drawerContainer = svg .append('g') .attr( 'transform', 'translate(' + margin.left + ',' + (height - drawerHeight - margin.bottom) + ')' ); // vertical line moving with mouse tip mousevline = svg.append('g').datum({ x: new Date(), visible: false }); mousevline .append('line') .attr('x1', 0) .attr('x2', 0) .attr('y1', yscale.range()[0]) .attr('y2', yscale.range()[1]) .attr('class', 'd3_timeseries mousevline'); // update mouse vline mousevline.update = function() { this.attr('transform', function(d) { return 'translate(' + (margin.left + xscale(d.x)) + ',' + margin.top + ')'; }).style('opacity', function(d) { return d.visible ? 1 : 0; }); }; mousevline.update(); var xAxis = d3.axisBottom().scale(xscale).tickFormat(xscale.setformat); var yAxis = d3.axisLeft().scale(yscale).tickFormat(yscale.setformat); brush .extent([ [fullxscale.range()[0], 0], [fullxscale.range()[1], drawerHeight - drawerTopMargin] ]) .on('brush', () => { let selection = d3.event.selection; xscale.domain(selection.map(fullxscale.invert, fullxscale)); series.forEach(drawSerie); svg.select('.focus.x.axis').call(xAxis); mousevline.update(); updatefocusRing(); }) .on('end', () => { let selection = d3.event.selection; if (selection === null) { xscale.domain(fullxscale.domain()); series.forEach(drawSerie); svg.select('.focus.x.axis').call(xAxis); mousevline.update(); updatefocusRing(); } }); svg .append('g') .attr('class', 'd3_timeseries focus x axis') .attr( 'transform', 'translate(' + margin.left + ',' + (height - margin.bottom - drawerHeight - drawerTopMargin) + ')' ) .call(xAxis); drawerContainer .append('g') .attr('class', 'd3_timeseries x axis') .attr('transform', 'translate(0,' + drawerHeight + ')') .call(xAxis); drawerContainer .append('g') .attr('class', 'd3_timeseries brush') .call(brush) .attr('transform', `translate(0, ${drawerTopMargin})`) .attr('height', drawerHeight - drawerTopMargin); svg .append('g') .attr('class', 'd3_timeseries y axis') .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')') .call(yAxis) .append('text') .attr('transform', 'rotate(-90)') .attr('x', -margin.top - d3.mean(yscale.range())) .attr('dy', '.71em') .attr('y', -margin.left + 5) .style('text-anchor', 'middle') .text(yscale.label); // catch event for mouse tip svg .append('rect') .attr('width', width) .attr('class', 'd3_timeseries mouse-catch') .attr('height', height - drawerHeight) // .style('fill','green') .style('opacity', 0) .on('mousemove', mouseMove) .on('mouseout', mouseOut); tooltipDiv = d3 .select(elem) .style('position', 'relative') .append('div') .attr('class', 'd3_timeseries tooltip') .style('opacity', 0); series.forEach(createLines); series.forEach(drawSerie); drawMiniDrawer(); }; chart.width = function(_) { if (!arguments.length) return width; width = _; return chart; }; chart.height = function(_) { if (!arguments.length) return height; height = _; return chart; }; chart.margin = function(_) { if (!arguments.length) return margin; margin = _; return chart; }; // accessors for margin.left(), margin.right(), margin.top(), margin.bottom() // UNDEFINED /* d3.keys(margin).forEach(function(k) { chart.margin[k] = function(_) { if (!arguments.length) return margin[k]; margin[k] = _; return chart; }; }); */ // scales accessors var scaleGetSet = function(scale) { return { tickFormat: function(_) { if (!arguments.length) return scale.setformat; scale.setformat = _; return chart; }, label: function(_) { if (!arguments.length) return scale.label; scale.label = _; return chart; }, domain: function(_) { if (!arguments.length && scale.fixedomain) return scale.fixedomain; if (!arguments.length) return null; scale.fixedomain = _; return chart; } }; }; chart.yscale = scaleGetSet(yscale); chart.xscale = scaleGetSet(xscale); chart.addSerie = function(data, aes, options) { if (!data && series.length > 0) data = series[0].data; if (!options.color) options.color = defaultColors[series.length % defaultColors.length]; series.push({ data: data, aes: aes, options: options }); return chart; }; return chart; } export default main;