UNPKG

@cwi/d3-bullet

Version:
325 lines (239 loc) 7.24 kB
import { descending as d3Descending } from 'd3-array'; import { scaleLinear as d3ScaleLinear } from 'd3-scale'; import { select as d3Select } from 'd3-selection'; import { timerFlush as d3TimerFlush } from 'd3-timer'; import 'd3-transition'; function bulletRanges(d) { return d.ranges; } function bulletMarkers(d) { return d.markers; } function bulletMeasures(d) { return d.measures; } function bulletTranslate(x) { return function (d) { return 'translate(' + x(d) + ',0)'; }; } function bulletWidth(x) { return function (d) { return Math.abs(x(d) - x(0)); }; } export default function () { var orient = 'left', // TODO top & bottom reverse = false, duration = 0, ranges = bulletRanges, markers = bulletMarkers, measures = bulletMeasures, width = 380, height = 30, tickFormat = null; // For each small multiple... function bullet(element) { // Selection element.each(function (data, i) { var rangez = ranges.call(this, data, i).slice().sort(d3Descending), markerz = markers.call(this, data, i).slice(), measurez = measures.call(this, data, i).slice().sort(d3Descending), g = d3Select(this); // Compute the new x-scale. var x1 = d3ScaleLinear() .domain([0, Math.max(rangez[0], markerz[0], measurez[0])]) .range(reverse ? [width, 0] : [0, width]); // Retrieve the old x-scale, if this is an update. var x0 = this.__chart__ || d3ScaleLinear() .domain([0, Infinity]) .range(x1.range()); // Stash the new scale. this.__chart__ = x1; // Derive width-scales from the x-scales. var w0 = bulletWidth(x0), w1 = bulletWidth(x1); // Update the range rects. var range = g.selectAll('rect.range') .data(rangez); range.enter() .append('rect') .attr('class', function (d, i) { return 'range s' + i; }) .attr('width', w0) .attr('height', height) .attr('x', reverse ? x0 : 0) .transition() .duration(duration) .attr('width', w1) .attr('x', reverse ? x1 : 0); range.transition() .duration(duration) .attr('x', reverse ? x1 : 0) .attr('width', w1) .attr('height', height); // Update the measure rects. var measure = g.selectAll('rect.measure') .data(measurez); measure.enter() .append('rect') .attr('class', function (value, index) { return [ 'measure', 's' + index, 'measure-' + (value >= (data.markers[index] || 0) ? 'gt' : 'lt') + '-marker', data.onTarget ? 'on-target' : 'out-of-target' ].join(' '); }) .attr('width', w0) .attr('height', height / 3) .attr('x', reverse ? x0 : 0) .attr('y', height / 3) .transition() .duration(duration) .attr('width', w1) .attr('x', reverse ? x1 : 0); measure.transition() .duration(duration) .attr('width', w1) .attr('height', height / 3) .attr('x', reverse ? x1 : 0) .attr('y', height / 3); g.append('text') .attr('class', 'measure-label') .append('tspan') .attr('dy', '.3em') .text(measurez[0]) .attr('x', 10) .attr('y', height / 2); // Update the marker lines. var marker = g.selectAll('line.marker') .data(markerz); marker.enter() .append('line') .attr('class', function (value, index) { return 'marker marker-' + index; }) .attr('x1', x0) .attr('x2', x0) .attr('y1', height / 6) .attr('y2', height * 5 / 6) .transition() .duration(duration) .attr('x1', x1) .attr('x2', x1); marker.transition() .duration(duration) .attr('x1', x1) .attr('x2', x1) .attr('y1', height / 6) .attr('y2', height * 5 / 6); // Compute the tick format. var format = tickFormat || x1.tickFormat(8); // Update the tick groups. var tick = g.selectAll('g.tick') .data(x1.ticks(8), function (d) { return this.textContent || format(d); }); // Initialize the ticks with the old scale, x0. var tickEnter = tick.enter() .append('g') .attr('class', 'tick') .attr('transform', bulletTranslate(x0)) .style('opacity', 1e-6); tickEnter.append('line') .attr('y1', height) .attr('y2', height * 7 / 6); tickEnter.append('text') .attr('text-anchor', 'middle') .attr('dy', '1em') .attr('y', height * 7 / 6) .text(format); // Transition the entering ticks to the new scale, x1. tickEnter.transition() .duration(duration) .attr('transform', bulletTranslate(x1)) .style('opacity', 1); // Transition the updating ticks to the new scale, x1. var tickUpdate = tick.transition() .duration(duration) .attr('transform', bulletTranslate(x1)) .style('opacity', 1); tickUpdate.select('line') .attr('y1', height) .attr('y2', height * 7 / 6); tickUpdate.select('text') .attr('y', height * 7 / 6); // Transition the exiting ticks to the new scale, x1. tick.exit() .transition() .duration(duration) .attr('transform', bulletTranslate(x1)) .style('opacity', 1e-6) .remove(); }); d3TimerFlush(); } // left, right, top, bottom bullet.orient = function (x) { if (!arguments.length) { return orient; } orient = x; reverse = orient == 'right' || orient == 'bottom'; return bullet; }; // ranges (bad, satisfactory, good) bullet.ranges = function (x) { if (!arguments.length) { return ranges; } ranges = x; return bullet; }; // markers (previous, goal) bullet.markers = function (x) { if (!arguments.length) { return markers; } markers = x; return bullet; }; // measures (actual, forecast) bullet.measures = function (x) { if (!arguments.length) { return measures; } measures = x; return bullet; }; bullet.width = function (x) { if (!arguments.length) { return width; } width = x; return bullet; }; bullet.height = function (x) { if (!arguments.length) { return height; } height = x; return bullet; }; bullet.tickFormat = function (x) { if (!arguments.length) { return tickFormat; } tickFormat = x; return bullet; }; bullet.duration = function (x) { if (!arguments.length) { return duration; } duration = x; return bullet; }; return bullet; }