UNPKG

d2b

Version:

A d3-based reusable chart library.

220 lines (182 loc) 7.33 kB
import { select } from 'd3-selection'; import { interpolateObject } from 'd3-interpolate'; import { annotation } from 'd3-svg-annotation'; import 'd3-transition'; import functor from './functor'; // copy d3.annotation instance for single use cases export const copy = function (_annotation) { return annotation() .disable(_annotation.disable()) .textWrap(_annotation.textWrap()) .notePadding(_annotation.notePadding()) .type(_annotation.type()) .accessors(_annotation.accessors()) .accessorsInverse(_annotation.accessorsInverse()) .ids(_annotation.ids()) .editMode(_annotation.editMode()) .collection(_annotation.collection()); }; // Update a set of annotations, should only be used for point oriented annotations. (e.g. not rectangles / thresholds) /** * @param {d3 transition or selection} context annotation container context * @param {d3 annotation} annotation annotation generator * @param {string} selectorClass class by which to select the annotations * @param {accessor} getData get annotation data array from context datum * @param {accessor (d, a) => {}} getColor get annotation color from from context datum and annotation datum * @param {accessor (d, a) => {}} getTransform get annotation transform from context datum and annotation datum * @param {accessor (d, a) => {}} getTransformEnter get annotation entrance transform from context datum and annotation datum */ export const update = function ( context, annotation, selectorClass, getData = d => d.annotation ? [d.annotation] : [], getColor = d => d.color, getTransform = 'translate(0, 0)', getTransformEnter = 'translate(0, 0)' ) { const selection = context.selection? context.selection() : context; context.each(function (d) { const el = select(this), data = functor(getData)(d), annotationSvg = el.selectAll('g.' + selectorClass).data(data), annotationEnter = annotationSvg.enter().append('g'); let annotationUpdate = annotationSvg.merge(annotationEnter), annotationExit = annotationSvg.exit(); annotationEnter .attr('class', selectorClass) .attr('transform', a => functor(getTransformEnter)(d, a)) .style('opacity', 0); data.forEach(a => { a.x = 0; a.y = 0; a.color = functor(getColor)(d, a); }); if (data.length && annotation) { annotationUpdate.selectAll('*').remove(); annotationUpdate.call(copy(annotation).annotations(data)); } // handle annotation transitions and exiting if (context !== selection) { annotationUpdate = annotationUpdate.transition(context); annotationExit = annotationExit.transition(context); } annotationUpdate .attr('transform', a => functor(getTransform)(d, a)) .style('opacity', 1); annotationExit .attr('transform', a => functor(getTransform)(d, a)) .style('opacity', 0) .remove(); }); }; // quick implementation of shallow object clone function clone(obj) { if (null == obj || "object" != typeof obj) return obj; const copy = obj.constructor(); for (let attr in obj) { if (obj.hasOwnProperty(attr)) copy[attr] = obj[attr]; } return copy; } // Transform x, y, x2, y2 into d3.annotation pixel attributes and update axis annotations accordingly /** * @param {d3 transition or selection} context annotation container context * @param {d3 annotation} annotation annotation generator * @param {accessor} data annotation data array * @param {object} scales annotation scales { x, y, x2, y2 } */ export const updateAxis = function (context, annotation, data, scales) { const selection = context.selection ? context.selection() : context, node = selection.node(), preScales = node.__scales || scales; const getTransform = function (a, scales) { const x = scales[a.xType], y = scales[a.yType], xVal = a.x === Infinity ? x.range()[0] : x(a.x), yVal = a.y === Infinity ? y.range()[0] : y(a.y), x2Val = a.x2 === Infinity ? x.range()[1] : x(a.x2), y2Val = a.y2 === Infinity ? y.range()[1] : y(a.y2); return `translate(${[ isNaN(x2Val) ? xVal : Math.min(xVal, x2Val), isNaN(y2Val) ? yVal : Math.min(yVal, y2Val) ]})`; }; const getSubject = function (a, scales) { const subjectCopy = clone(a.data.subject) || {}, x = scales[a.xType], y = scales[a.yType], xVal = a.x === Infinity ? x.range()[0] : x(a.x), yVal = a.y === Infinity ? y.range()[0] : y(a.y), x2Val = a.x2 === Infinity ? x.range()[1] : x(a.x2), y2Val = a.y2 === Infinity ? y.range()[1] : y(a.y2), width = a.x2 ? Math.abs(xVal - x2Val) : 0, height = a.y2 ? Math.abs(yVal - y2Val) : 0; if (a.x2 && a.y2) { subjectCopy.width = width; subjectCopy.height = height; } else if (a.x2) { subjectCopy.x1 = 0; subjectCopy.x2 = width; } else if (a.y2) { subjectCopy.y1 = 0; subjectCopy.y2 = height; } subjectCopy.dx = subjectCopy.dx * width; subjectCopy.dy = subjectCopy.dy * height; return subjectCopy; }; node.__scales = { x: scales.x.copy(), y: scales.y.copy(), x2: scales.x2.copy(), y2: scales.y2.copy() }; let aSvg = selection.selectAll('.d2b-axis-annotation').data(data, d => d.key), aEnter = aSvg.enter().append('g'), aExit = aSvg.exit(); // clear out the annotaiton before updating in case of a change in annotation type aSvg.selectAll('*').remove(); aSvg = aSvg.merge(aEnter); aEnter .attr('class', 'd2b-axis-annotation') .attr('transform', a => getTransform(a, preScales)) .style('opacity', 0); if (context !== selection) { aSvg = aSvg.transition(context); aExit = aExit.transition(context); } aSvg .attr('transform', a => getTransform(a, scales)) .style('opacity', 1); aExit .attr('transform', a => getTransform(a, scales)) .style('opacity', 0) .remove(); aSvg.each(function (a) { const aCopy = clone(a.data), aSvg = select(this), annotationCopy = copy(annotation); aCopy.x = 0; aCopy.y = 0; aCopy.color = a.color; aCopy.subject = getSubject(a, scales); aCopy.dx = (aCopy.subject.dx || 0) + (a.data.dx || 0); aCopy.dy = (aCopy.subject.dy || 0) + (a.data.dy || 0); // if transitioning, custom tween the subject contents if (context !== selection) { aSvg.transition(context).tween('annotation-tween', function () { const i = interpolateObject(this.__subject || getSubject(a, scales), aCopy.subject); return t => { this.__subject = aCopy.subject = i(t); aCopy.dx = (aCopy.subject.dx || 0) + (a.data.dx || 0); aCopy.dy = (aCopy.subject.dy || 0) + (a.data.dy || 0); aSvg.call(annotationCopy.annotations([aCopy])); }; }); } else { aSvg.call(annotationCopy.annotations([aCopy])); } }); }; export default update;