├── .babelrc ├── .gitignore ├── README.md ├── dist ├── d3_timeseries.min.css └── d3_timeseries.min.js ├── package.json ├── rollup.config.js └── src ├── d3_timeseries.css └── d3_timeseries.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", 5 | { 6 | "modules": false 7 | } 8 | ] 9 | ], 10 | "plugins": [ 11 | "external-helpers" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | node_modules 3 | 4 | npm-debug.log 5 | 6 | .eslintrc* 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # d3-timeseries 2 | Time series charting library based on d3.js 3 | 4 | ## [Examples](http://mcaule.github.io/d3-timeseries/) 5 | 6 | [ 7 | ![screenshot](http://mcaule.github.io/d3-timeseries/img/d3-timeseries_screenshot1.png) 8 | ](http://mcaule.github.io/d3-timeseries/) 9 | 10 | ## Installation 11 | 12 | ``` 13 | npm install d3-timeseries --save 14 | ``` 15 | -------------------------------------------------------------------------------- /dist/d3_timeseries.min.css: -------------------------------------------------------------------------------- 1 | .d3_timeseries.axis{font:10px sans-serif}.d3_timeseries.axis path,.d3_timeseries.axis line{fill:none;stroke:#000;stroke-width:1;shape-rendering:crispEdges}.d3_timeseries.mousevline{fill:none;stroke:#666;stroke-dasharray:3,6;stroke-width:1}.d3_timeseries.mousevline-text{font:10px sans-serif}.d3_timeseries.tooltip{position:absolute;text-align:left;font:10px sans-serif;padding:2px;background:white;border:solid #888 1px;border-radius:4px}.d3_timeseries.tooltip h4{margin-top:2px;margin-bottom:2px}.d3_timeseries.axis text{-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-o-user-select:none;user-select:none;cursor:default} 2 | -------------------------------------------------------------------------------- /dist/d3_timeseries.min.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("d3")):"function"==typeof define&&define.amd?define(["d3"],e):t.d3_timeseries=e(t.d3)}(this,function(t){"use strict";function e(t){return"function"==typeof t?t:function(e){return e[t]}}function a(t,e){var a="function"==typeof t?t:function(e){return e[t]};return function(t){return e(a(t))}}function n(t){return function(e){return e.hasOwnProperty(t)&&null!==e[t]&&!isNaN(e[t])}}function r(t){return function(e){return e[t]}}var i=["#a6cee3","#ff7f00","#b2df8a","#1f78b4","#fdbf6f","#33a02c","#cab2d6","#6a3d9a","#fb9a99","#e31a1c","#ffff99","#b15928"];return function(){function o(e){var i=e.aes;e.options.interpolate?e.options.interpolate="monotone"==e.options.interpolate?"monotoneX":"step-after"==e.options.interpolate?"stepAfter":"step-before"==e.options.interpolate?"stepBefore":e.options.interpolate:e.options.interpolate="linear";var o="curve"+e.options.interpolate[0].toUpperCase()+e.options.interpolate.slice(1);e.interpolationFunction=t[o]||t.curveLinear;var s=t.line().x(a(i.x,v)).y(a(i.y,g)).curve(e.interpolationFunction).defined(n(i.y));if(e.line=s,e.options.label=e.options.label||e.options.name||e.aes.label||e.aes.y,i.ci_up&&i.ci_down){var l=t.area().x(a(i.x,v)).y0(a(i.ci_down,g)).y1(a(i.ci_up,g)).curve(e.interpolationFunction);e.ciArea=l}i.diff&&(e.diffAreas=[t.area().x(a(i.x,v)).y0(a(i.y,g)).y1(function(t){return g(t[i.y]>t[i.diff]?t[i.diff]:t[i.y])}).curve(e.interpolationFunction),t.area().x(a(i.x,v)).y1(a(i.y,g)).y0(function(t){return g(t[i.y]1&&Number(a)-Number(e.data[n][i.x])>Number(e.data[n][i.x])-Number(e.data[n-1][i.x])?null:e.data[n]}}function s(t){if(t.linepath)t.linepath.attr("d",t.line),t.ciArea&&t.cipath.attr("d",t.ciArea),t.diffAreas&&(t.diffpaths[0].attr("d",t.diffAreas[0]),t.diffpaths[1].attr("d",t.diffAreas[1]));else{var e=_.append("path").datum(t.data).attr("class","d3_timeseries line").attr("d",t.line).attr("stroke",t.options.color).attr("stroke-linecap","round").attr("stroke-width",t.options.width||1.5).attr("fill","none");t.options.dashed&&(1==t.options.dashed||"dashed"==t.options.dashed?t["stroke-dasharray"]="5,5":"long"==t.options.dashed?t["stroke-dasharray"]="10,10":"dot"==t.options.dashed?t["stroke-dasharray"]="2,4":t["stroke-dasharray"]=t.options.dashed,e.attr("stroke-dasharray",t["stroke-dasharray"])),t.linepath=e,t.ciArea&&(t.cipath=_.insert("path",":first-child").datum(t.data).attr("class","d3_timeseries ci-area").attr("d",t.ciArea).attr("stroke","none").attr("fill",t.options.color).attr("opacity",t.options.ci_opacity||.3)),t.diffAreas&&(t.diffpaths=t.diffAreas.map(function(a,n){var r=(t.options.diff_colors?t.options.diff_colors:["green","red"])[n];return _.insert("path",function(){return e.node()}).datum(t.data).attr("class","d3_timeseries diff-area").attr("d",a).attr("stroke","none").attr("fill",r).attr("opacity",t.options.diff_opacity||.5)}))}}function l(t){var e=w.selectAll("circle.d3_timeseries.focusring");(e=null==t?e.data([]):e.data(x.map(function(e){return{x:t,item:e.find(t),aes:e.aes,color:e.options.color}}).filter(function(t){return void 0!==t.item&&null!==t.item&&null!==t.item[t.aes.y]&&!isNaN(t.item[t.aes.y])}))).transition().duration(50).attr("cx",function(t){return v(t.item[t.aes.x])}).attr("cy",function(t){return g(t.item[t.aes.y])}),e.enter().append("circle").attr("class","d3_timeseries focusring").attr("fill","none").attr("stroke-width",2).attr("r",5).attr("stroke",r("color")),e.exit().remove()}function d(t){if(null==t)E.style("opacity",0);else{var e=x.map(function(e){return{item:e.find(t),aes:e.aes,options:e.options}});E.style("opacity",.9).style("left",y.left+5+v(t)+"px").style("top","0px").html(M(t,e))}}function f(){var e=t.mouse(k.node())[0];e=v.invert(e),F.datum({x:e,visible:!0}),F.update(),l(e),d(e)}function c(){F.datum({x:null,visible:!1}),F.update(),l(null),d(null)}var p=480,u=600,m=80,h=10,y={top:10,bottom:20,left:30,right:10},x=[],g=t.scaleLinear(),v=t.scaleTime();g.label="",v.label="";var b,k,_,w,A,F,N,E,L=t.brushX();g.setformat=function(t){return t.toLocaleString()},v.setformat=v.tickFormat();var M=function(e,a){var n=''+a.filter(function(t){return void 0!==t.item&&null!==t.item}).map(function(t){return'"}).join("")+"
'+t.options.label+' '+g.setformat(t.item[t.aes.y])+"
";return"

