UNPKG

d2b

Version:

A d3-based reusable chart library.

1,160 lines (971 loc) 240 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var d3Selection = require('d3-selection'); var d3Interpolate = require('d3-interpolate'); require('d3-transition'); var d3Dispatch = require('d3-dispatch'); var d3Array = require('d3-array'); var d3Collection = require('d3-collection'); var d3Shape = require('d3-shape'); var d3Scales = require('d3-scale'); var d3ScaleChromatic = require('d3-scale-chromatic'); var d3Color = require('d3-color'); var d3SvgAnnotation = require('d3-svg-annotation'); var d3InterpolatePath = require('d3-interpolate-path'); var d3Format = require('d3-format'); var d3Hierarchy = require('d3-hierarchy'); var d3Sankey = require('d3-sankey'); var d3Drag = require('d3-drag'); var d3Axis = require('d3-axis'); function styleInject(css, ref) { if ( ref === void 0 ) ref = {}; var insertAt = ref.insertAt; if (!css || typeof document === 'undefined') { return; } var head = document.head || document.getElementsByTagName('head')[0]; var style = document.createElement('style'); style.type = 'text/css'; if (insertAt === 'top') { if (head.firstChild) { head.insertBefore(style, head.firstChild); } else { head.appendChild(style); } } else { head.appendChild(style); } if (style.styleSheet) { style.styleSheet.cssText = css; } else { style.appendChild(document.createTextNode(css)); } } var css = ".d2b-draggable{cursor:move}.d2b-chart-advanced{width:100%;height:100%}.d2b-pie-chart .d2b-pie-arc path{stroke-width:1px;stroke:#fff}.d2b-pie-chart .d2b-pie-arc text{fill:#fff;font-weight:700;pointer-events:none;text-anchor:middle}.d2b-axis-wrapper .d2b-axis-background{opacity:0}.d2b-chart-breadcrumbs{width:200px;padding-left:10px}.d2b-sunburst-breadcrumb .d2b-sunburst-label,.d2b-sunburst-tooltip .d2b-sunburst-label{text-align:center}.d2b-sunburst-breadcrumb .d2b-sunburst-value,.d2b-sunburst-tooltip .d2b-sunburst-value{font-size:14pt;margin-top:5px}.d2b-sunburst-breadcrumb .d2b-sunburst-percent{float:right}.d2b-breadcrumbs:not(.d2b-vertical) .d2b-sunburst-percent{margin-left:30px}.d2b-sunburst-tooltip{text-align:center}.d2b-sunburst-tooltip .d2b-sunburst-percent{display:inline}.d2b-sunburst-tooltip .d2b-sunburst-percent:before{content:\"(\"}.d2b-sunburst-tooltip .d2b-sunburst-percent:after{content:\")\"}.d2b-sankey-link-arrow{font-size:10px;position:relative;top:-2px}.d2b-line-graph .d2b-line{stroke-width:1.5px;fill:none}.d2b-area-graph .d2b-area{stroke:none;fill-opacity:.3}.d2b-box .d2b-box-center,.d2b-box .d2b-box-dash,.d2b-box .d2b-box-outlier,.d2b-box .d2b-box-rect{stroke-width:1.5px}.d2b-box .d2b-box-rect{fill:#fff;fill-opacity:.3}.d2b-box .d2b-box-center{stroke-dasharray:3 3}.d2b-box .d2b-box-label{fill:#555;font-size:10pt}.d2b-box .d2b-box-outlier{fill:none;stroke-opacity:.4}.d2b-bar-graph .d2b-bar-group rect{opacity:.8}.d2b-bubble-pack-graph .d2b-bubble-point{opacity:.75}.d2b-bubble-pack-graph .d2b-bubble-indicator rect{fill-opacity:.25;stroke-opacity:.9;cursor:pointer;stroke-width:1px}.d2b-bubble-pack-graph .d2b-bubble-indicator rect:hover{stroke-width:2px}.d2b-bubble-pack-graph .d2b-bubble-indicator path,.d2b-bubble-pack-graph .d2b-bubble-indicator text{pointer-events:none}.d2b-plane text{font-size:10pt}.d2b-plane .d2b-y2-axis .d2b-axis-label,.d2b-plane .d2b-y-axis .d2b-axis-label{-webkit-transform:rotate(-90deg);transform:rotate(-90deg)}.d2b-plane .d2b-y2-axis .tick line,.d2b-plane .d2b-y-axis .tick line{stroke-width:.6px}.d2b-plane .d2b-grid .tick line{shape-rendering:crispEdges;stroke-width:.5px;stroke-opacity:.15}.d2b-plane .d2b-grid .tick text{display:none}.d2b-plane .d2b-axis-label{fill:#000;font-weight:700}.d2b-plane path.domain{stroke-width:.4px;stroke-opacity:.4}.d2b-sunburst-arc{transition:opacity .2s,stroke-width .2s;cursor:pointer;opacity:.9;stroke-width:.8px;stroke:#fff}.d2b-sunburst-arc.d2b-transparent{fill-opacity:.2}.d2b-sunburst-label{font-size:8pt;fill-opacity:.8;pointer-events:none;font-family:arial}.d2b-sunburst-label.d2b-transparent{fill-opacity:.2}.d2b-sunburst-center{fill-opacity:0;stroke:none;cursor:pointer}.d2b-sunburst-ancestor{opacity:.4}.d2b-sankey-links path{fill:none}.d2b-sankey-links path,.d2b-sankey-links rect{opacity:.4}.d2b-sankey-links path:hover,.d2b-sankey-links rect:hover{opacity:.6}.d2b-sankey-nodes rect{stroke-width:.3px;stroke:#000;opacity:.6}.d2b-sankey-nodes rect:hover{opacity:.8}.d2b-text-anchor-end{text-anchor:end}.d2b-tooltip{pointer-events:none;background:#fff;border:1px solid #bbb;border-radius:2px;position:fixed;box-shadow:0 0 2px #ccc;margin:0}.d2b-tooltip:before{left:0;top:0;position:absolute;z-index:2;content:\"\"}.d2b-tooltip-content{white-space:nowrap;padding:6px 10px}.d2b-tooltip-top{-webkit-transform:translate(-50%,-100%);transform:translate(-50%,-100%);margin-left:0;margin-top:-15px}.d2b-tooltip-top:before{-webkit-transform:translateX(-50%);transform:translateX(-50%);left:50%;top:100%;border-color:transparent;border-style:solid;border-width:6px 7px 0;border-top-color:inherit}.d2b-tooltip-bottom{-webkit-transform:translate(-50%);transform:translate(-50%);margin-left:0;margin-top:15px}.d2b-tooltip-bottom:before{-webkit-transform:translateX(-50%);transform:translateX(-50%);left:50%;top:-6px;border-color:transparent;border-style:solid;border-width:0 7px 6px;border-bottom-color:inherit}.d2b-tooltip-right{margin-left:15px;margin-top:0}.d2b-tooltip-right,.d2b-tooltip-right:before{-webkit-transform:translateY(-50%);transform:translateY(-50%)}.d2b-tooltip-right:before{left:-6px;top:50%;border-color:transparent;border-style:solid;border-width:7px 6px 7px 0;border-right-color:inherit}.d2b-tooltip-left{-webkit-transform:translate(-100%,-50%);transform:translate(-100%,-50%);margin-left:-15px;margin-top:0}.d2b-tooltip-left:before{-webkit-transform:translateY(-50%);transform:translateY(-50%);left:100%;top:50%;border-color:transparent;border-style:solid;border-width:7px 0 7px 6px;border-left-color:inherit}.d2b-tooltip-marker{stroke-width:2px;stroke:#bbb;stroke-dasharray:5,5}.d2b-tooltip-axis{pointer-events:none;font-family:Arial;opacity:.9;white-space:nowrap;border:1px solid #ddd;background:hsla(0,0%,100%,.8);position:absolute;box-shadow:0 0 2px #ccc}.d2b-tooltip-axis .d2b-tooltip-title{text-align:center;background:rgba(150,165,175,.8);padding:5px 10px;font-weight:700;color:#fff}.d2b-tooltip-axis .d2b-tooltip-content{padding:5px 10px}.d2b-tooltip-axis .d2b-tooltip-content .d2b-tooltip-row{padding-left:5px;border-left-width:3px;border-left-style:solid;margin-bottom:5px}.d2b-tooltip-axis .d2b-tooltip-content .d2b-tooltip-row:last-child{margin-bottom:0}.d2b-breadcrumbs{color:#555;font-size:9pt;box-sizing:content-box}.d2b-breadcrumbs .d2b-breadcrumb{border-color:transparent;border-style:solid;border-width:0 0 1px 8px;padding:6px 13px;margin-bottom:10px;text-transform:uppercase;position:relative}.d2b-breadcrumbs .d2b-breadcrumb:first-child .d2b-breadcrumb-icon:after,.d2b-breadcrumbs .d2b-breadcrumb:last-child .d2b-breadcrumb-icon:after{position:absolute;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);left:-15.5px;width:13px;height:13px;text-align:center;font-family:FontAwesome,Font Awesome\\ 5 Free;font-weight:700;color:#fff;background-color:inherit;padding:5px;border-radius:15px}.d2b-breadcrumbs .d2b-breadcrumb:first-child .d2b-breadcrumb-icon:after{content:\"\\f015\"}.d2b-breadcrumbs .d2b-breadcrumb:not(:first-child):last-child .d2b-breadcrumb-icon:after{content:\"\\f25a\"}.d2b-breadcrumbs:not(.d2b-vertical){white-space:nowrap}.d2b-breadcrumbs:not(.d2b-vertical) .d2b-breadcrumb{border-right-width:1px;border-top-width:1px;white-space:nowrap;display:inline-block}.d2b-breadcrumbs:not(.d2b-vertical) .d2b-breadcrumb:not(:last-child){margin-right:20px}.d2b-breadcrumbs:not(.d2b-vertical) .d2b-breadcrumb:not(:last-child):after{border-color:transparent;border-style:solid;border-width:7px 0 7px 6px;border-left-color:inherit;position:absolute;z-index:2;content:\"\";top:50%;left:100%;-webkit-transform:translateY(-50%);transform:translateY(-50%)}.d2b-breadcrumbs.d2b-vertical .d2b-breadcrumb:not(:last-child):after{border-color:transparent;border-style:solid;border-width:6px 7px 0;border-top-color:inherit;height:6px;background-color:inherit;position:absolute;z-index:2;content:\"\";top:100%;left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%)}.d2b-legend{color:#555;font-size:9pt}.d2b-legend .d2b-legend-item{border:0 solid transparent;padding:2px 2px 2px 18px;position:relative;cursor:pointer}.d2b-legend .d2b-legend-item .d2b-legend-icon{position:absolute;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);left:7px;width:12px;height:12px}.d2b-legend .d2b-legend-item .d2b-legend-svg-icon{margin-left:-5px;margin-top:-5px;pointer-events:none}.d2b-legend .d2b-legend-item .d2b-legend-svg-icon path,.d2b-legend .d2b-legend-item .d2b-legend-svg-icon text{font-family:FontAwesome,Font Awesome\\ 5 Free;font-weight:700;text-anchor:middle;stroke-width:1px;fill-opacity:.8}.d2b-legend:not(.d2b-vertical) .d2b-legend-item{display:inline-block}.d2b-legend:not(.d2b-vertical) .d2b-legend-item:not(:last-child){margin-right:10px}.d2b-legend:not(.d2b-vertical) .d2b-legend-icon{margin-top:1.5px}.d2b-chart-frame{height:100%;width:100%;position:relative}.d2b-chart-frame .d2b-breadcrumbs-frame,.d2b-chart-frame .d2b-chart,.d2b-chart-frame .d2b-legend-frame{position:absolute;overflow:auto}.d2b-chart-frame .d2b-breadcrumbs-frame .d2b-breadcrumbs,.d2b-chart-frame .d2b-legend-frame .d2b-breadcrumbs{padding-left:10px;padding-right:2px}.d2b-chart-frame .d2b-breadcrumbs-frame:not(.d2b-vertical),.d2b-chart-frame .d2b-legend-frame:not(.d2b-vertical){overflow-y:hidden}.d2b-chart-frame .d2b-breadcrumbs-frame:not(.d2b-vertical).d2b-legend-frame,.d2b-chart-frame .d2b-legend-frame:not(.d2b-vertical).d2b-legend-frame{height:20px;text-align:center}.d2b-chart-frame .d2b-breadcrumbs-frame:not(.d2b-vertical).d2b-breadcrumbs-frame,.d2b-chart-frame .d2b-legend-frame:not(.d2b-vertical).d2b-breadcrumbs-frame{height:55px}.d2b-chart-frame .d2b-breadcrumbs-frame:not(.d2b-vertical) .d2b-breadcrumbs,.d2b-chart-frame .d2b-breadcrumbs-frame:not(.d2b-vertical) .d2b-legend,.d2b-chart-frame .d2b-breadcrumbs-frame:not(.d2b-vertical) .d2b-legend-item,.d2b-chart-frame .d2b-legend-frame:not(.d2b-vertical) .d2b-breadcrumbs,.d2b-chart-frame .d2b-legend-frame:not(.d2b-vertical) .d2b-legend,.d2b-chart-frame .d2b-legend-frame:not(.d2b-vertical) .d2b-legend-item{white-space:nowrap;overflow-y:hidden}.d2b-chart-frame .d2b-breadcrumbs-frame.d2b-vertical.d2b-breadcrumbs-frame,.d2b-chart-frame .d2b-legend-frame.d2b-vertical.d2b-breadcrumbs-frame{width:180px}.d2b-chart-frame .d2b-breadcrumbs-frame.d2b-vertical.d2b-legend-frame,.d2b-chart-frame .d2b-legend-frame.d2b-vertical.d2b-legend-frame{width:110px}.d2b-chart-frame .d2b-breadcrumbs-frame.d2b-vertical .d2b-breadcrumbs-container,.d2b-chart-frame .d2b-breadcrumbs-frame.d2b-vertical .d2b-legend-container,.d2b-chart-frame .d2b-legend-frame.d2b-vertical .d2b-breadcrumbs-container,.d2b-chart-frame .d2b-legend-frame.d2b-vertical .d2b-legend-container{display:table;height:100%;width:100%}.d2b-chart-frame .d2b-breadcrumbs-frame.d2b-vertical .d2b-breadcrumbs-container .d2b-legend,.d2b-chart-frame .d2b-breadcrumbs-frame.d2b-vertical .d2b-legend-container .d2b-legend,.d2b-chart-frame .d2b-legend-frame.d2b-vertical .d2b-breadcrumbs-container .d2b-legend,.d2b-chart-frame .d2b-legend-frame.d2b-vertical .d2b-legend-container .d2b-legend{display:table-cell;vertical-align:middle}.d2b-chart-frame .d2b-breadcrumbs-frame.d2b-vertical .d2b-breadcrumbs-container .d2b-breadcrumbs,.d2b-chart-frame .d2b-breadcrumbs-frame.d2b-vertical .d2b-legend-container .d2b-breadcrumbs,.d2b-chart-frame .d2b-legend-frame.d2b-vertical .d2b-breadcrumbs-container .d2b-breadcrumbs,.d2b-chart-frame .d2b-legend-frame.d2b-vertical .d2b-legend-container .d2b-breadcrumbs{display:table-cell;vertical-align:top}"; styleInject(css); var version = "1.0.10"; function functor(v) { return typeof v === 'function' ? v : function () { return v; }; } // Work around for JavaScripts ||= operator. Only null, undefined, NaN, and false will be construed as falsy. function oreq () { var truthy = function truthy(v) { return v !== null && v !== undefined && v !== false; }; for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } var val = args.filter(truthy)[0]; return truthy(val) ? val : undefined; } // d3's enter/update/exit strategy as to be less destructive on the text content. function textWrap (text) { var getText = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : function (d) { return d.label; }; var getCount = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : Infinity; var getAnchor = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 'start'; getText = functor(getText); getCount = functor(getCount); getAnchor = functor(getAnchor); text.each(function (d, i) { var text = d3Selection.select(this), words = "".concat(getText.call(this, d, i)).split(/\s+/).reverse(), word, lines = [], line = [words.pop()], lineHeight = 1.1, count = getCount.call(this, d, i), anchor = getAnchor.call(this, d, i), x = +text.attr('x'), y = +text.attr('y'), dy = parseFloat(text.attr('dy')) || 0; // clear text if the wrapper is being run for the first time if (oreq(text.html(), '').indexOf('tspan') === -1) text.text(''); word = words.pop(); while (word) { if (line.join(' ').length + word.length > count) { lines.push(line); line = []; } line.push(word); word = words.pop(); } lines.push(line); var tspan = text.selectAll('tspan').data(lines), height = (lines.length - 1) * lineHeight, offset = anchor === 'end' ? height : anchor === 'middle' ? height / 2 : 0; tspan.merge(tspan.enter().append('tspan')).text(function (d) { return d.join(' '); }).attr('x', x).attr('y', y).attr('dy', function (d, i) { return "".concat(dy + i * lineHeight - offset, "em"); }); }); } // This isn't used very frequently because it causes problems with event // rebinding namely double click events. function textWrapPX (text) { var width = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : Infinity; text.each(function () { var text = d3Selection.select(this), words = text.text().split(/\s+/).reverse(), word, line = [], lineNumber = 0, lineHeight = 1.1, // ems y = parseFloat(text.attr('y')) || 0, dy = parseFloat(text.attr('dy')) || 0, tspan = text.text(null).append('tspan').attr('x', 0).attr('y', y).attr('dy', dy + 'em'); word = words.pop(); while (word) { line.push(word); tspan.text(line.join(' ')); if (tspan.node().getComputedTextLength() > width) { line.pop(); tspan.text(line.join(' ')); line = [word]; tspan = text.append('tspan').attr('x', 0).attr('y', y).attr('dy', ++lineNumber * lineHeight + dy + 'em').text(word); } word = words.pop(); } }); } function tweenArc (context, arc) { var reform = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true; // Reform the arc config methods so that they will first look at the arc datum // for the config property and otherwise they will fallback to the arcs methods // original callback if (reform) { ['cornerRadius', 'innerRadius', 'outerRadius', 'startAngle', 'endAngle', 'cornerRadius', 'padAngle'].forEach(function (method) { var methodSave = arc[method](); if (methodSave && !methodSave.reformed) { var methodReformed = function methodReformed(d) { return d[method] || methodSave(d); }; methodReformed.reformed = true; arc[method](methodReformed); } }); } function getProperties(d) { return { innerRadius: arc.innerRadius()(d), outerRadius: arc.outerRadius()(d), startAngle: arc.startAngle()(d), endAngle: arc.endAngle()(d), cornerRadius: arc.cornerRadius()(d), padAngle: arc.padAngle()(d) }; } // if context is not a transition just render the arc and update current if (!context.selection) { return context.attr('d', function (d) { this.current = getProperties(d); return arc(d); }); } // if context is a transition tween the 'd' attribute context.attrTween('d', function (d) { var _this = this; // omit data attribute incase of a pie chart with nested associations d = getProperties(d); this.current = this.current || d; var i = d3Interpolate.interpolate(this.current, d); return function (t) { _this.current = i(t); return arc(_this.current); }; }); } function numberize (x) { var def = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; if (isNaN(x) || x === null) return def; return x; } function tweenNumber (context) { var number = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : function (d) { return d; }; var format = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : function (d) { return d; }; number = functor(number); // if context is not a transition just render the text and update current if (!context.selection) { return context.text(function (d, i) { this.current = numberize(number.call(this, d, i)); return format(this.current); }); } context.tween('text', function (d, i) { var _this = this; var val = numberize(number.call(this, d, i)); this.current = numberize(this.current, val); var interp = d3Interpolate.interpolate(this.current, val); return function (t) { _this.textContent = format(_this.current = interp(t)); }; }); } function tweenCentroid (context, arc) { var rotate = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : function (d) { return d.rotate; }; rotate = functor(rotate); // get specific set of properties in case of recursive function getProperties(d) { return { innerRadius: arc.innerRadius()(d), outerRadius: arc.outerRadius()(d), startAngle: arc.startAngle()(d), endAngle: arc.endAngle()(d), rotate: rotate(d) || 0 }; } // if context is not a transition just render the centroid and update current if (!context.selection) { return context.attr('transform', function (d) { this.current = getProperties(d); return "translate(".concat(arc.centroid(this.current), ")") + "rotate(".concat(this.current.rotate, ")"); }); } context.attrTween('transform', function (d) { var _this = this; d = getProperties(d); this.current = this.current || d; var i = d3Interpolate.interpolate(this.current, d); return function (t) { _this.current = i(t); return "translate(".concat(arc.centroid(_this.current), ") ") + "rotate(".concat(_this.current.rotate || 0, ")"); }; }); } /** * d2b.modelBase() returns a d2b base model. * * model.interface() will return a base interface with various built in * getter/setter methods. * model.values() will return the values set through the interface. * @param {function} base - function that will act as the model interface * @param {object} $$ - attributes set by interactive with the base interface * @return {Object} model - object with properties and methods */ function base () { var _this = this; var _base = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var $$ = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; // Define an emitter updater that will fire events around the base updater // if (typeof base === 'function') { // base.emitter = function (context) { // if (context.dispatch) context.dispatch('beforeApply', {bubbles: true}); // base.apply(this, arguments); // if (context.dispatch) context.dispatch('applied', {bubbles: true}); // }; // } var propFn = function propFn(prop, cb) { return function (_) { if (!arguments.length) return $$[prop]; var old = $$[prop]; $$[prop] = _; if (cb) cb(_, old); return _base; }; }; var propFnGet = function propFnGet(prop) { return function () { return $$[prop]; }; }; var propFnFunctor = function propFnFunctor(prop, cb) { return function (_) { if (!arguments.length) return $$[prop]; var old = $$[prop]; $$[prop] = functor(_); if (cb) cb($$[prop], old); return _base; }; }; var scaleFnFunctor = function scaleFnFunctor(prop, cb) { return function (_) { if (!arguments.length) return $$[prop]; var old = $$[prop]; if (_ && _.domain) $$[prop] = function () { return _; };else $$[prop] = functor(_); if (cb) cb($$[prop], old); return _base; }; }; // Conditionally call a prop with arguments // If there are more than 2 arguments supplied, the prop will be called // with the arguments as long as the condition is truthy // Else if only 2 arguments suplied, the prop will be called with the // condition argument as long as the condition is defined _base.conditionally = function (prop, cond) { for (var _len = arguments.length, args = new Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) { args[_key - 2] = arguments[_key]; } if (args.length) { if (cond || cond === 0) _base[prop].apply(null, args); } else { if (cond !== undefined) _base[prop](cond); } return _base; }; var methodAdded = function methodAdded(method) { // Adds the conditionally modifier to a newly created base method. Similar to base.conditionally, // but is added to the method itself. e.g. `base.method.conditionally()` method.conditionally = function (cond) { for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) { args[_key2 - 1] = arguments[_key2]; } if (args.length) { if (cond || cond === 0) method.apply(null, args); } else { if (cond !== undefined) method(cond); } return _base; }; }; /* Base Model */ var model = { base: function base() { return _base; }, values: function values() { return $$; }, /** * model.removeProp removes the specified property * @param {Number} prop - property key * @return {Object} model - returns model to allow for method chaining */ removeProp: function removeProp(prop) { $$[prop] = null; _base[prop] = null; return model; }, /** * model.addProp allows new properties to be added to the model and base * interface. If the property is already defined an error will be raised. * @param {Number} prop - property key * @param {Number} value - default value to set * @param {Number} fn - function as new prop getter/setter * @param {Number} cb - callback function after prop is set * @return {Object} model - returns model to allow for method chaining */ addProp: function addProp(prop) { var value = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; var fn = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : propFn(prop); var cb = arguments.length > 3 ? arguments[3] : undefined; if ($$[prop] || _base[prop]) { // console.error(`${prop} property is already defined.`); return model; } // allow for null:default 'fn' in order to access callback fn = fn || propFn(prop, cb); fn(value); _base[prop] = fn; methodAdded(_base[prop]); return model; }, /** * model.addPropGet is similar to addProp except it doesn't allow for the * property to be reset through the API. * @param {Number} prop - property key * @param {Number} value - default value to set * @param {Number} fn - function as new prop getter * @return {Object} model - returns model to allow for method chaining */ addPropGet: function addPropGet(prop) { var value = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; var fn = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : propFnGet(prop); if ($$[prop] || _base[prop]) { // console.error(`${prop} property is already defined.`); return model; } $$[prop] = value; _base[prop] = fn; methodAdded(_base[prop]); return model; }, /** * model.addMethod allows new methods to be added to the model and base * interface. If the method is already defined an error will be raised. * @param {Number} method - method key * @param {Number} fn - new method * @return {Object} model - returns model to allow for method chaining */ addMethod: function addMethod(method, fn) { if (_base[method]) { // console.error(`${method} method is already defined.`); return model; } _base[method] = fn; methodAdded(_base[method]); return model; }, /** * model.addPropFunctor allows new functor properties to be added to the * model and base interface. If the property is already defined an error * will be raised. * @param {Number} prop - property key * @param {Number} value - default value to set * @param {Number} fn - function as new prop getter/setter * @return {Object} model - returns model to allow for method chaining */ addPropFunctor: function addPropFunctor(prop) { var value = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; var fn = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : propFnFunctor(prop); var cb = arguments.length > 3 ? arguments[3] : undefined; if ($$[prop] || _base[prop]) { // console.error(`${prop} property is already defined.`); return model; } // allow for null:default 'fn' in order to access callback fn = fn || propFnFunctor(prop, cb); fn(value); var newProp = _base[prop] = fn; methodAdded(_base[prop]); newProp.proxy = function (proxy) { var current = newProp(); var original = current.original || current; var proxied = function proxied() { for (var _len3 = arguments.length, args = new Array(_len3), _key3 = 0; _key3 < _len3; _key3++) { args[_key3] = arguments[_key3]; } var value = proxy.apply(this, args); return value === undefined ? original.apply(this, args) : value; }; proxied.original = original; newProp(proxied); return _base; }; return model; }, /** * model.addScaleFunctor allows new scale functor properties to be added * to the model and base interface. If the property is already defined * an error will be raised. * @param {Number} prop - property key * @param {Number} value - default value to set * @param {Number} fn - function as new prop getter/setter * @return {Object} model - returns model to allow for method chaining */ addScaleFunctor: function addScaleFunctor(prop) { var value = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; var fn = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : scaleFnFunctor(prop); var cb = arguments.length > 3 ? arguments[3] : undefined; return model.addProp(prop, value, fn, cb); }, /** * model.addDispatch allows dispatcher to be added to the model and base * interface. * @param {Number} prop - property key * @param {Number} store - store key * @param {Number} events - array of event keys * @return {Object} model - returns model to allow for method chaining */ addDispatcher: function addDispatcher(events) { var prop = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'on'; var store = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'dispatch'; if (_base[prop]) { // console.error(`${prop} property is already defined.`); return model; } if ($$[store]) { // console.error(`${store} value is already defined.`); return model; } _base[prop] = function (key, fn) { if (arguments.length === 0) return $$[store]; if (arguments.length === 1) return $$[store].on(key); $$[store].on(key, fn); return _base; }; methodAdded(_base[prop]); $$[store] = d3Dispatch.dispatch.apply(_this, events); return model; }, /** * model.addAdvancedConfig adds the base.advanced prop that will proxy through * some config function before rendering the base on the selection. * @param {Number} conf - property key * @return {Object} model - returns model to allow for method chaining */ addAdvancedConfig: function addAdvancedConfig(config) { _base.advanced = function (context) { var selection = context.selection ? context.selection() : context; var transition = selection !== context; selection.each(function (datum) { if (datum.updated !== undefined) d3Selection.select(this).on('chart-updated.advanced', datum.updated); var el = d3Selection.select(this).selectAll('.d2b-chart-advanced').data([config(_base, datum)]); var elEnter = el.enter().append('div').attr('class', 'd2b-chart-advanced'); el = elEnter.merge(el); var elTransition = transition ? el.transition(context) : el; elTransition.call(_base); }); return _base; }; return model; } }; return model; } var isTouchDevice = function isTouchDevice() { return 'ontouchstart' in window || window.navigator.msPointerEnabled; }; function tooltip () { var $$ = {}; var tooltip = function tooltip(context) { var selection = context.selection ? context.selection() : context; selection.on(event('mouseover'), mouseover).on(event('mouseout'), mouseout); if (isTouchDevice()) { selection.on(event('click'), mouseout); } else { selection.on(event('mousemove'), mousemove); } return tooltip; }; var getCoords = function getCoords(d, i) { var box = this.getBoundingClientRect(); var coords = {}; // construct at object, if null automatically set it based on cursor event position var at = ($$.at.call(this, d, i) || (d3Selection.event.clientX > window.innerWidth / 2 ? 'center left' : 'center right')).split(' '); at = { x: at[1], y: at[0] }; // switch for horizontal coordinate switch (at.x) { case 'left': coords.x = box.left; break; case 'center': coords.x = box.left + box.width / 2; break; default: // right coords.x = box.left + box.width; } // switch for vertical coordinate switch (at.y) { case 'bottom': coords.y = box.top + box.height; break; case 'center': coords.y = box.top + box.height / 2; break; default: // top coords.y = box.top; } return coords; }; var mouseover = function mouseover(d, i) { var html = $$.html.call(this, d, i); if (!html) return; var tooltipUpdate = $$.container.selectAll('.d2b-tooltip').data(function (d) { return [d]; }); var newTooltip = tooltipUpdate.enter().append('div').style('opacity', 0).attr('class', 'd2b-tooltip'); newTooltip.append('div').attr('class', 'd2b-tooltip-content'); tooltipUpdate = tooltipUpdate.merge(newTooltip); tooltipUpdate.transition().duration(100).style('opacity', 1); $$.dispatch.call('insert', tooltipUpdate, this, d, i); mousemove.call(this, d, i); }; var mousemove = function mousemove(d, i) { var html = $$.html.call(this, d, i), target = $$.target.call(this, d, i), color = $$.color.call(this, d, i), targetNode = target ? target.node() : this, coords = $$.followMouse.call(this, d, i) ? { x: d3Selection.event.clientX, y: d3Selection.event.clientY } : getCoords.call(targetNode, d, i); if (!html) return; // if (!$$.container.selectAll('.d2b-tooltip').size()) return mouseover(d, i); var tooltipUpdate = $$.container.selectAll('.d2b-tooltip').data(function (d) { return [d]; }); var my = $$.my.call(this, d, i) || (d3Selection.event.clientX > window.innerWidth / 2 ? 'left' : 'right'); tooltipUpdate.attr('class', "d2b-tooltip d2b-tooltip-".concat(my)).style('top', coords.y + 'px').style('left', coords.x + 'px').style('border-color', color).select('.d2b-tooltip-content').html(html); $$.dispatch.call('move', tooltipUpdate, this, d, i); }; var mouseout = function mouseout(d, i) { var tooltipUpdate = $$.container.selectAll('.d2b-tooltip').data(function (d) { return [d]; }); tooltipUpdate.transition().duration(100).style('opacity', 0).remove(); $$.dispatch.call('remove', tooltipUpdate, this, d, i); }; var event = function event(listener) { return "".concat(listener, ".d2b-tooltip"); }; /* Inherit from base model */ base(tooltip, $$).addProp('container', d3Selection.select('body')) //, null, updateContainer) .addMethod('clear', function (context) { (context.selection ? context.selection() : context).on(event('mouseover'), null).on(event('mouseout'), null).on(event('mousemove'), null); return tooltip; }).addPropFunctor('followMouse', false).addPropFunctor('color', null).addPropFunctor('my', null).addPropFunctor('at', null).addPropFunctor('target', null).addPropFunctor('html', null).addDispatcher(['insert', 'move', 'remove']); return tooltip; } // Work around for JavaScripts ||= operator. Only undefined will be construed as falsy. function oreqUndefined () { var truthy = function truthy(v) { return v !== undefined; }; for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } var val = args.filter(truthy)[0]; return truthy(val) ? val : undefined; } function tooltipAxis () { var $$ = {}; var tooltip = {}; // Position markers relative to selected points and axes var positionMarker = function positionMarker(marker, info, type) { if (type === 'y') { if (info.y === Infinity) return marker.style('opacity', 0); marker.style('opacity', 1).attr('transform', "translate(0, ".concat(info.y, ")")).attr('y1', 0).attr('y2', 0).attr('x1', 0).attr('x2', $$.size.width); } else { if (info.x === Infinity) return marker.style('opacity', 0); marker.style('opacity', 1).attr('transform', "translate(".concat(info.x, ", 0)")).attr('x1', 0).attr('x2', 0).attr('y1', 0).attr('y2', $$.size.height); } }; // Position tooltip relative to selected points and axes var positionTooltip = function positionTooltip(tooltip, info, base) { var node = tooltip.node(); if (!node) return; var tooltipBox = tooltip.node().getBoundingClientRect(); var x = base.x, y = base.y, pad = 10; if ($$.trackY) { if (info.y > $$.size.height / 2) { y += info.y - pad - tooltipBox.height; } else { y += info.y + pad; } } else { if (d3Selection.event.clientY - base.y > $$.size.height / 2) { y = d3Selection.event.clientY - pad - tooltipBox.height; } else { y = d3Selection.event.clientY + pad; } } if ($$.trackX) { if (info.x > $$.size.width / 2) { x += info.x - pad - tooltipBox.width; } else { x += info.x + pad; } } else { if (d3Selection.event.clientX - base.x > $$.size.width / 2) { x = d3Selection.event.clientX - pad - tooltipBox.width; } else { x = d3Selection.event.clientX + pad; } } x += window.pageXOffset; y += window.pageYOffset; tooltip.style('left', x + 'px').style('top', y + 'px'); }; // Populate tooltip with point rows var populateTooltip = function populateTooltip(tooltip, info) { var title = $$.title(info.points.map(function (d) { return d.data; })); tooltip.select('.d2b-tooltip-title').style('display', title ? 'block' : 'none').html(title); var content = tooltip.select('.d2b-tooltip-content'); var row = content.selectAll('.d2b-tooltip-row').data(info.points); var rowEnter = row.enter().append('div').attr('class', 'd2b-tooltip-row'); row.exit().remove(); row = row.merge(rowEnter).html(function (d) { return d.row; }).style('border-left-color', function (d) { return d.color || 'transparent'; }); }; // Finds the x, y coordinates associated with the points 'closest' to the cursor. // Also returns the set of points that meet the 'closest' configuration. var findPointInfo = function findPointInfo(base) { var cursor = { x: d3Selection.event.clientX - base.x, y: d3Selection.event.clientY - base.y }; var x = Infinity, y = Infinity, points = []; for (var groupName in groups) { if (!groups.hasOwnProperty(groupName)) continue; var group = groups[groupName]; var _loop = function _loop(graphName) { if (!group.hasOwnProperty(graphName)) return "continue"; var graph = group[graphName]; var newPoints = []; graph.config.data.forEach(function (d, i) { var item = { data: d, x: oreq(graph.config.x(d, i), $$.x(d, i)), y: oreq(graph.config.y(d, i), $$.y(d, i)), color: oreq(graph.config.color(d, i), $$.color(d, i)), row: oreqUndefined(graph.config.row(d, i), $$.row(d, i)) }; if ($$.trackX && $$.trackY) { if (item.x === x && item.y === y) return newPoints.push(item); var od = Math.sqrt(Math.pow(x - cursor.x, 2) + Math.pow(y - cursor.y, 2)); var nd = Math.sqrt(Math.pow(item.x - cursor.x, 2) + Math.pow(item.y - cursor.y, 2)); if (nd < od && nd < $$.threshold) { x = item.x; y = item.y; points = []; newPoints = [item]; } } else if ($$.trackX) { if (item.x === x) return newPoints.push(item); var _od = Math.abs(x - cursor.x); var _nd = Math.abs(item.x - cursor.x); if (_nd < _od && _nd < $$.threshold) { x = item.x; points = []; newPoints = [item]; } } else if ($$.trackY) { if (item.y === y) return newPoints.push(item); var _od2 = Math.abs(y - cursor.y); var _nd2 = Math.abs(item.y - cursor.y); if (_nd2 < _od2 && _nd2 < $$.threshold) { y = item.y; points = []; newPoints = [item]; } } }); points = points.concat(newPoints); }; for (var graphName in group) { var _ret = _loop(graphName); if (_ret === "continue") continue; } } points = points.sort(function (a, b) { return d3Array.ascending(a.x, b.x) || d3Array.ascending(a.y, b.y); }); return { x: x, y: y, points: points }; }; // Exit tooltip element. var exitElement = function exitElement(el) { el.transition().duration(50).style('opacity', 0).remove(); }; // Enter tooltip element. var enterElement = function enterElement(el) { el.transition().duration(50).style('opacity', 1); }; // Enter tooltip components. var enter = function enter(d, i) { var markerX = $$.svgContainer.selectAll('.d2b-tooltip-marker-x').data($$.trackX ? [tooltip] : []); var markerXEnter = markerX.enter().append('line').attr('class', 'd2b-tooltip-marker-x d2b-tooltip-marker'); var markerY = $$.svgContainer.selectAll('.d2b-tooltip-marker-y').data($$.trackY ? [tooltip] : []); var markerYEnter = markerY.enter().append('line').attr('class', 'd2b-tooltip-marker-y d2b-tooltip-marker'); var tooltipEl = $$.htmlContainer.selectAll('.d2b-tooltip-axis').data([tooltip]); var tooltipEnter = tooltipEl.enter().append('div').attr('class', 'd2b-tooltip d2b-tooltip-axis'); tooltipEnter.merge(tooltipEl).call(enterElement); markerY.merge(markerYEnter).call(enterElement); markerX.merge(markerXEnter).call(enterElement); tooltipEnter.append('div').attr('class', 'd2b-tooltip-title'); tooltipEnter.append('div').attr('class', 'd2b-tooltip-content'); $$.dispatch.call('insert', $$.tooltip, d, i); }; // Exit tooltip components. var exit = function exit() { $$.svgContainer.selectAll('.d2b-tooltip-marker-x').data([]).exit().call(exitElement); $$.svgContainer.selectAll('.d2b-tooltip-marker-y').data([]).exit().call(exitElement); $$.htmlContainer.selectAll('.d2b-tooltip-axis').data([]).exit().call(exitElement); }; // Tracker mousemove event. var mousemove = function mousemove(d, i) { var base = $$.svgContainer.selectAll('.d2b-tooltip-base').data([d]); base.enter().append('rect').attr('class', 'd2b-tooltip-base'); var baseBox = $$.svgContainer.node().getBoundingClientRect(); baseBox = { x: baseBox.left, y: baseBox.top }; var pointInfo = findPointInfo(baseBox); if (pointInfo.points.length) enter();else return exit(); $$.svgContainer.select('.d2b-tooltip-marker-x').call(positionMarker, pointInfo, 'x'); $$.svgContainer.select('.d2b-tooltip-marker-y').call(positionMarker, pointInfo, 'y'); $$.htmlContainer.select('.d2b-tooltip-axis').call(populateTooltip, pointInfo).call(positionTooltip, pointInfo, baseBox); $$.dispatch.call('move', $$.tooltip, d, i); }; // Tracker mouseout event. var mouseout = function mouseout(d, i) { exit(); $$.dispatch.call('remove', $$.tooltip, d, i); }; // Event key builder. var event = function event(listener) { return "".concat(listener, ".d2b-tooltip-axis"); }; // update mouse event tracker var updateTracker = function updateTracker(n, o) { if (o) { o.on(event('mouseout'), null).on(event('mousemove'), null); } if (n) { n.on(event('mouseout'), mouseout).on(event('mousemove'), mousemove); } }; // setup tooltip model base(tooltip, $$).addProp('htmlContainer', d3Selection.select('body')).addProp('svgContainer', null).addProp('tracker', d3Selection.select('body'), null, updateTracker).addProp('size', { height: 0, width: 0 }).addProp('trackX', true).addProp('trackY', false).addProp('threshold', Infinity).addMethod('clear', function (groupName, graphName) { if (arguments.length === 0) groups = {};else if (arguments.length === 1) delete groups[groupName];else if (arguments.length >= 2) delete groups[groupName][graphName]; return tooltip; }).addPropFunctor('title', null).addPropFunctor('x', function (d) { return d.x; }).addPropFunctor('y', function (d) { return d.y; }).addPropFunctor('color', null).addPropFunctor('row', null).addDispatcher(['move', 'insert', 'remove']); // construct an interface for each graph that is initialized var groups = {}; tooltip.graph = function () { var groupName = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; var graphName = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; var graphs = groups[groupName] = groups[groupName] || {}; var graph = graphs[graphName]; if (!graph) { graph = graphs[graphName] = { interface: {}, config: {} }; var graphModel = base(graph.interface, graph.config); graphModel.addProp('data', []).addMethod('clear', function () { graph.config.data = []; return graph.interface; }).addMethod('addPoint', function (p) { graph.config.data.push(p); return graph.interface; }).addPropFunctor('x', null).addPropFunctor('y', null).addPropFunctor('color', null).addPropFunctor('row', function () { return undefined; }); } return graph.interface; }; return tooltip; } // some datum and d3.stack configuration. function stack () { var $$ = {}; var stack = function stack(datum) { var _this = this; var original = datum; // for simplicity map datum to just array of values arrays datum = datum.map($$.values); // format values to be in the form // [ // {x_1: y_1, x_2: y_2, .. }, // {x_1: y_1, x_2: y_2, .. }, // .. // ] var xset = []; var vals = datum.map(function (d) { var nodes = {}; d.forEach(function (d, i) { var x = $$.x.call(_this, d, i); xset.push(x); nodes[x] = $$.y.call(_this, d, i); }); return nodes; }); // find unique set of x values xset = d3Collection.set(xset).values(); // value => index mapping of x values var xmap = xset.reduce(function (o, v, i) { o[v] = i; return o; }, {}); // graph keys (just use index) var keys = d3Array.range(0, datum.length); // transpose values for d3.stack var tvals = xset.map(function (col) { return vals.map(function (row) { return row[col] || 0; }); }); // stack transposed values $$.stack.keys(keys).value(function (d, k) { return d[k] || 0; }); var stacking = $$.stack(tvals); // reassociate the stacked values with the original datum datum.forEach(function (d, i) { d.forEach(function (val, ind) { var x = $$.x.call(_this, val, ind); var ys = stacking[i][xmap[x]]; $$.out.call(_this, val, ys[0], ys[1], x); }); }); return original; }; /* Inherit from base model */ base(stack, $$).addProp('stack', d3Shape.stack()).addPropFunctor('values', function (d) { return d; }).addPropFunctor('x', function (d) { return d.x; }).addPropFunctor('y', function (d) { return d.y; }).addProp('out', function (d, y0, y1) { d.y0 = y0; d.y1 = y1; }); return stack; } function breadcrumbs () { var $$ = {}; var breadcrumbs = function breadcrumbs(context) { var selection = context.selection ? context.selection() : context; var bcs = selection.selectAll('.d2b-breadcrumbs').data(function (d) { return [d]; }); var bcsEnter = bcs.enter().append('div').attr('class', 'd2b-breadcrumbs'); bcs = bcs.merge(bcsEnter).classed('d2b-vertical', $$.vertical); // Set breadcrumb data from values accessor. And only show breadcrumbs that have html content. var bc = bcs.selectAll('.d2b-breadcrumb').data(function (d) { return $$.values(d).filter($$.html); }, $$.key), bcExit = bc.exit(); var bcEnter = bc.enter().append('div').attr('class', 'd2b-breadcrumb').style('opacity', 0); bcEnter.append('div').attr('class', 'd2b-breadcrumb-icon'); bcEnter.append('div').attr('class', 'd2b-breadcrumb-content'); bc = bc.merge(bcEnter).order(); bc.select('.d2b-breadcrumb-content').html($$.html); if (context !== selection) { bc = bc.transition(context); bcExit = bcExit.transition(context).style('opacity', 0); } bc.style('border-color', $$.color).style('opacity', 1).select('.d2b-breadcrumb-icon').style('background-color', $$.color); bcExit.remove(); selection.dispatch('breadcrumbs-updated', { bubbles: true }); return breadcrumbs; }; /* Inherit from base model */ base(breadcrumbs, $$).addPropFunctor('values', function (d) { return d; }).addPropFunctor('key', function (d, i) { return i; }).addPropFunctor('color', 'blue').addPropFunctor('html', function (d) { return d.html; }).addPropFunctor('vertical', true); return breadcrumbs; } var color = d3Scales.scaleOrdinal(d3ScaleChromatic.schemeCategory10); function legend () { var $$ = {}; var symbol = d3Shape.symbol().size(80); var legend = function legend(context) { var selection = context.selection ? context.selection() : context; var leg = selection.selectAll('.d2b-legend').data(function (d) { return [d]; }); var legEnter = leg.enter().append('div').attr('class', 'd2b-legend'); leg = leg.merge(legEnter).classed('d2b-vertical', $$.vertical); var legItem = leg.selectAll('.d2b-legend-item').data($$.values, $$.key), legItemExit = legItem.exit(); var legItemEnter = legItem.enter().append('div').attr('class', 'd2b-legend-item').style('opacity', 0); legItemEnter.append('div').attr('class', 'd2b-legend-icon'); legItemEnter.append('div').attr('class', 'd2b-legend-content'); legItem = legItem.merge(legItemEnter).order(); legItem.select('.d2b-legend-content').html($$.html); if (context !== selection) { legItem = legItem.transition(context); legItemExit = legItemExit.transition(context).style('opacity', 0); } legItem.style('opacity', 1).each(function (d, i) { // legend item customization var item = d3Selection.select(this); var color = $$.color(d, i), empty = $$.empty(d, i); // legend icon customization var icon = $$.icon(d, i), iconDiv = item.select('.d2b-legend-icon'), fa = [], shape = [], rect = iconDiv.node().getBoundingClientRect(), size = { width: rect.width, height: rect.