UNPKG

dc

Version:

A multi-dimensional charting library built to work natively with crossfilter and rendered using d3.js

1,473 lines (1,328 loc) 487 kB
/*! * dc 4.2.7 * http://dc-js.github.io/dc.js/ * Copyright 2012-2021 Nick Zhu & the dc.js Developers * https://github.com/dc-js/dc.js/blob/master/AUTHORS * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('d3'), require('d3'), require('d3'), require('d3'), require('d3'), require('d3'), require('d3'), require('d3'), require('d3'), require('d3'), require('d3'), require('d3'), require('d3'), require('d3'), require('d3'), require('d3'), require('d3'), require('d3'), require('d3')) : typeof define === 'function' && define.amd ? define(['exports', 'd3', 'd3', 'd3', 'd3', 'd3', 'd3', 'd3', 'd3', 'd3', 'd3', 'd3', 'd3', 'd3', 'd3', 'd3', 'd3', 'd3', 'd3', 'd3'], factory) : (global = global || self, factory(global.dc = {}, global.d3, global.d3, global.d3, global.d3, global.d3, global.d3, global.d3, global.d3, global.d3, global.d3, global.d3, global.d3, global.d3, global.d3, global.d3, global.d3, global.d3, global.d3, global.d3)); }(this, (function (exports, d3TimeFormat, d3Time, d3Format, d3Selection, d3Dispatch, d3Array, d3Scale, d3Interpolate, d3ScaleChromatic, d3Axis, d3Zoom, d3Brush, d3Timer, d3Shape, d3Geo, d3Ease, d3Hierarchy, d3, d3Collection) { 'use strict'; const version = "4.2.7"; class BadArgumentException extends Error { } const constants = { CHART_CLASS: 'dc-chart', DEBUG_GROUP_CLASS: 'debug', STACK_CLASS: 'stack', DESELECTED_CLASS: 'deselected', SELECTED_CLASS: 'selected', NODE_INDEX_NAME: '__index__', GROUP_INDEX_NAME: '__group_index__', DEFAULT_CHART_GROUP: '__default_chart_group__', EVENT_DELAY: 40, NEGLIGIBLE_NUMBER: 1e-10 }; /** * Provides basis logging and deprecation utilities */ class Logger { constructor () { /** * Enable debug level logging. Set to `false` by default. * @name enableDebugLog * @memberof Logger * @instance */ this.enableDebugLog = false; this._alreadyWarned = {}; } /** * Put a warning message to console * @example * logger.warn('Invalid use of .tension on CurveLinear'); * @param {String} [msg] * @returns {Logger} */ warn (msg) { if (console) { if (console.warn) { console.warn(msg); } else if (console.log) { console.log(msg); } } return this; } /** * Put a warning message to console. It will warn only on unique messages. * @example * logger.warnOnce('Invalid use of .tension on CurveLinear'); * @param {String} [msg] * @returns {Logger} */ warnOnce (msg) { if (!this._alreadyWarned[msg]) { this._alreadyWarned[msg] = true; logger.warn(msg); } return this; } /** * Put a debug message to console. It is controlled by `logger.enableDebugLog` * @example * logger.debug('Total number of slices: ' + numSlices); * @param {String} [msg] * @returns {Logger} */ debug (msg) { if (this.enableDebugLog && console) { if (console.debug) { console.debug(msg); } else if (console.log) { console.log(msg); } } return this; } } const logger = new Logger(); /** * General configuration */ class Config { constructor () { this._defaultColors = Config._schemeCategory20c; /** * The default date format for dc.js * @type {Function} * @default d3.timeFormat('%m/%d/%Y') */ this.dateFormat = d3TimeFormat.timeFormat('%m/%d/%Y'); this._renderlet = null; /** * If this boolean is set truthy, all transitions will be disabled, and changes to the charts will happen * immediately. * @type {Boolean} * @default false */ this.disableTransitions = false; } /** * Set the default color scheme for ordinal charts. Changing it will impact all ordinal charts. * * By default it is set to a copy of * `d3.schemeCategory20c` for backward compatibility. This color scheme has been * [removed from D3v5](https://github.com/d3/d3/blob/master/CHANGES.md#changes-in-d3-50). * In DC 3.1 release it will change to a more appropriate default. * * @example * config.defaultColors(d3.schemeSet1) * @param {Array} [colors] * @returns {Array|config} */ defaultColors (colors) { if (!arguments.length) { // Issue warning if it uses _schemeCategory20c if (this._defaultColors === Config._schemeCategory20c) { logger.warnOnce('You are using d3.schemeCategory20c, which has been removed in D3v5. ' + 'See the explanation at https://github.com/d3/d3/blob/master/CHANGES.md#changes-in-d3-50. ' + 'DC is using it for backward compatibility, however it will be changed in DCv3.1. ' + 'You can change it by calling dc.config.defaultColors(newScheme). ' + 'See https://github.com/d3/d3-scale-chromatic for some alternatives.'); } return this._defaultColors; } this._defaultColors = colors; return this; } } // D3v5 has removed schemeCategory20c, copied here for backward compatibility Config._schemeCategory20c = [ '#3182bd', '#6baed6', '#9ecae1', '#c6dbef', '#e6550d', '#fd8d3c', '#fdae6b', '#fdd0a2', '#31a354', '#74c476', '#a1d99b', '#c7e9c0', '#756bb1', '#9e9ac8', '#bcbddc', '#dadaeb', '#636363', '#969696', '#bdbdbd', '#d9d9d9']; /** * General configuration object; see {@link Config} for members. */ const config = new Config(); /** * d3.js compatiblity layer */ const d3compat = { eventHandler: handler => function eventHandler (a, b) { console.warn('No d3.js compatbility event handler registered, defaulting to v6 behavior.'); handler.call(this, b, a); }, nester: ({key, sortKeys, sortValues, entries}) => { throw new Error('No d3.js compatbility nester registered, load v5 or v6 compability layer.'); }, pointer: () => { throw new Error('No d3.js compatbility pointer registered, load v5 or v6 compability layer.'); } }; /** * The ChartRegistry maintains sets of all instantiated dc.js charts under named groups * and the default group. There is a single global ChartRegistry object named `chartRegistry` * * A chart group often corresponds to a crossfilter instance. It specifies * the set of charts which should be updated when a filter changes on one of the charts or when the * global functions {@link filterAll filterAll}, {@link refocusAll refocusAll}, * {@link renderAll renderAll}, {@link redrawAll redrawAll}, or chart functions * {@link baseMixin#renderGroup baseMixin.renderGroup}, * {@link baseMixin#redrawGroup baseMixin.redrawGroup} are called. */ class ChartRegistry { constructor () { // chartGroup:string => charts:array this._chartMap = {}; } _initializeChartGroup (group) { if (!group) { group = constants.DEFAULT_CHART_GROUP; } if (!(this._chartMap)[group]) { (this._chartMap)[group] = []; } return group; } /** * Determine if a given chart instance resides in any group in the registry. * @param {Object} chart dc.js chart instance * @returns {Boolean} */ has (chart) { for (const e in this._chartMap) { if ((this._chartMap)[e].indexOf(chart) >= 0) { return true; } } return false; } /** * Add given chart instance to the given group, creating the group if necessary. * If no group is provided, the default group `constants.DEFAULT_CHART_GROUP` will be used. * @param {Object} chart dc.js chart instance * @param {String} [group] Group name * @return {undefined} */ register (chart, group) { const _chartMap = this._chartMap; group = this._initializeChartGroup(group); _chartMap[group].push(chart); } /** * Remove given chart instance from the given group, creating the group if necessary. * If no group is provided, the default group `constants.DEFAULT_CHART_GROUP` will be used. * @param {Object} chart dc.js chart instance * @param {String} [group] Group name * @return {undefined} */ deregister (chart, group) { group = this._initializeChartGroup(group); for (let i = 0; i < (this._chartMap)[group].length; i++) { if ((this._chartMap)[group][i].anchorName() === chart.anchorName()) { (this._chartMap)[group].splice(i, 1); break; } } } /** * Clear given group if one is provided, otherwise clears all groups. * @param {String} group Group name * @return {undefined} */ clear (group) { if (group) { delete (this._chartMap)[group]; } else { this._chartMap = {}; } } /** * Get an array of each chart instance in the given group. * If no group is provided, the charts in the default group are returned. * @param {String} [group] Group name * @returns {Array<Object>} */ list (group) { group = this._initializeChartGroup(group); return (this._chartMap)[group]; } } /** * The chartRegistry object maintains sets of all instantiated dc.js charts under named groups * and the default group. See {@link ChartRegistry ChartRegistry} for its methods. */ const chartRegistry = new ChartRegistry(); /** * Add given chart instance to the given group, creating the group if necessary. * If no group is provided, the default group `constants.DEFAULT_CHART_GROUP` will be used. * @function registerChart * @param {Object} chart dc.js chart instance * @param {String} [group] Group name * @return {undefined} */ const registerChart = function (chart, group) { chartRegistry.register(chart, group); }; /** * Remove given chart instance from the given group, creating the group if necessary. * If no group is provided, the default group `constants.DEFAULT_CHART_GROUP` will be used. * @function deregisterChart * @param {Object} chart dc.js chart instance * @param {String} [group] Group name * @return {undefined} */ const deregisterChart = function (chart, group) { chartRegistry.deregister(chart, group); }; /** * Determine if a given chart instance resides in any group in the registry. * @function hasChart * @param {Object} chart dc.js chart instance * @returns {Boolean} */ const hasChart = function (chart) { return chartRegistry.has(chart); }; /** * Clear given group if one is provided, otherwise clears all groups. * @function deregisterAllCharts * @param {String} group Group name * @return {undefined} */ const deregisterAllCharts = function (group) { chartRegistry.clear(group); }; /** * Clear all filters on all charts within the given chart group. If the chart group is not given then * only charts that belong to the default chart group will be reset. * @function filterAll * @param {String} [group] * @return {undefined} */ const filterAll = function (group) { const charts = chartRegistry.list(group); for (let i = 0; i < charts.length; ++i) { charts[i].filterAll(); } }; /** * Reset zoom level / focus on all charts that belong to the given chart group. If the chart group is * not given then only charts that belong to the default chart group will be reset. * @function refocusAll * @param {String} [group] * @return {undefined} */ const refocusAll = function (group) { const charts = chartRegistry.list(group); for (let i = 0; i < charts.length; ++i) { if (charts[i].focus) { charts[i].focus(); } } }; /** * Re-render all charts belong to the given chart group. If the chart group is not given then only * charts that belong to the default chart group will be re-rendered. * @function renderAll * @param {String} [group] * @return {undefined} */ const renderAll = function (group) { const charts = chartRegistry.list(group); for (let i = 0; i < charts.length; ++i) { charts[i].render(); } if (config._renderlet !== null) { config._renderlet(group); } }; /** * Redraw all charts belong to the given chart group. If the chart group is not given then only charts * that belong to the default chart group will be re-drawn. Redraw is different from re-render since * when redrawing dc tries to update the graphic incrementally, using transitions, instead of starting * from scratch. * @function redrawAll * @param {String} [group] * @return {undefined} */ const redrawAll = function (group) { const charts = chartRegistry.list(group); for (let i = 0; i < charts.length; ++i) { charts[i].redraw(); } if (config._renderlet !== null) { config._renderlet(group); } }; /** * Start a transition on a selection if transitions are globally enabled * ({@link disableTransitions} is false) and the duration is greater than zero; otherwise return * the selection. Since most operations are the same on a d3 selection and a d3 transition, this * allows a common code path for both cases. * @function transition * @param {d3.selection} selection - the selection to be transitioned * @param {Number|Function} [duration=250] - the duration of the transition in milliseconds, a * function returning the duration, or 0 for no transition * @param {Number|Function} [delay] - the delay of the transition in milliseconds, or a function * returning the delay, or 0 for no delay * @param {String} [name] - the name of the transition (if concurrent transitions on the same * elements are needed) * @returns {d3.transition|d3.selection} */ const transition = function (selection, duration, delay, name) { if (config.disableTransitions || duration <= 0) { return selection; } let s = selection.transition(name); if (duration >= 0 || duration !== undefined) { s = s.duration(duration); } if (delay >= 0 || delay !== undefined) { s = s.delay(delay); } return s; }; /* somewhat silly, but to avoid duplicating logic */ const optionalTransition = function (enable, duration, delay, name) { if (enable) { return function (selection) { return transition(selection, duration, delay, name); }; } else { return function (selection) { return selection; }; } }; // See http://stackoverflow.com/a/20773846 const afterTransition = function (_transition, callback) { if (_transition.empty() || !_transition.duration) { callback.call(_transition); } else { let n = 0; _transition .each(() => { ++n; }) .on('end', () => { if (!--n) { callback.call(_transition); } }); } }; const renderlet = function (_) { if (!arguments.length) { return config._renderlet; } config._renderlet = _; return null; }; const instanceOfChart = function (o) { return o instanceof Object && o.__dcFlag__ && true; }; const events = { current: null }; /** * This function triggers a throttled event function with a specified delay (in milli-seconds). Events * that are triggered repetitively due to user interaction such brush dragging might flood the library * and invoke more renders than can be executed in time. Using this function to wrap your event * function allows the library to smooth out the rendering by throttling events and only responding to * the most recent event. * @name events.trigger * @example * chart.on('renderlet', function(chart) { * // smooth the rendering through event throttling * events.trigger(function(){ * // focus some other chart to the range selected by user on this chart * someOtherChart.focus(chart.filter()); * }); * }) * @param {Function} closure * @param {Number} [delay] * @return {undefined} */ events.trigger = function (closure, delay) { if (!delay) { closure(); return; } events.current = closure; setTimeout(() => { if (closure === events.current) { closure(); } }, delay); }; /** * The dc.js filters are functions which are passed into crossfilter to chose which records will be * accumulated to produce values for the charts. In the crossfilter model, any filters applied on one * dimension will affect all the other dimensions but not that one. dc always applies a filter * function to the dimension; the function combines multiple filters and if any of them accept a * record, it is filtered in. * * These filter constructors are used as appropriate by the various charts to implement brushing. We * mention below which chart uses which filter. In some cases, many instances of a filter will be added. * * Each of the dc.js filters is an object with the following properties: * * `isFiltered` - a function that returns true if a value is within the filter * * `filterType` - a string identifying the filter, here the name of the constructor * * Currently these filter objects are also arrays, but this is not a requirement. Custom filters * can be used as long as they have the properties above. * @namespace filters * @type {{}} */ const filters = {}; /** * RangedFilter is a filter which accepts keys between `low` and `high`. It is used to implement X * axis brushing for the {@link CoordinateGridMixin coordinate grid charts}. * * Its `filterType` is 'RangedFilter' * @name RangedFilter * @memberof filters * @param {Number} low * @param {Number} high * @returns {Array<Number>} * @constructor */ filters.RangedFilter = function (low, high) { const range = new Array(low, high); range.isFiltered = function (value) { return value >= this[0] && value < this[1]; }; range.filterType = 'RangedFilter'; return range; }; /** * TwoDimensionalFilter is a filter which accepts a single two-dimensional value. It is used by the * {@link HeatMap heat map chart} to include particular cells as they are clicked. (Rows and columns are * filtered by filtering all the cells in the row or column.) * * Its `filterType` is 'TwoDimensionalFilter' * @name TwoDimensionalFilter * @memberof filters * @param {Array<Number>} filter * @returns {Array<Number>} * @constructor */ filters.TwoDimensionalFilter = function (filter) { if (filter === null) { return null; } const f = filter; f.isFiltered = function (value) { return value.length && value.length === f.length && value[0] === f[0] && value[1] === f[1]; }; f.filterType = 'TwoDimensionalFilter'; return f; }; /** * The RangedTwoDimensionalFilter allows filtering all values which fit within a rectangular * region. It is used by the {@link ScatterPlot scatter plot} to implement rectangular brushing. * * It takes two two-dimensional points in the form `[[x1,y1],[x2,y2]]`, and normalizes them so that * `x1 <= x2` and `y1 <= y2`. It then returns a filter which accepts any points which are in the * rectangular range including the lower values but excluding the higher values. * * If an array of two values are given to the RangedTwoDimensionalFilter, it interprets the values as * two x coordinates `x1` and `x2` and returns a filter which accepts any points for which `x1 <= x < * x2`. * * Its `filterType` is 'RangedTwoDimensionalFilter' * @name RangedTwoDimensionalFilter * @memberof filters * @param {Array<Array<Number>>} filter * @returns {Array<Array<Number>>} * @constructor */ filters.RangedTwoDimensionalFilter = function (filter) { if (filter === null) { return null; } const f = filter; let fromBottomLeft; if (f[0] instanceof Array) { fromBottomLeft = [ [Math.min(filter[0][0], filter[1][0]), Math.min(filter[0][1], filter[1][1])], [Math.max(filter[0][0], filter[1][0]), Math.max(filter[0][1], filter[1][1])] ]; } else { fromBottomLeft = [[filter[0], -Infinity], [filter[1], Infinity]]; } f.isFiltered = function (value) { let x, y; if (value instanceof Array) { x = value[0]; y = value[1]; } else { x = value; y = fromBottomLeft[0][1]; } return x >= fromBottomLeft[0][0] && x < fromBottomLeft[1][0] && y >= fromBottomLeft[0][1] && y < fromBottomLeft[1][1]; }; f.filterType = 'RangedTwoDimensionalFilter'; return f; }; // ******** Sunburst Chart ******** /** * HierarchyFilter is a filter which accepts a key path as an array. It matches any node at, or * child of, the given path. It is used by the {@link SunburstChart sunburst chart} to include particular cells and all * their children as they are clicked. * * @name HierarchyFilter * @memberof filters * @param {String} path * @returns {Array<String>} * @constructor */ filters.HierarchyFilter = function (path) { if (path === null) { return null; } const filter = path.slice(0); filter.isFiltered = function (value) { if (!(filter.length && value && value.length && value.length >= filter.length)) { return false; } for (let i = 0; i < filter.length; i++) { if (value[i] !== filter[i]) { return false; } } return true; }; return filter; }; class InvalidStateException extends Error { } /** * Returns a function that given a string property name, can be used to pluck the property off an object. A function * can be passed as the second argument to also alter the data being returned. * * This can be a useful shorthand method to create accessor functions. * @example * var xPluck = pluck('x'); * var objA = {x: 1}; * xPluck(objA) // 1 * @example * var xPosition = pluck('x', function (x, i) { * // `this` is the original datum, * // `x` is the x property of the datum, * // `i` is the position in the array * return this.radius + x; * }); * selectAll('.circle').data(...).x(xPosition); * @function pluck * @param {String} n * @param {Function} [f] * @returns {Function} */ const pluck = function (n, f) { if (!f) { return function (d) { return d[n]; }; } return function (d, i) { return f.call(d, d[n], i); }; }; /** * @namespace utils * @type {{}} */ const utils = {}; /** * Print a single value filter. * @method printSingleValue * @memberof utils * @param {any} filter * @returns {String} */ utils.printSingleValue = function (filter) { let s = `${filter}`; if (filter instanceof Date) { s = config.dateFormat(filter); } else if (typeof (filter) === 'string') { s = filter; } else if (utils.isFloat(filter)) { s = utils.printSingleValue.fformat(filter); } else if (utils.isInteger(filter)) { s = Math.round(filter); } return s; }; utils.printSingleValue.fformat = d3Format.format('.2f'); // convert 'day' to d3.timeDay and similar utils._toTimeFunc = function (t) { const mappings = { 'second': d3Time.timeSecond, 'minute': d3Time.timeMinute, 'hour': d3Time.timeHour, 'day': d3Time.timeDay, 'week': d3Time.timeWeek, 'month': d3Time.timeMonth, 'year': d3Time.timeYear }; return mappings[t]; }; /** * Arbitrary add one value to another. * * If the value l is of type Date, adds r units to it. t becomes the unit. * For example utils.add(dt, 3, 'week') will add 3 (r = 3) weeks (t= 'week') to dt. * * If l is of type numeric, t is ignored. In this case if r is of type string, * it is assumed to be percentage (whether or not it includes %). For example * utils.add(30, 10) will give 40 and utils.add(30, '10') will give 33. * * They also generate strange results if l is a string. * @method add * @memberof utils * @param {Date|Number} l the value to modify * @param {String|Number} r the amount by which to modify the value * @param {Function|String} [t=d3.timeDay] if `l` is a `Date`, then this should be a * [d3 time interval](https://github.com/d3/d3-time/blob/master/README.md#_interval). * For backward compatibility with dc.js 2.0, it can also be the name of an interval, i.e. * 'millis', 'second', 'minute', 'hour', 'day', 'week', 'month', or 'year' * @returns {Date|Number} */ utils.add = function (l, r, t) { if (typeof r === 'string') { r = r.replace('%', ''); } if (l instanceof Date) { if (typeof r === 'string') { r = +r; } if (t === 'millis') { return new Date(l.getTime() + r); } t = t || d3Time.timeDay; if (typeof t !== 'function') { t = utils._toTimeFunc(t); } return t.offset(l, r); } else if (typeof r === 'string') { const percentage = (+r / 100); return l > 0 ? l * (1 + percentage) : l * (1 - percentage); } else { return l + r; } }; /** * Arbitrary subtract one value from another. * * If the value l is of type Date, subtracts r units from it. t becomes the unit. * For example utils.subtract(dt, 3, 'week') will subtract 3 (r = 3) weeks (t= 'week') from dt. * * If l is of type numeric, t is ignored. In this case if r is of type string, * it is assumed to be percentage (whether or not it includes %). For example * utils.subtract(30, 10) will give 20 and utils.subtract(30, '10') will give 27. * * They also generate strange results if l is a string. * @method subtract * @memberof utils * @param {Date|Number} l the value to modify * @param {String|Number} r the amount by which to modify the value * @param {Function|String} [t=d3.timeDay] if `l` is a `Date`, then this should be a * [d3 time interval](https://github.com/d3/d3-time/blob/master/README.md#_interval). * For backward compatibility with dc.js 2.0, it can also be the name of an interval, i.e. * 'millis', 'second', 'minute', 'hour', 'day', 'week', 'month', or 'year' * @returns {Date|Number} */ utils.subtract = function (l, r, t) { if (typeof r === 'string') { r = r.replace('%', ''); } if (l instanceof Date) { if (typeof r === 'string') { r = +r; } if (t === 'millis') { return new Date(l.getTime() - r); } t = t || d3Time.timeDay; if (typeof t !== 'function') { t = utils._toTimeFunc(t); } return t.offset(l, -r); } else if (typeof r === 'string') { const percentage = (+r / 100); return l < 0 ? l * (1 + percentage) : l * (1 - percentage); } else { return l - r; } }; /** * Is the value a number? * @method isNumber * @memberof utils * @param {any} n * @returns {Boolean} */ utils.isNumber = function (n) { return n === +n; }; /** * Is the value a float? * @method isFloat * @memberof utils * @param {any} n * @returns {Boolean} */ utils.isFloat = function (n) { return n === +n && n !== (n | 0); }; /** * Is the value an integer? * @method isInteger * @memberof utils * @param {any} n * @returns {Boolean} */ utils.isInteger = function (n) { return n === +n && n === (n | 0); }; /** * Is the value very close to zero? * @method isNegligible * @memberof utils * @param {any} n * @returns {Boolean} */ utils.isNegligible = function (n) { return !utils.isNumber(n) || (n < constants.NEGLIGIBLE_NUMBER && n > -constants.NEGLIGIBLE_NUMBER); }; /** * Ensure the value is no greater or less than the min/max values. If it is return the boundary value. * @method clamp * @memberof utils * @param {any} val * @param {any} min * @param {any} max * @returns {any} */ utils.clamp = function (val, min, max) { return val < min ? min : (val > max ? max : val); }; /** * Given `x`, return a function that always returns `x`. * * {@link https://github.com/d3/d3/blob/master/CHANGES.md#internals `d3.functor` was removed in d3 version 4}. * This function helps to implement the replacement, * `typeof x === "function" ? x : utils.constant(x)` * @method constant * @memberof utils * @param {any} x * @returns {Function} */ utils.constant = function (x) { return function () { return x; }; }; /** * Using a simple static counter, provide a unique integer id. * @method uniqueId * @memberof utils * @returns {Number} */ let _idCounter = 0; utils.uniqueId = function () { return ++_idCounter; }; /** * Convert a name to an ID. * @method nameToId * @memberof utils * @param {String} name * @returns {String} */ utils.nameToId = function (name) { return name.toLowerCase().replace(/[\s]/g, '_').replace(/[\.']/g, ''); }; /** * Append or select an item on a parent element. * @method appendOrSelect * @memberof utils * @param {d3.selection} parent * @param {String} selector * @param {String} tag * @returns {d3.selection} */ utils.appendOrSelect = function (parent, selector, tag) { tag = tag || selector; let element = parent.select(selector); if (element.empty()) { element = parent.append(tag); } return element; }; /** * Return the number if the value is a number; else 0. * @method safeNumber * @memberof utils * @param {Number|any} n * @returns {Number} */ utils.safeNumber = function (n) { return utils.isNumber(+n) ? +n : 0;}; /** * Return true if both arrays are equal, if both array are null these are considered equal * @method arraysEqual * @memberof utils * @param {Array|null} a1 * @param {Array|null} a2 * @returns {Boolean} */ utils.arraysEqual = function (a1, a2) { if (!a1 && !a2) { return true; } if (!a1 || !a2) { return false; } return a1.length === a2.length && // If elements are not integers/strings, we hope that it will match because of toString // Test cases cover dates as well. a1.every((elem, i) => elem.valueOf() === a2[i].valueOf()); }; // ******** Sunburst Chart ******** utils.allChildren = function (node) { let paths = []; paths.push(node.path); console.log('currentNode', node); if (node.children) { for (let i = 0; i < node.children.length; i++) { paths = paths.concat(utils.allChildren(node.children[i])); } } return paths; }; // builds a d3 Hierarchy from a collection // TODO: turn this monster method something better. utils.toHierarchy = function (list, accessor) { const root = {'key': 'root', 'children': []}; for (let i = 0; i < list.length; i++) { const data = list[i]; const parts = data.key; const value = accessor(data); let currentNode = root; for (let j = 0; j < parts.length; j++) { const currentPath = parts.slice(0, j + 1); const children = currentNode.children; const nodeName = parts[j]; let childNode; if (j + 1 < parts.length) { // Not yet at the end of the sequence; move down the tree. childNode = findChild(children, nodeName); // If we don't already have a child node for this branch, create it. if (childNode === void 0) { childNode = {'key': nodeName, 'children': [], 'path': currentPath}; children.push(childNode); } currentNode = childNode; } else { // Reached the end of the sequence; create a leaf node. childNode = {'key': nodeName, 'value': value, 'data': data, 'path': currentPath}; children.push(childNode); } } } return root; }; function findChild (children, nodeName) { for (let k = 0; k < children.length; k++) { if (children[k].key === nodeName) { return children[k]; } } } utils.getAncestors = function (node) { const path = []; let current = node; while (current.parent) { path.unshift(current.name); current = current.parent; } return path; }; utils.arraysIdentical = function (a, b) { let i = a.length; if (i !== b.length) { return false; } while (i--) { if (a[i] !== b[i]) { return false; } } return true; }; /** * @namespace printers * @type {{}} */ const printers = {}; /** * Converts a list of filters into a readable string. * @method filters * @memberof printers * @param {Array<filters>} filters * @returns {String} */ printers.filters = function (filters) { let s = ''; for (let i = 0; i < filters.length; ++i) { if (i > 0) { s += ', '; } s += printers.filter(filters[i]); } return s; }; /** * Converts a filter into a readable string. * @method filter * @memberof printers * @param {filters|any|Array<any>} filter * @returns {String} */ printers.filter = function (filter) { let s = ''; if (typeof filter !== 'undefined' && filter !== null) { if (filter instanceof Array) { if (filter.length >= 2) { s = `[${filter.map(e => utils.printSingleValue(e)).join(' -> ')}]`; } else if (filter.length >= 1) { s = utils.printSingleValue(filter[0]); } } else { s = utils.printSingleValue(filter); } } return s; }; /** * @namespace units * @type {{}} */ const units = {}; /** * The default value for {@link CoordinateGridMixin#xUnits .xUnits} for the * {@link CoordinateGridMixin Coordinate Grid Chart} and should * be used when the x values are a sequence of integers. * It is a function that counts the number of integers in the range supplied in its start and end parameters. * @method integers * @memberof units * @see {@link CoordinateGridMixin#xUnits coordinateGridMixin.xUnits} * @example * chart.xUnits(units.integers) // already the default * @param {Number} start * @param {Number} end * @returns {Number} */ units.integers = function (start, end) { return Math.abs(end - start); }; /** * This argument can be passed to the {@link CoordinateGridMixin#xUnits .xUnits} function of a * coordinate grid chart to specify ordinal units for the x axis. Usually this parameter is used in * combination with passing * {@link https://github.com/d3/d3-scale/blob/master/README.md#ordinal-scales d3.scaleOrdinal} * to {@link CoordinateGridMixin#x .x}. * * As of dc.js 3.0, this is purely a placeholder or magic value which causes the chart to go into ordinal mode; the * function is not called. * @method ordinal * @memberof units * @return {uncallable} * @see {@link https://github.com/d3/d3-scale/blob/master/README.md#ordinal-scales d3.scaleOrdinal} * @see {@link CoordinateGridMixin#xUnits coordinateGridMixin.xUnits} * @see {@link CoordinateGridMixin#x coordinateGridMixin.x} * @example * chart.xUnits(units.ordinal) * .x(d3.scaleOrdinal()) */ units.ordinal = function () { throw new Error('dc.units.ordinal should not be called - it is a placeholder'); }; /** * @namespace fp * @memberof units * @type {{}} */ units.fp = {}; /** * This function generates an argument for the {@link CoordinateGridMixin Coordinate Grid Chart} * {@link CoordinateGridMixin#xUnits .xUnits} function specifying that the x values are floating-point * numbers with the given precision. * The returned function determines how many values at the given precision will fit into the range * supplied in its start and end parameters. * @method precision * @memberof units.fp * @see {@link CoordinateGridMixin#xUnits coordinateGridMixin.xUnits} * @example * // specify values (and ticks) every 0.1 units * chart.xUnits(units.fp.precision(0.1) * // there are 500 units between 0.5 and 1 if the precision is 0.001 * var thousandths = units.fp.precision(0.001); * thousandths(0.5, 1.0) // returns 500 * @param {Number} precision * @returns {Function} start-end unit function */ units.fp.precision = function (precision) { const _f = function (s, e) { const d = Math.abs((e - s) / _f.resolution); if (utils.isNegligible(d - Math.floor(d))) { return Math.floor(d); } else { return Math.ceil(d); } }; _f.resolution = precision; return _f; }; const _defaultFilterHandler = (dimension, filters) => { if (filters.length === 0) { dimension.filter(null); } else if (filters.length === 1 && !filters[0].isFiltered) { // single value and not a function-based filter dimension.filterExact(filters[0]); } else if (filters.length === 1 && filters[0].filterType === 'RangedFilter') { // single range-based filter dimension.filterRange(filters[0]); } else { dimension.filterFunction(d => { for (let i = 0; i < filters.length; i++) { const filter = filters[i]; if (filter.isFiltered) { if(filter.isFiltered(d)) { return true; } } else if (filter <= d && filter >= d) { return true; } } return false; }); } return filters; }; const _defaultHasFilterHandler = (filters, filter) => { if (filter === null || typeof (filter) === 'undefined') { return filters.length > 0; } return filters.some(f => filter <= f && filter >= f); }; const _defaultRemoveFilterHandler = (filters, filter) => { for (let i = 0; i < filters.length; i++) { if (filters[i] <= filter && filters[i] >= filter) { filters.splice(i, 1); break; } } return filters; }; const _defaultAddFilterHandler = (filters, filter) => { filters.push(filter); return filters; }; const _defaultResetFilterHandler = filters => []; /** * `BaseMixin` is an abstract functional object representing a basic `dc` chart object * for all chart and widget implementations. Methods from the {@link #BaseMixin BaseMixin} are inherited * and available on all chart implementations in the `dc` library. * @mixin BaseMixin */ class BaseMixin { constructor () { this.__dcFlag__ = utils.uniqueId(); this._svgDescription = null; this._keyboardAccessible = false; this._dimension = undefined; this._group = undefined; this._anchor = undefined; this._root = undefined; this._svg = undefined; this._isChild = undefined; this._minWidth = 200; this._defaultWidthCalc = element => { const width = element && element.getBoundingClientRect && element.getBoundingClientRect().width; return (width && width > this._minWidth) ? width : this._minWidth; }; this._widthCalc = this._defaultWidthCalc; this._minHeight = 200; this._defaultHeightCalc = element => { const height = element && element.getBoundingClientRect && element.getBoundingClientRect().height; return (height && height > this._minHeight) ? height : this._minHeight; }; this._heightCalc = this._defaultHeightCalc; this._width = undefined; this._height = undefined; this._useViewBoxResizing = false; this._keyAccessor = pluck('key'); this._valueAccessor = pluck('value'); this._label = pluck('key'); this._ordering = pluck('key'); this._renderLabel = false; this._title = d => `${this.keyAccessor()(d)}: ${this.valueAccessor()(d)}`; this._renderTitle = true; this._controlsUseVisibility = false; this._transitionDuration = 750; this._transitionDelay = 0; this._filterPrinter = printers.filters; this._mandatoryAttributesList = ['dimension', 'group']; this._chartGroup = constants.DEFAULT_CHART_GROUP; this._listeners = d3Dispatch.dispatch( 'preRender', 'postRender', 'preRedraw', 'postRedraw', 'filtered', 'zoomed', 'renderlet', 'pretransition'); this._legend = undefined; this._commitHandler = undefined; this._defaultData = group => group.all(); this._data = this._defaultData; this._filters = []; this._filterHandler = _defaultFilterHandler; this._hasFilterHandler = _defaultHasFilterHandler; this._removeFilterHandler = _defaultRemoveFilterHandler; this._addFilterHandler = _defaultAddFilterHandler; this._resetFilterHandler = _defaultResetFilterHandler; } /** * Set or get the height attribute of a chart. The height is applied to the SVGElement generated by * the chart when rendered (or re-rendered). If a value is given, then it will be used to calculate * the new height and the chart returned for method chaining. The value can either be a numeric, a * function, or falsy. If no value is specified then the value of the current height attribute will * be returned. * * By default, without an explicit height being given, the chart will select the width of its * anchor element. If that isn't possible it defaults to 200 (provided by the * {@link BaseMixin#minHeight minHeight} property). Setting the value falsy will return * the chart to the default behavior. * @see {@link BaseMixin#minHeight minHeight} * @example * // Default height * chart.height(function (element) { * var height = element && element.getBoundingClientRect && element.getBoundingClientRect().height; * return (height && height > chart.minHeight()) ? height : chart.minHeight(); * }); * * chart.height(250); // Set the chart's height to 250px; * chart.height(function(anchor) { return doSomethingWith(anchor); }); // set the chart's height with a function * chart.height(null); // reset the height to the default auto calculation * @param {Number|Function} [height] * @returns {Number|BaseMixin} */ height (height) { if (!arguments.length) { if (!utils.isNumber(this._height)) { // only calculate once this._height = this._heightCalc(this._root.node()); } return this._height; } this._heightCalc = height ? (typeof height === 'function' ? height : utils.constant(height)) : this._defaultHeightCalc; this._height = undefined; return this; } /** * Set or get the width attribute of a chart. * @see {@link BaseMixin#height height} * @see {@link BaseMixin#minWidth minWidth} * @example * // Default width * chart.width(function (element) { * var width = element && element.getBoundingClientRect && element.getBoundingClientRect().width; * return (width && width > chart.minWidth()) ? width : chart.minWidth(); * }); * @param {Number|Function} [width] * @returns {Number|BaseMixin} */ width (width) { if (!arguments.length) { if (!utils.isNumber(this._width)) { // only calculate once this._width = this._widthCalc(this._root.node()); } return this._width; } this._widthCalc = width ? (typeof width === 'function' ? width : utils.constant(width)) : this._defaultWidthCalc; this._width = undefined; return this; } /** * Set or get the minimum width attribute of a chart. This only has effect when used with the default * {@link BaseMixin#width width} function. * @see {@link BaseMixin#width width} * @param {Number} [minWidth=200] * @returns {Number|BaseMixin} */ minWidth (minWidth) { if (!arguments.length) { return this._minWidth; } this._minWidth = minWidth; return this; } /** * Set or get the minimum height attribute of a chart. This only has effect when used with the default * {@link BaseMixin#height height} function. * @see {@link BaseMixin#height height} * @param {Number} [minHeight=200] * @returns {Number|BaseMixin} */ minHeight (minHeight) { if (!arguments.length) { return this._minHeight; } this._minHeight = minHeight; return this; } /** * Turn on/off using the SVG * {@link https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/viewBox `viewBox` attribute}. * When enabled, `viewBox` will be set on the svg root element instead of `width` and `height`. * Requires that the chart aspect ratio be defined using chart.width(w) and chart.height(h). * * This will maintain the aspect ratio while enabling the chart to resize responsively to the * space given to the chart using CSS. For example, the chart can use `width: 100%; height: * 100%` or absolute positioning to resize to its parent div. * * Since the text will be sized as if the chart is drawn according to the width and height, and * will be resized if the chart is any other size, you need to set the chart width and height so * that the text looks good. In practice, 600x400 seems to work pretty well for most charts. * * You can see examples of this resizing strategy in the [Chart Resizing * Examples](http://dc-js.github.io/dc.js/resizing/); just add `?resize=viewbox` to any of the * one-chart examples to enable `useViewBoxResizing`. * @param {Boolean} [useViewBoxResizing=false] * @returns {Boolean|BaseMixin} */ useViewBoxResizing (useViewBoxResizing) { if (!arguments.length) { return this._useViewBoxResizing; } this._useViewBoxResizing = useViewBoxResizing; return this; } /** * **mandatory** * * Set or get the dimension attribute of a chart. In `dc`, a dimension can be any valid * {@link https://github.com/crossfilter/crossfilter/wiki/API-Reference#dimension crossfilter dimension} * * If a value is given, then it will be used as the new dimension. If no value is specified then * the current dimension will be ret