"+v.setformat(t.timeDay(e))+"

"+n},P=function(i){x=x.map(function(a){var n=t.extent(a.data.map(e(a.aes.y)));return a.min=n[0],a.max=n[1],n=t.extent(a.data.map(e(a.aes.x))),a.dateMin=n[0],a.dateMax=n[1],a}),g.range([p-y.top-y.bottom-m-h,0]).domain([t.min(x.map(r("min"))),t.max(x.map(r("max")))]).nice(),v.range([0,u-y.left-y.right]).domain([t.min(x.map(r("dateMin"))),t.max(x.map(r("dateMax")))]).nice(),g.fixedomain&&(1==g.fixedomain.length&&g.fixedomain.push(g.domain()[1]),g.domain(g.fixedomain)),v.fixedomain&&v.domain(g.fixedomain),N=v.copy(),(b=t.select(i).append("svg").attr("width",u).attr("height",p)).append("defs").append("clipPath").attr("id","clip").append("rect").attr("width",u-y.left-y.right).attr("height",p-y.bottom-m-h).attr("y",-y.top),k=b.insert("g","rect.mouse-catch").attr("transform","translate("+y.left+","+y.top+")").attr("clip-path","url(#clip)"),_=k.append("g"),w=k.append("g"),A=b.append("g").attr("transform","translate("+y.left+","+(p-m-y.bottom)+")"),(F=b.append("g").datum({x:new Date,visible:!1})).append("line").attr("x1",0).attr("x2",0).attr("y1",g.range()[0]).attr("y2",g.range()[1]).attr("class","d3_timeseries mousevline"),F.update=function(){this.attr("transform",function(t){return"translate("+(y.left+v(t.x))+","+y.top+")"}).style("opacity",function(t){return t.visible?1:0})},F.update();var d=t.axisBottom().scale(v).tickFormat(v.setformat),M=t.axisLeft().scale(g).tickFormat(g.setformat);L.extent([[N.range()[0],0],[N.range()[1],m-h]]).on("brush",function(){var e=t.event.selection;v.domain(e.map(N.invert,N)),x.forEach(s),b.select(".focus.x.axis").call(d),F.update(),l()}).on("end",function(){null===t.event.selection&&(v.domain(N.domain()),x.forEach(s),b.select(".focus.x.axis").call(d),F.update(),l())}),b.append("g").attr("class","d3_timeseries focus x axis").attr("transform","translate("+y.left+","+(p-y.bottom-m-h)+")").call(d),A.append("g").attr("class","d3_timeseries x axis").attr("transform","translate(0,"+m+")").call(d),A.append("g").attr("class","d3_timeseries brush").call(L).attr("transform","translate(0, "+h+")").attr("height",m-h),b.append("g").attr("class","d3_timeseries y axis").attr("transform","translate("+y.left+","+y.top+")").call(M).append("text").attr("transform","rotate(-90)").attr("x",-y.top-t.mean(g.range())).attr("dy",".71em").attr("y",5-y.left).style("text-anchor","middle").text(g.label),b.append("rect").attr("width",u).attr("class","d3_timeseries mouse-catch").attr("height",p-m).style("opacity",0).on("mousemove",f).on("mouseout",c),E=t.select(i).style("position","relative").append("div").attr("class","d3_timeseries tooltip").style("opacity",0),x.forEach(o),x.forEach(s),function(){var e=g.copy().range([m-h,0]),r=x[0],i=t.line().x(a(r.aes.x,N)).y(a(r.aes.y,e)).curve(r.interpolationFunction).defined(n(r.aes.y)),o=A.insert("path",":first-child").datum(r.data).attr("class","d3_timeseries.line").attr("transform","translate(0,"+h+")").attr("d",i).attr("stroke",r.options.color).attr("stroke-width",r.options.width||1.5).attr("fill","none");r.hasOwnProperty("stroke-dasharray")&&o.attr("stroke-dasharray",r["stroke-dasharray"])}()};P.width=function(t){return arguments.length?(u=t,P):u},P.height=function(t){return arguments.length?(p=t,P):p},P.margin=function(t){return arguments.length?(y=t,P):y},t.keys(y).forEach(function(t){P.margin[t]=function(e){return arguments.length?(y[t]=e,P):y[t]}});var j=function(t){return{tickFormat:function(e){return arguments.length?(t.setformat=e,P):t.setformat},label:function(e){return arguments.length?(t.label=e,P):t.label},domain:function(e){return!arguments.length&&t.fixedomain?t.fixedomain:arguments.length?(t.fixedomain=e,P):null}}};return P.yscale=j(g),P.xscale=j(v),P.addSerie=function(t,e,a){return!t&&x.length>0&&(t=x[0].data),a.color||(a.color=i[x.length%i.length]),x.push({data:t,aes:e,options:a}),P},P}}); 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "d3-timeseries", 3 | "version": "1.0.1", 4 | "description": "Timeseries library with d3.js and SVG", 5 | "main": "src/d3_timeseries.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "rollup -c", 9 | "uglifycss": "uglifycss src/d3_timeseries.css > dist/d3_timeseries.min.css" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/mcaule/d3-timeseries.git" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/mcaule/d3-timeseries/issues" 17 | }, 18 | "homepage": "https://github.com/mcaule/d3-timeseries#readme", 19 | "keywords": [ 20 | "d3", 21 | "timeseries" 22 | ], 23 | "author": { 24 | "name":"Matthieu Caule", 25 | "email":"matthieu.caule@gmail.com" 26 | }, 27 | "license": "MIT", 28 | "devDependencies": { 29 | "babel-plugin-external-helpers": "^6.22.0", 30 | "babel-preset-env": "^1.6.1", 31 | "rollup": "^0.45.2", 32 | "rollup-plugin-babel": "^2.7.1", 33 | "rollup-plugin-node-resolve": "^3.0.0", 34 | "rollup-plugin-uglify": "^2.0.1", 35 | "uglifycss": "0.0.27" 36 | }, 37 | "dependencies": { 38 | "d3": "^4.10.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | import resolve from 'rollup-plugin-node-resolve'; 3 | import uglify from 'rollup-plugin-uglify'; 4 | 5 | export default { 6 | entry: 'src/d3_timeseries.js', 7 | format: 'umd', 8 | moduleName: 'd3_timeseries', 9 | dest: 'dist/d3_timeseries.min.js', 10 | external: ['d3'], 11 | globals: { 12 | d3: 'd3' 13 | }, 14 | 15 | plugins: [ 16 | resolve(), 17 | babel({ 18 | exclude: 'node_modules/**' // only transpile our source code 19 | }) 20 | , uglify() 21 | ], 22 | }; 23 | -------------------------------------------------------------------------------- /src/d3_timeseries.css: -------------------------------------------------------------------------------- 1 | .d3_timeseries.axis { 2 | font: 10px sans-serif; 3 | } 4 | 5 | 6 | .d3_timeseries.axis path, 7 | .d3_timeseries.axis line { 8 | fill: none; 9 | stroke: #000; 10 | stroke-width:1; 11 | shape-rendering: crispEdges; 12 | } 13 | 14 | .d3_timeseries.mousevline { 15 | fill:none; 16 | stroke: #666; 17 | stroke-dasharray: 3,6; 18 | stroke-width:1; 19 | } 20 | 21 | .d3_timeseries.mousevline-text { 22 | font: 10px sans-serif; 23 | } 24 | 25 | .d3_timeseries.tooltip { 26 | position: absolute; 27 | text-align: left; 28 | font: 10px sans-serif; 29 | 30 | padding: 2px; 31 | background: white; 32 | border: solid #888 1px; 33 | border-radius: 4px; 34 | } 35 | 36 | .d3_timeseries.tooltip h4{ 37 | margin-top: 2px; 38 | margin-bottom: 2px; 39 | } 40 | /*g.tick text,*/ 41 | .d3_timeseries.axis text{ 42 | -webkit-user-select: none; 43 | -khtml-user-select: none; 44 | -moz-user-select: none; 45 | -o-user-select: none; 46 | user-select: none; 47 | cursor: default; 48 | } 49 | -------------------------------------------------------------------------------- /src/d3_timeseries.js: -------------------------------------------------------------------------------- 1 | import * as d3 from "d3"; 2 | 3 | var defaultColors = ["#a6cee3", "#ff7f00", "#b2df8a", "#1f78b4", "#fdbf6f", "#33a02c", "#cab2d6", "#6a3d9a", "#fb9a99", "#e31a1c", "#ffff99", "#b15928"]; 4 | 5 | 6 | // utils 7 | function functorkey(v) { 8 | return typeof v === "function" ? v : function (d) { 9 | return d[v]; 10 | }; 11 | } 12 | 13 | function functorkeyscale(v, scale) { 14 | var f = typeof v === "function" ? v : function (d) { 15 | return d[v]; 16 | }; 17 | return function (d) { 18 | return scale(f(d)); 19 | }; 20 | } 21 | 22 | function keyNotNull(k) { 23 | return function (d) { 24 | return d.hasOwnProperty(k) && d[k] !== null && !isNaN(d[k]); 25 | }; 26 | } 27 | 28 | function fk(v) { 29 | return function (d) { 30 | return d[v]; 31 | }; 32 | } 33 | 34 | export default function () { 35 | // default 36 | var height = 480; 37 | var width = 600; 38 | 39 | var drawerHeight = 80; 40 | var drawerTopMargin = 10; 41 | var margin = {top: 10, bottom: 20, left: 30, right: 10}; 42 | 43 | var series = []; 44 | 45 | var yscale = d3.scaleLinear(); 46 | var xscale = d3.scaleTime(); 47 | yscale.label = ""; 48 | xscale.label = ""; 49 | 50 | var brush = d3.brushX(); 51 | 52 | var svg, container, serieContainer, annotationsContainer, drawerContainer, mousevline; 53 | var fullxscale, tooltipDiv; 54 | 55 | yscale.setformat = function (n) { 56 | return n.toLocaleString(); 57 | }; 58 | xscale.setformat = xscale.tickFormat(); 59 | 60 | // default tool tip function 61 | var _tipFunction = function (date, series) { 62 | var spans = '' + series.filter(function (d) { 63 | return d.item !== undefined && d.item !== null; 64 | }).map(function (d) { 65 | return '' + 66 | ''; 67 | }).join('') + '
' + d.options.label + ' ' + yscale.setformat(d.item[d.aes.y]) + '
'; 68 | 69 | return '

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

' + spans; 70 | }; 71 | 72 | function createLines(serie) { 73 | // https://github.com/d3/d3-shape/blob/master/README.md#curves 74 | var aes = serie.aes; 75 | 76 | if (! serie.options.interpolate) { 77 | serie.options.interpolate = "linear"; 78 | } else { 79 | // translate curvenames 80 | serie.options.interpolate = ( 81 | serie.options.interpolate == 'monotone' ? 'monotoneX' 82 | : serie.options.interpolate == 'step-after' ? 'stepAfter' 83 | : serie.options.interpolate == 'step-before' ? 'stepBefore' 84 | : serie.options.interpolate 85 | ); 86 | } 87 | // to uppercase for d3 curve name 88 | var curveName = 'curve' + serie.options.interpolate[0].toUpperCase() + serie.options.interpolate.slice(1); 89 | serie.interpolationFunction = d3[curveName] || d3.curveLinear; 90 | 91 | var line = d3.line() 92 | .x(functorkeyscale(aes.x, xscale)) 93 | .y(functorkeyscale(aes.y, yscale)) 94 | .curve(serie.interpolationFunction) 95 | .defined(keyNotNull(aes.y)); 96 | 97 | serie.line = line; 98 | 99 | serie.options.label = serie.options.label || 100 | serie.options.name || 101 | serie.aes.label || 102 | serie.aes.y; 103 | 104 | if (aes.ci_up && aes.ci_down) { 105 | var ciArea = d3.area() 106 | .x(functorkeyscale(aes.x, xscale)) 107 | .y0(functorkeyscale(aes.ci_down, yscale)) 108 | .y1(functorkeyscale(aes.ci_up, yscale)) 109 | .curve(serie.interpolationFunction); 110 | serie.ciArea = ciArea; 111 | } 112 | 113 | if (aes.diff) { 114 | serie.diffAreas = [d3.area() 115 | .x(functorkeyscale(aes.x, xscale)) 116 | .y0(functorkeyscale(aes.y, yscale)) 117 | .y1(function (d) { 118 | if (d[aes.y] > d[aes.diff]) 119 | return yscale(d[aes.diff]); 120 | return yscale(d[aes.y]); 121 | }) 122 | .curve(serie.interpolationFunction) 123 | , 124 | d3.area() 125 | .x(functorkeyscale(aes.x, xscale)) 126 | .y1(functorkeyscale(aes.y, yscale)) 127 | .y0(function (d) { 128 | if (d[aes.y] < d[aes.diff]) 129 | return yscale(d[aes.diff]); 130 | return yscale(d[aes.y]); 131 | }) 132 | .curve(serie.interpolationFunction) 133 | ]; 134 | } 135 | 136 | serie.find = function (date) { 137 | var bisect = d3.bisector(fk(aes.x)).left; 138 | var i = bisect(serie.data, date) - 1; 139 | if (i == -1) { 140 | return null; 141 | } 142 | 143 | // look to far after serie is defined 144 | if (i == serie.data.length - 1 && serie.data.length > 1 && 145 | Number(date) - Number(serie.data[i][aes.x]) > Number(serie.data[i][aes.x]) - Number(serie.data[i - 1][aes.x])) { 146 | return null; 147 | } 148 | return serie.data[i]; 149 | }; 150 | } 151 | 152 | function drawSerie(serie) { 153 | if (! serie.linepath) { 154 | var linepath = serieContainer.append("path") 155 | .datum(serie.data) 156 | .attr('class', 'd3_timeseries line') 157 | .attr('d', serie.line) 158 | .attr('stroke', serie.options.color) 159 | .attr('stroke-linecap', 'round') 160 | .attr('stroke-width', serie.options.width || 1.5) 161 | .attr('fill', 'none'); 162 | 163 | if (serie.options.dashed) { 164 | if (serie.options.dashed == true || serie.options.dashed == 'dashed') { 165 | serie['stroke-dasharray'] = '5,5'; 166 | } else if (serie.options.dashed == 'long') { 167 | serie['stroke-dasharray'] = '10,10'; 168 | } else if (serie.options.dashed == 'dot') { 169 | serie['stroke-dasharray'] = '2,4'; 170 | } else { 171 | serie['stroke-dasharray'] = serie.options.dashed; 172 | } 173 | linepath.attr('stroke-dasharray', serie['stroke-dasharray']); 174 | } 175 | serie.linepath = linepath; 176 | 177 | if (serie.ciArea) { 178 | serie.cipath = serieContainer.insert("path", ":first-child") 179 | .datum(serie.data) 180 | .attr('class', 'd3_timeseries ci-area') 181 | .attr('d', serie.ciArea) 182 | .attr('stroke', 'none') 183 | .attr('fill', serie.options.color) 184 | .attr('opacity', serie.options.ci_opacity || 0.3); 185 | } 186 | if (serie.diffAreas) { 187 | serie.diffpaths = serie.diffAreas.map( 188 | function (area, i) { 189 | var c = (serie.options.diff_colors ? serie.options.diff_colors : ['green', 'red'])[i]; 190 | return serieContainer.insert("path", 191 | function () { 192 | return linepath.node(); 193 | }) 194 | .datum(serie.data) 195 | .attr('class', 'd3_timeseries diff-area') 196 | .attr('d', area) 197 | .attr('stroke', 'none') 198 | .attr('fill', c) 199 | .attr('opacity', serie.options.diff_opacity || 0.5); 200 | }); 201 | } 202 | } else { 203 | serie.linepath.attr('d', serie.line); 204 | if (serie.ciArea) { 205 | serie.cipath.attr('d', serie.ciArea); 206 | } 207 | if (serie.diffAreas) { 208 | serie.diffpaths[0].attr('d', serie.diffAreas[0]); 209 | serie.diffpaths[1].attr('d', serie.diffAreas[1]); 210 | } 211 | } 212 | } 213 | 214 | function updatefocusRing(xdate) { 215 | var s = annotationsContainer.selectAll("circle.d3_timeseries.focusring"); 216 | 217 | if (xdate == null) { 218 | s = s.data([]); 219 | } else { 220 | s = s.data(series.map( 221 | function (s) { 222 | return {x: xdate, item: s.find(xdate), 223 | aes: s.aes, 224 | color: s.options.color}; 225 | }).filter( 226 | function (d) { 227 | return (d.item !== undefined && d.item !== null && 228 | d.item[d.aes.y] !== null && !isNaN(d.item[d.aes.y])); 229 | })); 230 | } 231 | 232 | s.transition().duration(50) 233 | .attr('cx', function (d) { 234 | return xscale(d.item[d.aes.x]); 235 | }) 236 | .attr('cy', function (d) { 237 | return yscale(d.item[d.aes.y]); 238 | }); 239 | 240 | s.enter().append("circle") 241 | .attr('class', 'd3_timeseries focusring') 242 | .attr('fill', 'none') 243 | .attr('stroke-width', 2) 244 | .attr('r', 5) 245 | .attr('stroke', fk('color')); 246 | 247 | s.exit().remove(); 248 | } 249 | 250 | function updateTip(xdate) { 251 | if (xdate == null) { 252 | tooltipDiv.style('opacity', 0); 253 | } else { 254 | var s = series.map(function (s) { 255 | return {item: s.find(xdate), 256 | aes: s.aes, options: s.options}; 257 | }); 258 | 259 | tooltipDiv.style('opacity', 0.9) 260 | .style('left', (margin.left + 5 + xscale(xdate)) + 'px') 261 | .style('top', "0px") 262 | .html(_tipFunction(xdate, s)); 263 | } 264 | } 265 | 266 | function drawMiniDrawer() { 267 | var smallyscale = yscale.copy() 268 | .range([drawerHeight - drawerTopMargin, 0]); 269 | var serie = series[0]; 270 | var line = d3.line() 271 | .x(functorkeyscale(serie.aes.x, fullxscale)) 272 | .y(functorkeyscale(serie.aes.y, smallyscale)) 273 | .curve(serie.interpolationFunction) 274 | .defined(keyNotNull(serie.aes.y)); 275 | var linepath = drawerContainer.insert("path", ":first-child") 276 | .datum(serie.data) 277 | .attr('class', 'd3_timeseries.line') 278 | .attr('transform', 'translate(0,' + drawerTopMargin + ')') 279 | .attr('d', line) 280 | .attr('stroke', serie.options.color) 281 | .attr('stroke-width', serie.options.width || 1.5) 282 | .attr('fill', 'none'); 283 | if (serie.hasOwnProperty('stroke-dasharray')) { 284 | linepath.attr('stroke-dasharray', serie['stroke-dasharray']); 285 | } 286 | } 287 | 288 | function mouseMove() { 289 | var x = d3.mouse(container.node())[0]; 290 | x = xscale.invert(x); 291 | mousevline.datum({x: x, visible: true}); 292 | mousevline.update(); 293 | updatefocusRing(x); 294 | updateTip(x); 295 | } 296 | function mouseOut() { 297 | mousevline.datum({x: null, visible: false}); 298 | mousevline.update(); 299 | updatefocusRing(null); 300 | updateTip(null); 301 | } 302 | 303 | var chart = function (elem) { 304 | // compute mins max on all series 305 | series = series.map(function (s) { 306 | var extent = d3.extent(s.data.map(functorkey(s.aes.y))); 307 | s.min = extent[0]; 308 | s.max = extent[1]; 309 | extent = d3.extent(s.data.map(functorkey(s.aes.x))); 310 | s.dateMin = extent[0]; 311 | s.dateMax = extent[1]; 312 | return s; 313 | }); 314 | 315 | 316 | // set scales 317 | 318 | yscale.range([height - margin.top - margin.bottom - drawerHeight - drawerTopMargin, 0]) 319 | .domain([d3.min(series.map(fk('min'))), 320 | d3.max(series.map(fk('max')))]) 321 | .nice(); 322 | 323 | 324 | xscale.range([0, width - margin.left - margin.right]) 325 | .domain([d3.min(series.map(fk('dateMin'))), 326 | d3.max(series.map(fk('dateMax')))]) 327 | .nice(); 328 | 329 | // if user specify domain 330 | if (yscale.fixedomain) { 331 | // for showing 0 : 332 | // chart.addSerie(...) 333 | // .yscale.domain([0]) 334 | if (yscale.fixedomain.length == 1) { 335 | yscale.fixedomain.push(yscale.domain()[1]); 336 | } 337 | yscale.domain(yscale.fixedomain); 338 | } 339 | 340 | if (xscale.fixedomain) { 341 | xscale.domain(yscale.fixedomain); 342 | } 343 | 344 | fullxscale = xscale.copy(); 345 | 346 | // create svg 347 | svg = d3.select(elem).append('svg') 348 | .attr('width', width) 349 | .attr('height', height); 350 | 351 | 352 | // clipping for scrolling in focus area 353 | svg.append('defs').append('clipPath') 354 | .attr('id', 'clip') 355 | .append('rect') 356 | .attr('width', width - margin.left - margin.right) 357 | .attr('height', height - margin.bottom - drawerHeight - drawerTopMargin) 358 | .attr('y', -margin.top); 359 | 360 | // container for focus area 361 | container = svg.insert('g', "rect.mouse-catch") 362 | .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')') 363 | .attr('clip-path', 'url(#clip)'); 364 | 365 | serieContainer = container.append('g'); 366 | annotationsContainer = container.append('g'); 367 | 368 | // mini container at the bottom 369 | drawerContainer = svg.append('g') 370 | .attr('transform', 'translate(' + margin.left + ',' + (height - drawerHeight - margin.bottom) + ')'); 371 | 372 | 373 | // vertical line moving with mouse tip 374 | mousevline = svg.append('g') 375 | .datum({ 376 | x: new Date(), 377 | visible: false 378 | }); 379 | mousevline.append('line') 380 | .attr('x1', 0) 381 | .attr('x2', 0) 382 | .attr('y1', yscale.range()[0]) 383 | .attr('y2', yscale.range()[1]) 384 | .attr('class', 'd3_timeseries mousevline'); 385 | // update mouse vline 386 | mousevline.update = function () { 387 | this.attr('transform', 388 | function (d) { 389 | return 'translate(' + (margin.left + xscale(d.x)) + ',' + margin.top + ')'; 390 | }) 391 | .style('opacity', function (d) { 392 | return d.visible ? 1 : 0; 393 | }); 394 | }; 395 | mousevline.update(); 396 | 397 | var xAxis = d3.axisBottom().scale(xscale).tickFormat(xscale.setformat); 398 | var yAxis = d3.axisLeft().scale(yscale).tickFormat(yscale.setformat); 399 | 400 | brush.extent([[fullxscale.range()[0], 0], [fullxscale.range()[1], drawerHeight - drawerTopMargin]]) 401 | 402 | .on('brush', () => { 403 | let selection = d3.event.selection; 404 | 405 | xscale.domain(selection.map(fullxscale.invert, fullxscale)); 406 | 407 | series.forEach(drawSerie); 408 | svg.select(".focus.x.axis").call(xAxis); 409 | mousevline.update(); 410 | updatefocusRing(); 411 | }) 412 | 413 | .on('end', () => { 414 | let selection = d3.event.selection; 415 | if (selection === null) { 416 | xscale.domain(fullxscale.domain()); 417 | 418 | series.forEach(drawSerie); 419 | svg.select(".focus.x.axis").call(xAxis); 420 | mousevline.update(); 421 | updatefocusRing(); 422 | } 423 | }); 424 | 425 | svg.append('g') 426 | .attr('class', 'd3_timeseries focus x axis') 427 | .attr("transform", "translate(" + margin.left + "," + (height - margin.bottom - drawerHeight - drawerTopMargin) + ")") 428 | .call(xAxis); 429 | 430 | drawerContainer.append('g') 431 | .attr('class', 'd3_timeseries x axis') 432 | .attr("transform", "translate(0," + (drawerHeight) + ")") 433 | .call(xAxis); 434 | 435 | drawerContainer.append("g") 436 | .attr("class", "d3_timeseries brush") 437 | .call(brush) 438 | .attr('transform', `translate(0, ${drawerTopMargin})`) 439 | .attr("height", (drawerHeight - drawerTopMargin)); 440 | 441 | svg.append('g') 442 | .attr('class', 'd3_timeseries y axis') 443 | .attr("transform", "translate(" + margin.left + "," + margin.top + ")") 444 | .call(yAxis) 445 | .append("text") 446 | .attr("transform", "rotate(-90)") 447 | .attr("x", -margin.top - d3.mean(yscale.range())) 448 | .attr("dy", ".71em") 449 | .attr('y', -margin.left + 5) 450 | .style("text-anchor", "middle") 451 | .text(yscale.label); 452 | 453 | // catch event for mouse tip 454 | svg.append('rect') 455 | .attr('width', width) 456 | .attr('class', 'd3_timeseries mouse-catch') 457 | .attr('height', height - drawerHeight) 458 | // .style('fill','green') 459 | .style('opacity', 0) 460 | .on('mousemove', mouseMove) 461 | .on('mouseout', mouseOut); 462 | 463 | tooltipDiv = d3.select(elem) 464 | .style('position', 'relative') 465 | .append('div') 466 | .attr('class', 'd3_timeseries tooltip') 467 | .style('opacity', 0); 468 | 469 | 470 | series.forEach(createLines); 471 | series.forEach(drawSerie); 472 | drawMiniDrawer(); 473 | }; 474 | 475 | 476 | chart.width = function (_) { 477 | if (!arguments.length) return width; 478 | width = _; 479 | return chart; 480 | }; 481 | 482 | chart.height = function (_) { 483 | if (!arguments.length) return height; 484 | height = _; 485 | return chart; 486 | }; 487 | 488 | chart.margin = function (_) { 489 | if (!arguments.length) return margin; 490 | margin = _; 491 | return chart; 492 | }; 493 | // accessors for margin.left(), margin.right(), margin.top(), margin.bottom() 494 | d3.keys(margin).forEach(function (k) { 495 | chart.margin[k] = function (_) { 496 | if (!arguments.length) return margin[k]; 497 | margin[k] = _; 498 | return chart; 499 | }; 500 | }); 501 | 502 | 503 | // scales accessors 504 | var scaleGetSet = function (scale) { 505 | return { 506 | tickFormat: function (_) { 507 | if (!arguments.length) return scale.setformat; 508 | scale.setformat = _; 509 | return chart; 510 | }, 511 | label: function (_) { 512 | if (!arguments.length) return scale.label; 513 | scale.label = _; 514 | return chart; 515 | }, 516 | domain: function (_) { 517 | if (!arguments.length && scale.fixedomain) return scale.fixedomain; 518 | if (!arguments.length) return null; 519 | scale.fixedomain = _; 520 | return chart; 521 | } 522 | }; 523 | }; 524 | 525 | chart.yscale = scaleGetSet(yscale); 526 | chart.xscale = scaleGetSet(xscale); 527 | 528 | chart.addSerie = function (data, aes, options) { 529 | if (!data && series.length > 0) 530 | data = series[0].data; 531 | if (!options.color) 532 | options.color = defaultColors[series.length % defaultColors.length]; 533 | series.push({data: data, aes: aes, options: options}); 534 | return chart; 535 | }; 536 | 537 | return chart; 538 | } 539 | --------------------------------------------------------------------------------