Permalink
Cannot retrieve contributors at this time
Fetching contributors…
| /*! | |
| * wq.app 1.0.0-dev - wq/chart.js | |
| * Reusable SVG charts for analyzing time-series data. | |
| * (c) 2013-2016, S. Andrew Sheppard | |
| * https://wq.io/license | |
| */ | |
| define(["d3"], | |
| function(d3) { | |
| var chart = {}; | |
| function _trans(x, y, off) { | |
| if (off) { | |
| x -= 0.5; | |
| y -= 0.5; | |
| } | |
| return 'translate(' + x + ',' + y + ')'; | |
| } | |
| function _selectOrAppend(sel, name, cls) { | |
| var selector = name; | |
| if (cls) { | |
| selector += "." + cls; | |
| } | |
| var elem = sel.select(selector); | |
| if (elem.empty()) { | |
| elem = sel.append(name); | |
| if (cls) { | |
| elem.attr('class', cls); | |
| } | |
| } | |
| return elem; | |
| } | |
| // General chart configuration | |
| chart.base = function() { | |
| var width=700, height=300, padding=7.5, | |
| marginGroups = { | |
| 'padding': {'left': 10, 'right': 10, 'top': 10, 'bottom': 10}, | |
| 'xaxis': {'bottom': 20} | |
| }, | |
| viewBox=true, | |
| nestedSvg=false, | |
| renderBackground = false, | |
| chartover=false, | |
| chartout=false, | |
| xscale = null, | |
| xscalefn = d3.scale.linear, | |
| xnice = null, | |
| xticks = null, | |
| yscales = {}, | |
| yscalefn = d3.scale.linear, | |
| cscale = d3.scale.category20(), | |
| outerFill = '#f3f3f3', | |
| innerFill = '#eee', | |
| legend = null, | |
| leftYAxis = true; | |
| // Accessors for entire data object | |
| var datasets = function(d) { | |
| if (d.data) { | |
| return d.data; | |
| } | |
| return d; | |
| }; | |
| var legendItems = function(d) { | |
| return datasets(d); | |
| }; | |
| // Accessors for individual datasets | |
| var id = function(dataset) { | |
| return dataset.id; | |
| }; | |
| var label = function(dataset) { | |
| return dataset.label; | |
| }; | |
| var items = function(dataset) { | |
| return dataset.data || dataset.list; | |
| }; | |
| var legendItemId = function(d) { | |
| return id(d); | |
| }; | |
| var legendItemLabel = function(d) { | |
| return label(d); | |
| }; | |
| var xvalue = function(d) { | |
| /* jshint unused: false */ | |
| throw "xvalue accessor not defined!"; | |
| }; | |
| var yvalue = function(d) { | |
| /* jshint unused: false */ | |
| throw "yvalue accessor not defined!"; | |
| }; | |
| var xunits = function(dataset) { | |
| /* jshint unused: false */ | |
| throw "xunits accessor not defined!"; | |
| }; | |
| var xmax = function(dataset) { | |
| return d3.max(items(dataset), xvalue); | |
| }; | |
| var xmin = function(dataset) { | |
| return d3.min(items(dataset), xvalue); | |
| }; | |
| var xset = function(d) { | |
| var xvals = d3.set(); | |
| datasets(d).forEach(function(dataset) { | |
| items(dataset).forEach(function(d) { | |
| xvals.add(xvalue(d)); | |
| }); | |
| }); | |
| return xvals.values().map(function(d) { | |
| return isNaN(+d) ? d : +d; | |
| }); | |
| }; | |
| var yunits = function(dataset) { | |
| return dataset.units; | |
| }; | |
| var ymax = function(dataset) { | |
| return d3.max(items(dataset), yvalue); | |
| }; | |
| var ymin = function(dataset) { | |
| return d3.min(items(dataset), yvalue); | |
| }; | |
| // Accessors for individual items | |
| var xscaled = function(d) { | |
| return xscale.scale(xvalue(d)); | |
| }; | |
| var yscaled = function(scaleid) { | |
| var yscale = yscales[scaleid]; | |
| return function(d) { | |
| return yscale.scale(yvalue(d)); | |
| }; | |
| }; | |
| var itemid = function(d) { | |
| return xvalue(d) + "=" + yvalue(d); | |
| }; | |
| // Rendering functions (should be overridden) | |
| var init = function(datasets, opts) { | |
| /* jshint unused: false */ | |
| }; | |
| var render = function(dataset) { | |
| /* jshint unused: false */ | |
| }; | |
| var wrapup = function(datasets, opts) { | |
| /* jshint unused: false */ | |
| }; | |
| // Legend item rendering | |
| var legendItemShape = function(sid) { | |
| /* jshint unused: false */ | |
| return "rect"; | |
| }; | |
| var rectStyle = function(sid) { | |
| var color = cscale(sid); | |
| return function(sel) { | |
| sel.attr('x', -3) | |
| .attr('y', -3) | |
| .attr('width', 6) | |
| .attr('height', 6) | |
| .attr('fill', color); | |
| }; | |
| }; | |
| var circleStyle = function(sid) { | |
| var color = cscale(sid); | |
| return function(sel) { | |
| sel.attr('r', 3) | |
| .attr('fill', color) | |
| .attr('stroke', 'black') | |
| .attr('stroke-width', 0.2) | |
| .attr('cursor', 'pointer'); | |
| }; | |
| }; | |
| var legendItemStyle = function(sid) { | |
| return rectStyle(sid); | |
| }; | |
| // Generate translation function xscale + given yscale | |
| var translate = function(scaleid) { | |
| var yfn = yscaled(scaleid); | |
| return function(d) { | |
| var x = xscaled(d); | |
| var y = yfn(d); | |
| return _trans(x, y); | |
| }; | |
| }; | |
| // Plot using given selection (usually one object, but wrapped as array) | |
| function plot(sel) { | |
| if (nestedSvg) { | |
| sel = _selectOrAppend(sel, 'svg', nestedSvg); | |
| } | |
| sel.each(_plot); | |
| } | |
| // The actual work | |
| function _plot(data) { | |
| if (legend === null || legend.auto) { | |
| _positionLegend.call(this, legendItems(data)); | |
| } | |
| _computeScales(datasets(data)); | |
| var ordinal = xscalefn().rangePoints || false; | |
| var svg = d3.select(this); | |
| var uid = svg.attr('data-wq-uid') || Math.round( | |
| Math.random() * 1000000 | |
| ); | |
| svg.attr('data-wq-uid', uid); | |
| var vbstr; | |
| if (viewBox) { | |
| if (viewBox === true) { | |
| vbstr = "0 0 " + width + " " + height; | |
| } else { | |
| vbstr = viewBox; | |
| } | |
| svg.attr("viewBox", vbstr); | |
| } | |
| var cwidth = width - padding - padding; | |
| var cheight = height - padding - padding; | |
| var margins = plot.getMargins(); | |
| var gwidth = cwidth - margins.left - margins.right; | |
| var gheight = cheight - margins.top - margins.bottom; | |
| var cbottom = cheight - margins.bottom; | |
| var opts = { | |
| 'padding': padding, | |
| 'gwidth': gwidth, | |
| 'gheight': gheight, | |
| 'cwidth': cwidth, | |
| 'cheight': cheight | |
| }; | |
| init.call(this, datasets(data), opts); | |
| // Clip for inner graphing area | |
| var clipId = "clip" + uid; | |
| var defs = _selectOrAppend(svg, 'defs'); | |
| // Webkit can't select clipPath #83438 | |
| var clip = defs.select('#' + clipId); | |
| if (clip.empty()) { | |
| clip = defs.append('clipPath').attr('id', clipId); | |
| clip.append('rect'); | |
| } | |
| clip.select('rect') | |
| .attr('width', gwidth) | |
| .attr('height', gheight); | |
| // Outer chart area (includes legends, axes & actual graph) | |
| var outer = _selectOrAppend(svg, 'g', 'outer'); | |
| outer.attr('transform', _trans(padding, padding, true)); | |
| _selectOrAppend(outer, 'rect') | |
| .attr('width', cwidth) | |
| .attr('height', cheight) | |
| .attr('fill', outerFill); | |
| // Inner graphing area (clipped) | |
| var inner = _selectOrAppend(outer, 'g', 'inner') | |
| .attr('clip-path', 'url(#' + clipId + ')') | |
| .attr('transform', _trans(margins.left, margins.top)); | |
| _selectOrAppend(inner, 'rect') | |
| .attr('width', gwidth) | |
| .attr('height', gheight) | |
| .attr('fill', innerFill); | |
| if (chartover) { | |
| inner.on('mouseover', chartover(data)); | |
| inner.on('mousemove', chartover(data)); | |
| } | |
| if (chartout) { | |
| inner.on('mouseout', chartout(data)); | |
| } | |
| // Create actual scale & axis objects | |
| xscale.scale = xscalefn(); | |
| if (ordinal) { | |
| xscale.scale | |
| .domain(xset(data).sort(d3.ascending)) | |
| .rangePoints([0, gwidth], 1); | |
| } else { | |
| xscale.scale | |
| .domain([xscale.xmin, xscale.xmax]) | |
| .range([0, gwidth]); | |
| } | |
| if (xscale.scale.nice && xnice) { | |
| xscale.scale.nice(xnice); | |
| } | |
| xscale.axis = d3.svg.axis() | |
| .scale(xscale.scale) | |
| .orient('bottom') | |
| .tickSize(4, 2, 1); | |
| if (xticks) { | |
| xscale.axis.ticks(xticks); | |
| } | |
| for (var scaleid in yscales) { | |
| var scale = yscales[scaleid]; | |
| var domain; | |
| if (scale.invert) { | |
| domain = [scale.ymax, scale.ymin]; | |
| } else { | |
| domain = [scale.ymin, scale.ymax]; | |
| } | |
| scale.scale = yscalefn() | |
| .domain(domain) | |
| .nice() | |
| .range([gheight, 0]); | |
| scale.axis = d3.svg.axis() | |
| .scale(scale.scale) | |
| .orient(scale.orient) | |
| .tickSize(4, 2, 1); | |
| } | |
| // Render each dataset | |
| if (renderBackground) { | |
| var background = _selectOrAppend(inner, 'g', 'background') | |
| .selectAll('g.dataset-background') | |
| .data(datasets(data), id); | |
| background.enter() | |
| .append('g') | |
| .attr('class', 'dataset-background'); | |
| background.exit().remove(); | |
| background.each(renderBackground); | |
| } | |
| var series = _selectOrAppend(inner, 'g', 'datasets') | |
| .selectAll('g.dataset') | |
| .data(datasets(data), id); | |
| series.enter() | |
| .append('g') | |
| .attr('class', 'dataset'); | |
| series.exit().remove(); | |
| series.each(render); | |
| // Render axes | |
| _selectOrAppend(outer, 'g', 'xaxis') | |
| .attr('transform', _trans(margins.left, cbottom)) | |
| .call(xscale.axis) | |
| .selectAll('line').attr('stroke', '#000'); | |
| var yaxes = outer.selectAll('g.axis') | |
| .data(d3.values(yscales), function(s){ return s.id; }); | |
| yaxes.enter().append('g').attr('class', 'axis').append('text'); | |
| yaxes.exit().remove(); | |
| yaxes.attr('transform', function(d) { | |
| var x; | |
| if (d.orient == 'left') { | |
| x = margins.left; | |
| } else { | |
| x = cwidth - margins.right; | |
| } | |
| var y = margins.top; | |
| return _trans(x, y); | |
| }) | |
| .each(function(d) { | |
| var axis = d3.select(this); | |
| axis.call(d.axis) | |
| .selectAll('line').attr('stroke', '#000'); | |
| axis.select('text') | |
| .text(d.id || '') | |
| .attr('text-anchor', 'middle') | |
| .attr('font-weight', 'bold') | |
| .attr('transform', function() { | |
| if (d.orient == 'left') { | |
| return ( | |
| 'rotate(-90),' + | |
| 'translate(-' + gheight / 2 + ',-60)' | |
| ); | |
| } else { | |
| return ( | |
| 'rotate(90),' + | |
| 'translate(' + gheight / 2 + ',-60)' | |
| ); | |
| } | |
| }); | |
| }); | |
| if (legend && legend.position) { | |
| _renderLegend.call(this, legendItems(data), opts); | |
| } else { | |
| outer.select('g.legend').remove(); | |
| } | |
| wrapup.call(this, datasets(data), opts); | |
| } | |
| function _positionLegend(items) { | |
| var rows = items.length; | |
| if (rows > 5) { | |
| plot.legend({ | |
| 'position': 'right', | |
| 'size': 150, | |
| 'auto': true | |
| }); | |
| } else { | |
| plot.legend({ | |
| 'position': 'bottom', | |
| 'size': (rows * 22 + 20), | |
| 'auto': true | |
| }); | |
| } | |
| } | |
| // Compute horizontal & vertical scales | |
| // - may be more than one vertical scale if there are different units | |
| function _computeScales(datasets) { | |
| datasets.forEach(function(dataset) { | |
| if (!xscale) { | |
| xscale = { | |
| 'xmin': Infinity, | |
| 'xmax': -Infinity, | |
| 'auto': true | |
| }; | |
| } | |
| if (xscale.auto) { | |
| xscale.xmax = d3.max([xscale.xmax, xmax(dataset)]); | |
| xscale.xmin = d3.min([xscale.xmin, xmin(dataset)]); | |
| } | |
| var scaleid = yunits(dataset); | |
| if (!yscales[scaleid]) { | |
| yscales[scaleid] = { | |
| 'ymin': 0, | |
| 'ymax': 0, | |
| 'auto': true | |
| }; | |
| } | |
| var yscale = yscales[scaleid]; | |
| if (!yscale.id) { | |
| yscale.id = scaleid; | |
| } | |
| if (!yscale.orient) { | |
| yscale.orient = leftYAxis ? 'left' : 'right'; | |
| leftYAxis = !leftYAxis; | |
| } | |
| if (!yscale.sets) { | |
| yscale.sets = 0; | |
| } | |
| yscale.sets++; | |
| if (yscale.auto) { | |
| yscale.ymax = d3.max([yscale.ymax, ymax(dataset)]); | |
| yscale.ymin = d3.min([yscale.ymin, ymin(dataset)]); | |
| } | |
| }); | |
| var ymargin = {'left': 70}; | |
| if (d3.keys(yscales).length > 1) { | |
| ymargin.right = 70; | |
| } | |
| plot.setMargin('yaxis', ymargin); | |
| } | |
| function _renderLegend(items, opts) { | |
| var svg = d3.select(this), | |
| outer = svg.select('g.outer'), | |
| margins = plot.getMargins(), | |
| legendX, legendY, legendW, legendH; | |
| if (legend.position == 'bottom') { | |
| legendX = margins.left; | |
| legendY = opts.cheight - margins.bottom + 30; | |
| legendW = opts.gwidth; | |
| legendH = legend.size; | |
| } else { | |
| legendX = opts.cwidth - legend.size - 10; | |
| legendY = margins.top; | |
| legendW = legend.size; | |
| legendH = opts.gheight; | |
| } | |
| var leg = _selectOrAppend(outer, 'g', 'legend') | |
| .attr('transform', _trans(legendX, legendY)); | |
| _selectOrAppend(leg, 'rect') | |
| .attr('width', legendW) | |
| .attr('height', legendH) | |
| .attr('fill', 'white') | |
| .attr('stroke', '#999'); | |
| var legitems = leg.selectAll('g.legenditem') | |
| .data(items, legendItemId); | |
| var newitems = legitems.enter().append('g') | |
| .attr('class', 'legenditem') | |
| .append('g') | |
| .attr('class', 'data'); | |
| newitems.each(function(d) { | |
| var g = d3.select(this), | |
| sid = legendItemId(d); | |
| g.append(legendItemShape(sid)); | |
| g.append('text'); | |
| }); | |
| legitems.exit().remove(); | |
| legitems.each(function(d, i) { | |
| var g = d3.select(this).select('g.data'), | |
| sid = legendItemId(d); | |
| g.attr('transform', _trans(20, 20 + i * 22)); | |
| g.select(legendItemShape(sid)).call(legendItemStyle(sid)); | |
| g.select('text') | |
| .text(legendItemLabel(d)) | |
| .attr('transform', _trans(10, 5)); | |
| }); | |
| } | |
| // Getters/setters for chart configuration | |
| plot.width = function(val) { | |
| if (!arguments.length) { | |
| return width; | |
| } | |
| width = val; | |
| return plot; | |
| }; | |
| plot.height = function(val) { | |
| if (!arguments.length) { | |
| return height; | |
| } | |
| height = val; | |
| return plot; | |
| }; | |
| plot.viewBox = function(val) { | |
| if (!arguments.length) { | |
| return viewBox; | |
| } | |
| viewBox = val; | |
| return plot; | |
| }; | |
| plot.nestedSvg = function(val) { | |
| if (!arguments.length) { | |
| return nestedSvg; | |
| } | |
| nestedSvg = val; | |
| return plot; | |
| }; | |
| plot.outerFill = function(val) { | |
| if (!arguments.length) { | |
| return outerFill; | |
| } | |
| outerFill = val; | |
| return plot; | |
| }; | |
| plot.innerFill = function(val) { | |
| if (!arguments.length) { | |
| return innerFill; | |
| } | |
| innerFill = val; | |
| return plot; | |
| }; | |
| plot.legend = function(val) { | |
| if (!arguments.length) { | |
| return legend; | |
| } | |
| legend = val || {}; | |
| var lmargin = {}; | |
| if (legend.position == 'bottom') { | |
| lmargin.bottom = legend.size + 10; | |
| } else if (legend.position == 'right') { | |
| lmargin.right = legend.size + 10; | |
| } | |
| plot.setMargin('legend', lmargin); | |
| return plot; | |
| }; | |
| plot.xscale = function(val) { | |
| if (!arguments.length) { | |
| return xscale; | |
| } | |
| xscale = val; | |
| return plot; | |
| }; | |
| plot.xscalefn = function(fn) { | |
| if (!arguments.length) { | |
| return xscalefn; | |
| } | |
| xscalefn = fn; | |
| return plot; | |
| }; | |
| plot.xscaled = function(fn) { | |
| if (!arguments.length) { | |
| return xscaled; | |
| } | |
| xscaled = fn; | |
| return plot; | |
| }; | |
| plot.xnice = function(val) { | |
| if (!arguments.length) { | |
| return xnice; | |
| } | |
| xnice = val; | |
| return plot; | |
| }; | |
| plot.xticks = function(val) { | |
| if (!arguments.length) { | |
| return xticks; | |
| } | |
| xticks = val; | |
| return plot; | |
| }; | |
| plot.yscales = function(val) { | |
| if (!arguments.length) { | |
| return yscales; | |
| } | |
| yscales = val; | |
| return plot; | |
| }; | |
| plot.yscalefn = function(fn) { | |
| if (!arguments.length) { | |
| return yscalefn; | |
| } | |
| yscalefn = fn; | |
| return plot; | |
| }; | |
| plot.yscaled = function(fn) { | |
| if (!arguments.length) { | |
| return yscaled; | |
| } | |
| yscaled = fn; | |
| return plot; | |
| }; | |
| plot.cscale = function(fn) { | |
| if (!arguments.length) { | |
| return cscale; | |
| } | |
| cscale = fn; | |
| return plot; | |
| }; | |
| // Getters/setters for accessors | |
| plot.datasets = function(fn) { | |
| if (!arguments.length) { | |
| return datasets; | |
| } | |
| datasets = fn; | |
| return plot; | |
| }; | |
| plot.id = function(fn) { | |
| if (!arguments.length) { | |
| return id; | |
| } | |
| id = fn; | |
| return plot; | |
| }; | |
| plot.label = function(fn) { | |
| if (!arguments.length) { | |
| return label; | |
| } | |
| label = fn; | |
| return plot; | |
| }; | |
| plot.legendItems = function(fn) { | |
| if (!arguments.length) { | |
| return legendItems; | |
| } | |
| legendItems = fn; | |
| return plot; | |
| }; | |
| plot.legendItemId = function(fn) { | |
| if (!arguments.length) { | |
| return legendItemId; | |
| } | |
| legendItemId = fn; | |
| return plot; | |
| }; | |
| plot.legendItemLabel = function(fn) { | |
| if (!arguments.length) { | |
| return legendItemLabel; | |
| } | |
| legendItemLabel = fn; | |
| return plot; | |
| }; | |
| plot.items = function(fn) { | |
| if (!arguments.length) { | |
| return items; | |
| } | |
| items = fn; | |
| return plot; | |
| }; | |
| plot.yunits = function(fn) { | |
| if (!arguments.length) { | |
| return yunits; | |
| } | |
| yunits = fn; | |
| return plot; | |
| }; | |
| plot.xunits = function(fn) { | |
| if (!arguments.length) { | |
| return xunits; | |
| } | |
| xunits = fn; | |
| return plot; | |
| }; | |
| plot.xvalue = function(fn) { | |
| if (!arguments.length) { | |
| return xvalue; | |
| } | |
| xvalue = fn; | |
| return plot; | |
| }; | |
| plot.xmin = function(fn) { | |
| if (!arguments.length) { | |
| return xmin; | |
| } | |
| xmin = fn; | |
| return plot; | |
| }; | |
| plot.xmax = function(fn) { | |
| if (!arguments.length) { | |
| return xmax; | |
| } | |
| xmax = fn; | |
| return plot; | |
| }; | |
| plot.xset = function(fn) { | |
| if (!arguments.length) { | |
| return xset; | |
| } | |
| xset = fn; | |
| return plot; | |
| }; | |
| plot.yvalue = function(fn) { | |
| if (!arguments.length) { | |
| return yvalue; | |
| } | |
| yvalue = fn; | |
| return plot; | |
| }; | |
| plot.ymin = function(fn) { | |
| if (!arguments.length) { | |
| return ymin; | |
| } | |
| ymin = fn; | |
| return plot; | |
| }; | |
| plot.ymax = function(fn) { | |
| if (!arguments.length) { | |
| return ymax; | |
| } | |
| ymax = fn; | |
| return plot; | |
| }; | |
| plot.itemid = function(fn) { | |
| if (!arguments.length) { | |
| return itemid; | |
| } | |
| itemid = fn; | |
| return plot; | |
| }; | |
| // Getters/setters for render functions | |
| plot.init = function(fn) { | |
| if (!arguments.length) { | |
| return init; | |
| } | |
| init = fn; | |
| return plot; | |
| }; | |
| plot.chartover = function(fn) { | |
| if (!arguments.length) { | |
| return chartover; | |
| } | |
| chartover = fn; | |
| return plot; | |
| }; | |
| plot.chartout = function(fn) { | |
| if (!arguments.length) { | |
| return chartout; | |
| } | |
| chartout = fn; | |
| return plot; | |
| }; | |
| plot.renderBackground = function(fn) { | |
| if (!arguments.length) { | |
| return renderBackground; | |
| } | |
| renderBackground = fn; | |
| return plot; | |
| }; | |
| plot.render = function(fn) { | |
| if (!arguments.length) { | |
| return render; | |
| } | |
| render = fn; | |
| return plot; | |
| }; | |
| plot.wrapup = function(fn) { | |
| if (!arguments.length) { | |
| return wrapup; | |
| } | |
| wrapup = fn; | |
| return plot; | |
| }; | |
| plot.translate = function(fn) { | |
| if (!arguments.length) { | |
| return translate; | |
| } | |
| translate = fn; | |
| return plot; | |
| }; | |
| plot.legendItemShape = function(fn) { | |
| if (!arguments.length) { | |
| return legendItemShape; | |
| } | |
| legendItemShape = fn; | |
| return plot; | |
| }; | |
| plot.legendItemStyle = function(fn) { | |
| if (!arguments.length) { | |
| return legendItemStyle; | |
| } | |
| legendItemStyle = fn; | |
| return plot; | |
| }; | |
| plot.rectStyle = function(fn) { | |
| if (!arguments.length) { | |
| return rectStyle; | |
| } | |
| rectStyle = fn; | |
| return plot; | |
| }; | |
| plot.circleStyle = function(fn) { | |
| if (!arguments.length) { | |
| return circleStyle; | |
| } | |
| circleStyle = fn; | |
| return plot; | |
| }; | |
| // Inner margin has separate getter and setter as it is composed of a | |
| // number of individually-set components | |
| plot.setMargin = function(name, offsets) { | |
| marginGroups[name] = offsets; | |
| return plot; | |
| }; | |
| plot.getMargins = function() { | |
| var margins = { | |
| 'left': 0, | |
| 'right': 0, | |
| 'top': 0, | |
| 'bottom': 0 | |
| }; | |
| for (var name in marginGroups) { | |
| for (var dir in marginGroups[name]) { | |
| var val = marginGroups[name][dir]; | |
| if (val) { | |
| margins[dir] += val; | |
| } | |
| } | |
| } | |
| return margins; | |
| }; | |
| return plot; | |
| }; | |
| // Scatter plot | |
| chart.scatter = function() { | |
| var plot = chart.base(), pointStyle = plot.circleStyle(), pointShape; | |
| plot.xvalue(function(d) { | |
| return d.x; | |
| }).xunits(function(dataset) { | |
| return dataset.xunits; | |
| }).yvalue(function(d) { | |
| return d.y; | |
| }).yunits(function(dataset) { | |
| return dataset.yunits; | |
| }).legendItemShape(function(sid) { | |
| return pointShape(sid); | |
| }).legendItemStyle(function(sid) { | |
| return pointStyle(sid); | |
| }); | |
| /* To customize points beyond just the color, override these functions */ | |
| pointShape = function(sid) { | |
| /* jshint unused: false */ | |
| return "circle"; | |
| }; | |
| // pointStyle function is initialized above | |
| /* To customize lines beyond just the color, override this function */ | |
| var lineStyle = function(sid) { | |
| var color = plot.cscale()(sid); | |
| return function(sel) { | |
| sel.attr('stroke', color); | |
| }; | |
| }; | |
| var pointover = function(sid) { | |
| /* jshint unused: false */ | |
| return function(d) { | |
| d3.select(this).selectAll(pointShape(sid)) | |
| .attr('fill', '#9999ff'); | |
| }; | |
| }; | |
| var pointout = function(sid) { | |
| /* jshint unused: false */ | |
| return function(d) { | |
| d3.select(this).selectAll(pointShape(sid)) | |
| .attr('fill', plot.cscale()(sid)); | |
| }; | |
| }; | |
| var pointLabel = function(sid) { | |
| var x = plot.xvalue(), | |
| y = plot.yvalue(); | |
| return function(d) { | |
| return sid + " at " + x(d) + ": " + y(d); | |
| }; | |
| }; | |
| var drawPointsIf = function(dataset) { | |
| var items = plot.items()(dataset); | |
| return items && items.length <= 50; | |
| }; | |
| var drawLinesIf = function(dataset){ | |
| var items = plot.items()(dataset); | |
| return items && items.length > 50; | |
| }; | |
| plot.chartover(function(data) { | |
| return function() { | |
| var inner = d3.select(this), | |
| mouse = d3.mouse(this), | |
| xscale = plot.xscale(), | |
| xvalue = plot.xvalue(), | |
| translate = plot.translate(), | |
| x = xscale.scale.invert(mouse[0]), | |
| bisect = d3.bisector(xvalue), | |
| hoverData = []; | |
| plot.datasets()(data).forEach(function(dataset) { | |
| if (!drawLinesIf(dataset)) { | |
| return; | |
| } | |
| var sid = plot.id()(dataset), | |
| items = plot.items()(dataset), | |
| yunits = plot.yunits()(dataset), | |
| yscaled = plot.yscaled()(yunits), | |
| index = bisect.left(items, x), | |
| d1 = items[index > 0 ? index - 1 : 0], | |
| d2 = items[index < items.length ? index : index - 1], | |
| ptx1 = xscale.scale(xvalue(d1)), | |
| pty1 = yscaled(d1), | |
| dist1 = Math.sqrt( | |
| Math.pow(ptx1 - mouse[0], 2) + | |
| Math.pow(pty1 - mouse[1], 2) | |
| ), | |
| ptx2 = xscale.scale(xvalue(d2)), | |
| pty2 = yscaled(d2), | |
| dist2 = Math.sqrt( | |
| Math.pow(ptx2 - mouse[0], 2) + | |
| Math.pow(pty2 - mouse[1], 2) | |
| ), | |
| threshold = 20; | |
| if (dist1 < threshold || dist2 < threshold) { | |
| hoverData.push({ | |
| 'id': sid, | |
| 'units': yunits, | |
| 'data': dist1 < dist2 ? d1 : d2 | |
| }); | |
| } | |
| }); | |
| var hover = inner.selectAll('g.line-hover').data(hoverData); | |
| hover.enter().append('g') | |
| .attr('class', 'line-hover'); | |
| hover.each(function(d) { | |
| var g = d3.select(this).datum(d.data); | |
| _selectOrAppend(g, pointShape(d.id)) | |
| .call(pointStyle(d.id)) | |
| .attr('transform', translate(d.units)); | |
| _selectOrAppend(g, 'title') | |
| .text(pointLabel(d.id)); | |
| }); | |
| hover.exit().remove(); | |
| }; | |
| }); | |
| // Render lines in background to ensure all points are above them | |
| plot.renderBackground(function(dataset) { | |
| var items = plot.items()(dataset), | |
| yunits = plot.yunits()(dataset), | |
| sid = plot.id()(dataset), | |
| xscaled = plot.xscaled(), | |
| yscaled = plot.yscaled()(yunits), | |
| g = d3.select(this), | |
| path = g.select('path.data'), | |
| line = d3.svg.line() | |
| .x(xscaled) | |
| .y(yscaled); | |
| d3.select(g.node().parentNode.parentNode) | |
| .selectAll('g.line-hover').remove(); | |
| if (!drawLinesIf(dataset)) { | |
| path.remove(); | |
| return; | |
| } | |
| // Generate path element for new datasets | |
| if (path.empty()) { | |
| path = g.append('path') | |
| .attr('class', 'data') | |
| .attr('fill', 'transparent'); | |
| } | |
| // Update path for new and existing datasets | |
| path.datum(items) | |
| .attr('d', line) | |
| .call(lineStyle(sid)); | |
| }); | |
| plot.render(function(dataset) { | |
| var items = plot.items()(dataset), | |
| yunits = plot.yunits()(dataset), | |
| sid = plot.id()(dataset), | |
| translate = plot.translate(), | |
| g = d3.select(this), | |
| points, newpoints; | |
| if (!drawPointsIf(dataset)) { | |
| g.selectAll('g.data').remove(); | |
| return; | |
| } | |
| points = g.selectAll('g.data').data(items, plot.itemid()); | |
| // Generate elements for new data | |
| newpoints = points.enter().append('g') | |
| .attr('class', 'data'); | |
| newpoints.append('title'); | |
| newpoints.append(pointShape(sid)); | |
| points.exit().remove(); | |
| // Update elements for new or existing data | |
| points.on('mouseover', pointover(sid)) | |
| .on('mouseout', pointout(sid)); | |
| points.attr('transform', translate(yunits)); | |
| points.select(pointShape(sid)).call(pointStyle(sid)); | |
| points.select('title').text(pointLabel(sid)); | |
| }); | |
| // Getters/setters for chart configuration | |
| plot.pointShape = function(fn) { | |
| if (!arguments.length) { | |
| return pointShape; | |
| } | |
| pointShape = fn; | |
| return plot; | |
| }; | |
| plot.pointStyle = function(fn) { | |
| if (!arguments.length) { | |
| return pointStyle; | |
| } | |
| pointStyle = fn; | |
| return plot; | |
| }; | |
| plot.lineStyle = function(fn) { | |
| if (!arguments.length) { | |
| return lineStyle; | |
| } | |
| lineStyle = fn; | |
| return plot; | |
| }; | |
| plot.pointover = function(fn) { | |
| if (!arguments.length) { | |
| return pointover; | |
| } | |
| pointover = fn; | |
| return plot; | |
| }; | |
| plot.pointout = function(fn) { | |
| if (!arguments.length) { | |
| return pointout; | |
| } | |
| pointout = fn; | |
| return plot; | |
| }; | |
| plot.pointLabel = function(fn) { | |
| if (!arguments.length) { | |
| return pointLabel; | |
| } | |
| pointLabel = fn; | |
| return plot; | |
| }; | |
| plot.drawPointsIf = function(fn) { | |
| if (!arguments.length) { | |
| return drawPointsIf; | |
| } | |
| drawPointsIf = fn; | |
| return plot; | |
| }; | |
| plot.drawLinesIf = function(fn) { | |
| if (!arguments.length) { | |
| return drawLinesIf; | |
| } | |
| drawLinesIf = fn; | |
| return plot; | |
| }; | |
| return plot; | |
| }; | |
| // Time series scatter plot | |
| chart.timeSeries = function() { | |
| var plot = chart.scatter(), | |
| format = d3.time.format('%Y-%m-%d'); | |
| plot.xvalue(function(d) { | |
| return format.parse(d.date); | |
| }) | |
| .xscalefn(d3.time.scale) | |
| .xnice(d3.time.year) | |
| .yvalue(function(d) { | |
| return d.value; | |
| }) | |
| .yunits(function(d) { | |
| return d.units; | |
| }) | |
| .pointLabel(function(sid) { | |
| var x = plot.xvalue(), | |
| y = plot.yvalue(); | |
| return function(d) { | |
| return sid + " on " + format(x(d)) + ": " + y(d); | |
| }; | |
| }); | |
| // Getters/setters for chart configuration | |
| plot.timeFormat = function(val) { | |
| if (!arguments.length) { | |
| return format; | |
| } | |
| format = d3.time.format(val); | |
| return plot; | |
| }; | |
| return plot; | |
| }; | |
| // Contours (precomputed) | |
| chart.contour = function() { | |
| var plot = chart.scatter(); | |
| plot.legendItemShape(function(sid) { | |
| /* jshint unused: false */ | |
| return 'rect'; | |
| }).legendItemStyle( | |
| plot.rectStyle() | |
| ); | |
| plot.render(function(dataset) { | |
| var x = plot.xvalue(), | |
| y = plot.yvalue(), | |
| yunits = plot.yunits(), | |
| xscale = plot.xscale().scale, | |
| yscale = plot.yscales()[yunits(dataset)].scale, | |
| cscale = plot.cscale(), | |
| id = plot.id(), | |
| items = plot.items(); | |
| var path = d3.svg.line() | |
| .x(function(d) { | |
| return xscale(x(d)); | |
| }) | |
| .y(function(d) { | |
| return yscale(y(d)); | |
| }); | |
| var contours = d3.select(this).selectAll('path.contour') | |
| .data(items(dataset), id); | |
| contours.enter() | |
| .append('path') | |
| .attr('class', 'contour'); | |
| contours.exit().remove(); | |
| contours.attr('d', path) | |
| .attr('fill', cscale(id(dataset))); | |
| }); | |
| return plot; | |
| }; | |
| // Box & whiskers (precomputed) | |
| chart.boxplot = function() { | |
| var plot = chart.base(), r, wr, offsets = {}, prefix = 'value-'; | |
| // Accessors for individual items | |
| var q1 = function(d) { | |
| if ('p25' in d) { | |
| // Backwards compatibility with old names; remove in 1.0 | |
| return d.p25; | |
| } | |
| return d[prefix + 'q1']; | |
| }; | |
| var q3 = function(d) { | |
| if ('p75' in d) { | |
| // Backwards compatibility with old names; remove in 1.0 | |
| return d.p75; | |
| } | |
| return d[prefix + 'q3']; | |
| }; | |
| var med = function(d) { | |
| if ('median' in d) { | |
| // Backwards compatibility with old names; remove in 1.0 | |
| return d.median; | |
| } | |
| return d[prefix + 'med']; | |
| }; | |
| var whishi = function(d) { | |
| if ('max' in d) { | |
| // Backwards compatibility with old names; remove in 1.0 | |
| return d.max; | |
| } | |
| return d[prefix + 'whishi']; | |
| }; | |
| var whislo = function(d) { | |
| if ('min' in d) { | |
| // Backwards compatibility with old names; remove in 1.0 | |
| return d.min; | |
| } | |
| return d[prefix + 'whislo']; | |
| }; | |
| plot.xscalefn(d3.scale.ordinal) | |
| .itemid(function(d) { return plot.xvalue()(d); }) | |
| .ymin(function(dataset) { | |
| var items = plot.items(); | |
| return d3.min(items(dataset), function(d) { | |
| return whislo(d); | |
| }); | |
| }) | |
| .ymax(function(dataset) { | |
| var items = plot.items(); | |
| return d3.max(items(dataset), function(d) { | |
| return whishi(d); | |
| }); | |
| }) | |
| .init(function(datasets, opts) { | |
| var step = plot.xset()(datasets).length; // Number of x axis labels | |
| var slots = step * (datasets.length + 1); // ~How many boxes to fit | |
| var space = opts.gwidth / slots; // Space available for each box | |
| r = d3.min([space * 0.8 / 2, 20]); // "radius" of box (use 80%) | |
| wr = r / 2; // "radius" of whiskers | |
| var width = (datasets.length - 1) * space; | |
| datasets.forEach(function(dataset, i) { | |
| offsets[plot.id()(dataset)] = i * space - width / 2; | |
| }); | |
| }) | |
| .render(function(dataset) { | |
| var items = plot.items()(dataset), | |
| yunits = plot.yunits()(dataset), | |
| sid = plot.id()(dataset), | |
| yscales = plot.yscales(), | |
| xscale = plot.xscale(), | |
| xvalue = plot.xvalue(); | |
| function translate(scaleid) { | |
| var yscale = yscales[scaleid]; | |
| return function(d) { | |
| var x = xscale.scale(xvalue(d)); | |
| var y = yscale.scale(0); | |
| return _trans(x, y); | |
| }; | |
| } | |
| var boxes = d3.select(this).selectAll('g.data').data( | |
| items, plot.itemid() | |
| ); | |
| boxes.enter().append('g').attr('class', 'data'); | |
| boxes.exit().remove(); | |
| boxes.attr('transform', translate(yunits)) | |
| .each(box(sid, yunits)); | |
| }); | |
| function box(sid, yunits) { | |
| var yscale = plot.yscales()[yunits]; | |
| var color = plot.cscale()(sid); | |
| return function(d) { | |
| var dq1 = q1(d), | |
| dq3 = q3(d), | |
| dmed = med(d), | |
| dwhislo = whislo(d), | |
| dwhishi = whishi(d); | |
| if (!d || (!dmed && !dwhislo && !dwhishi)) { | |
| return; | |
| } | |
| function y(val) { | |
| return yscale.scale(val) - yscale.scale(0); | |
| } | |
| var box = _selectOrAppend(d3.select(this), 'g', 'box') | |
| .attr('transform', _trans(offsets[sid], 0)); | |
| _selectOrAppend(box, 'line', 'q1') | |
| .attr('x1', -r) | |
| .attr('x2', r) | |
| .attr('y1', y(dq1)) | |
| .attr('y2', y(dq1)) | |
| .attr('stroke-width', 2) | |
| .attr('stroke', color); | |
| _selectOrAppend(box, 'line', 'q3') | |
| .attr('x1', -r) | |
| .attr('x2', r) | |
| .attr('y1', y(dq3)) | |
| .attr('y2', y(dq3)) | |
| .attr('stroke-width', 2) | |
| .attr('stroke', color); | |
| _selectOrAppend(box, 'line', 'med') | |
| .attr('x1', -r) | |
| .attr('x2', r) | |
| .attr('y1', y(dmed)) | |
| .attr('y2', y(dmed)) | |
| .attr('stroke-width', 2) | |
| .attr('stroke', color); | |
| _selectOrAppend(box, 'line', 'iqr-left') | |
| .attr('x1', -r) | |
| .attr('x2', -r) | |
| .attr('y1', y(dq1)) | |
| .attr('y2', y(dq3)) | |
| .attr('stroke-width', 2) | |
| .attr('stroke', color); | |
| _selectOrAppend(box, 'line', 'iqr-right') | |
| .attr('x1', r) | |
| .attr('x2', r) | |
| .attr('y1', y(dq1)) | |
| .attr('y2', y(dq3)) | |
| .attr('stroke-width', 2) | |
| .attr('stroke', color); | |
| _selectOrAppend(box, 'line', 'w-top') | |
| .attr('x1', -wr) | |
| .attr('x2', wr) | |
| .attr('y1', y(dwhishi)) | |
| .attr('y2', y(dwhishi)) | |
| .attr('stroke', color); | |
| _selectOrAppend(box, 'line', 'w-bottom') | |
| .attr('x1', -wr) | |
| .attr('x2', wr) | |
| .attr('y1', y(dwhislo)) | |
| .attr('y2', y(dwhislo)) | |
| .attr('stroke', color); | |
| _selectOrAppend(box, 'line', 'w-q3') | |
| .attr('x1', 0) | |
| .attr('x2', 0) | |
| .attr('y1', y(dwhishi)) | |
| .attr('y2', y(dq3)) | |
| .attr('stroke', color); | |
| _selectOrAppend(box, 'line', 'w-q1') | |
| .attr('x1', 0) | |
| .attr('x2', 0) | |
| .attr('y1', y(dwhislo)) | |
| .attr('y2', y(dq1)) | |
| .attr('stroke', color); | |
| }; | |
| } | |
| // Getters/setters for accessors | |
| plot.prefix = function(val) { | |
| if (!arguments.length) { | |
| return prefix; | |
| } | |
| prefix = val; | |
| return plot; | |
| }; | |
| plot.q1 = function(fn) { | |
| if (!arguments.length) { | |
| return q1; | |
| } | |
| q1 = fn; | |
| return plot; | |
| }; | |
| plot.q3 = function(fn) { | |
| if (!arguments.length) { | |
| return q3; | |
| } | |
| q3 = fn; | |
| return plot; | |
| }; | |
| plot.med = function(fn) { | |
| if (!arguments.length) { | |
| return med; | |
| } | |
| med = fn; | |
| return plot; | |
| }; | |
| plot.whishi = function(fn) { | |
| if (!arguments.length) { | |
| return whishi; | |
| } | |
| whishi = fn; | |
| return plot; | |
| }; | |
| plot.whislo = function(fn) { | |
| if (!arguments.length) { | |
| return whislo; | |
| } | |
| whislo = fn; | |
| return plot; | |
| }; | |
| return plot; | |
| }; | |
| return chart; | |
| }); |