UNPKG

dc

Version:

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

1,555 lines (1,420 loc) 377 kB
/*! * dc 2.1.8 * http://dc-js.github.io/dc.js/ * Copyright 2012-2016 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() { function _dc(d3, crossfilter) { 'use strict'; /** * The entire dc.js library is scoped under the **dc** name space. It does not introduce * anything else into the global name space. * * Most `dc` functions are designed to allow function chaining, meaning they return the current chart * instance whenever it is appropriate. The getter forms of functions do not participate in function * chaining because they return values that are not the chart, although some, * such as {@link dc.baseMixin#svg .svg} and {@link dc.coordinateGridMixin#xAxis .xAxis}, * return values that are themselves chainable d3 objects. * @namespace dc * @version 2.1.8 * @example * // Example chaining * chart.width(300) * .height(300) * .filter('sunday'); */ /*jshint -W079*/ var dc = { version: '2.1.8', 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 }, _renderlet: null }; /*jshint +W079*/ /** * The dc.chartRegistry object maintains sets of all instantiated dc.js charts under named groups * and the default group. * * 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 dc.filterAll dc.filterAll}, {@link dc.refocusAll dc.refocusAll}, * {@link dc.renderAll dc.renderAll}, {@link dc.redrawAll dc.redrawAll}, or chart functions * {@link dc.baseMixin#renderGroup baseMixin.renderGroup}, * {@link dc.baseMixin#redrawGroup baseMixin.redrawGroup} are called. * * @namespace chartRegistry * @memberof dc * @type {{has, register, deregister, clear, list}} */ dc.chartRegistry = (function () { // chartGroup:string => charts:array var _chartMap = {}; function initializeChartGroup (group) { if (!group) { group = dc.constants.DEFAULT_CHART_GROUP; } if (!_chartMap[group]) { _chartMap[group] = []; } return group; } return { /** * Determine if a given chart instance resides in any group in the registry. * @method has * @memberof dc.chartRegistry * @param {Object} chart dc.js chart instance * @returns {Boolean} */ has: function (chart) { for (var e in _chartMap) { if (_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 `dc.constants.DEFAULT_CHART_GROUP` will be used. * @method register * @memberof dc.chartRegistry * @param {Object} chart dc.js chart instance * @param {String} [group] Group name */ register: function (chart, group) { group = 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 `dc.constants.DEFAULT_CHART_GROUP` will be used. * @method deregister * @memberof dc.chartRegistry * @param {Object} chart dc.js chart instance * @param {String} [group] Group name */ deregister: function (chart, group) { group = initializeChartGroup(group); for (var i = 0; i < _chartMap[group].length; i++) { if (_chartMap[group][i].anchorName() === chart.anchorName()) { _chartMap[group].splice(i, 1); break; } } }, /** * Clear given group if one is provided, otherwise clears all groups. * @method clear * @memberof dc.chartRegistry * @param {String} group Group name */ clear: function (group) { if (group) { delete _chartMap[group]; } else { _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. * @method list * @memberof dc.chartRegistry * @param {String} [group] Group name * @returns {Array<Object>} */ list: function (group) { group = initializeChartGroup(group); return _chartMap[group]; } }; })(); /** * Add given chart instance to the given group, creating the group if necessary. * If no group is provided, the default group `dc.constants.DEFAULT_CHART_GROUP` will be used. * @memberof dc * @method registerChart * @param {Object} chart dc.js chart instance * @param {String} [group] Group name */ dc.registerChart = function (chart, group) { dc.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 `dc.constants.DEFAULT_CHART_GROUP` will be used. * @memberof dc * @method deregisterChart * @param {Object} chart dc.js chart instance * @param {String} [group] Group name */ dc.deregisterChart = function (chart, group) { dc.chartRegistry.deregister(chart, group); }; /** * Determine if a given chart instance resides in any group in the registry. * @memberof dc * @method hasChart * @param {Object} chart dc.js chart instance * @returns {Boolean} */ dc.hasChart = function (chart) { return dc.chartRegistry.has(chart); }; /** * Clear given group if one is provided, otherwise clears all groups. * @memberof dc * @method deregisterAllCharts * @param {String} group Group name */ dc.deregisterAllCharts = function (group) { dc.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. * @memberof dc * @method filterAll * @param {String} [group] */ dc.filterAll = function (group) { var charts = dc.chartRegistry.list(group); for (var 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. * @memberof dc * @method refocusAll * @param {String} [group] */ dc.refocusAll = function (group) { var charts = dc.chartRegistry.list(group); for (var 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. * @memberof dc * @method renderAll * @param {String} [group] */ dc.renderAll = function (group) { var charts = dc.chartRegistry.list(group); for (var i = 0; i < charts.length; ++i) { charts[i].render(); } if (dc._renderlet !== null) { dc._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. * @memberof dc * @method redrawAll * @param {String} [group] */ dc.redrawAll = function (group) { var charts = dc.chartRegistry.list(group); for (var i = 0; i < charts.length; ++i) { charts[i].redraw(); } if (dc._renderlet !== null) { dc._renderlet(group); } }; /** * If this boolean is set truthy, all transitions will be disabled, and changes to the charts will happen * immediately. * @memberof dc * @member disableTransitions * @type {Boolean} * @default false */ dc.disableTransitions = false; /** * Start a transition on a selection if transitions are globally enabled * ({@link dc.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. * @memberof dc * @method 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} */ dc.transition = function (selection, duration, delay, name) { if (dc.disableTransitions || duration <= 0) { return selection; } var 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 */ dc.optionalTransition = function (enable, duration, delay, name) { if (enable) { return function (selection) { return dc.transition(selection, duration, delay, name); }; } else { return function (selection) { return selection; }; } }; // See http://stackoverflow.com/a/20773846 dc.afterTransition = function (transition, callback) { if (transition.empty() || !transition.duration) { callback.call(transition); } else { var n = 0; transition .each(function () { ++n; }) .each('end', function () { if (!--n) { callback.call(transition); } }); } }; /** * @namespace units * @memberof dc * @type {{}} */ dc.units = {}; /** * The default value for {@link dc.coordinateGridMixin#xUnits .xUnits} for the * {@link dc.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 dc.units * @see {@link dc.coordinateGridMixin#xUnits coordinateGridMixin.xUnits} * @example * chart.xUnits(dc.units.integers) // already the default * @param {Number} start * @param {Number} end * @returns {Number} */ dc.units.integers = function (start, end) { return Math.abs(end - start); }; /** * This argument can be passed to the {@link dc.coordinateGridMixin#xUnits .xUnits} function of the to * specify ordinal units for the x axis. Usually this parameter is used in combination with passing * {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Ordinal-Scales.md d3.scale.ordinal} to * {@link dc.coordinateGridMixin#x .x}. * It just returns the domain passed to it, which for ordinal charts is an array of all values. * @method ordinal * @memberof dc.units * @see {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Ordinal-Scales.md d3.scale.ordinal} * @see {@link dc.coordinateGridMixin#xUnits coordinateGridMixin.xUnits} * @see {@link dc.coordinateGridMixin#x coordinateGridMixin.x} * @example * chart.xUnits(dc.units.ordinal) * .x(d3.scale.ordinal()) * @param {*} start * @param {*} end * @param {Array<String>} domain * @returns {Array<String>} */ dc.units.ordinal = function (start, end, domain) { return domain; }; /** * @namespace fp * @memberof dc.units * @type {{}} */ dc.units.fp = {}; /** * This function generates an argument for the {@link dc.coordinateGridMixin Coordinate Grid Chart} * {@link dc.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 dc.units.fp * @see {@link dc.coordinateGridMixin#xUnits coordinateGridMixin.xUnits} * @example * // specify values (and ticks) every 0.1 units * chart.xUnits(dc.units.fp.precision(0.1) * // there are 500 units between 0.5 and 1 if the precision is 0.001 * var thousandths = dc.units.fp.precision(0.001); * thousandths(0.5, 1.0) // returns 500 * @param {Number} precision * @returns {Function} start-end unit function */ dc.units.fp.precision = function (precision) { var _f = function (s, e) { var d = Math.abs((e - s) / _f.resolution); if (dc.utils.isNegligible(d - Math.floor(d))) { return Math.floor(d); } else { return Math.ceil(d); } }; _f.resolution = precision; return _f; }; dc.round = {}; dc.round.floor = function (n) { return Math.floor(n); }; dc.round.ceil = function (n) { return Math.ceil(n); }; dc.round.round = function (n) { return Math.round(n); }; dc.override = function (obj, functionName, newFunction) { var existingFunction = obj[functionName]; obj['_' + functionName] = existingFunction; obj[functionName] = newFunction; }; dc.renderlet = function (_) { if (!arguments.length) { return dc._renderlet; } dc._renderlet = _; return dc; }; dc.instanceOfChart = function (o) { return o instanceof Object && o.__dcFlag__ && true; }; dc.errors = {}; dc.errors.Exception = function (msg) { var _msg = msg || 'Unexpected internal error'; this.message = _msg; this.toString = function () { return _msg; }; this.stack = (new Error()).stack; }; dc.errors.Exception.prototype = Object.create(Error.prototype); dc.errors.Exception.prototype.constructor = dc.errors.Exception; dc.errors.InvalidStateException = function () { dc.errors.Exception.apply(this, arguments); }; dc.errors.InvalidStateException.prototype = Object.create(dc.errors.Exception.prototype); dc.errors.InvalidStateException.prototype.constructor = dc.errors.InvalidStateException; dc.errors.BadArgumentException = function () { dc.errors.Exception.apply(this, arguments); }; dc.errors.BadArgumentException.prototype = Object.create(dc.errors.Exception.prototype); dc.errors.BadArgumentException.prototype.constructor = dc.errors.BadArgumentException; /** * The default date format for dc.js * @name dateFormat * @memberof dc * @type {Function} * @default d3.time.format('%m/%d/%Y') */ dc.dateFormat = d3.time.format('%m/%d/%Y'); /** * @namespace printers * @memberof dc * @type {{}} */ dc.printers = {}; /** * Converts a list of filters into a readable string. * @method filters * @memberof dc.printers * @param {Array<dc.filters>} filters * @returns {String} */ dc.printers.filters = function (filters) { var s = ''; for (var i = 0; i < filters.length; ++i) { if (i > 0) { s += ', '; } s += dc.printers.filter(filters[i]); } return s; }; /** * Converts a filter into a readable string. * @method filter * @memberof dc.printers * @param {dc.filters|any|Array<any>} filter * @returns {String} */ dc.printers.filter = function (filter) { var s = ''; if (typeof filter !== 'undefined' && filter !== null) { if (filter instanceof Array) { if (filter.length >= 2) { s = '[' + dc.utils.printSingleValue(filter[0]) + ' -> ' + dc.utils.printSingleValue(filter[1]) + ']'; } else if (filter.length >= 1) { s = dc.utils.printSingleValue(filter[0]); } } else { s = dc.utils.printSingleValue(filter); } } return s; }; /** * 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. * @method pluck * @memberof dc * @example * var xPluck = dc.pluck('x'); * var objA = {x: 1}; * xPluck(objA) // 1 * @example * var xPosition = dc.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; * }); * dc.selectAll('.circle').data(...).x(xPosition); * @param {String} n * @param {Function} [f] * @returns {Function} */ dc.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 * @memberof dc * @type {{}} */ dc.utils = {}; /** * Print a single value filter. * @method printSingleValue * @memberof dc.utils * @param {any} filter * @returns {String} */ dc.utils.printSingleValue = function (filter) { var s = '' + filter; if (filter instanceof Date) { s = dc.dateFormat(filter); } else if (typeof(filter) === 'string') { s = filter; } else if (dc.utils.isFloat(filter)) { s = dc.utils.printSingleValue.fformat(filter); } else if (dc.utils.isInteger(filter)) { s = Math.round(filter); } return s; }; dc.utils.printSingleValue.fformat = d3.format('.2f'); /** * Arbitrary add one value to another. * @method add * @memberof dc.utils * @todo * These assume than any string r is a percentage (whether or not it includes %). * They also generate strange results if l is a string. * @param {String|Date|Number} l the value to modify * @param {Number} r the amount by which to modify the value * @param {String} [t] if `l` is a `Date`, the * [interval](https://github.com/d3/d3-3.x-api-reference/blob/master/Time-Intervals.md#interval) in * the `d3.time` namespace * @returns {String|Date|Number} */ dc.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 || 'day'; return d3.time[t].offset(l, r); } else if (typeof r === 'string') { var percentage = (+r / 100); return l > 0 ? l * (1 + percentage) : l * (1 - percentage); } else { return l + r; } }; /** * Arbitrary subtract one value from another. * @method subtract * @memberof dc.utils * @todo * These assume than any string r is a percentage (whether or not it includes %). * They also generate strange results if l is a string. * @param {String|Date|Number} l the value to modify * @param {Number} r the amount by which to modify the value * @param {String} [t] if `l` is a `Date`, the * [interval](https://github.com/d3/d3-3.x-api-reference/blob/master/Time-Intervals.md#interval) in * the `d3.time` namespace * @returns {String|Date|Number} */ dc.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 || 'day'; return d3.time[t].offset(l, -r); } else if (typeof r === 'string') { var percentage = (+r / 100); return l < 0 ? l * (1 + percentage) : l * (1 - percentage); } else { return l - r; } }; /** * Is the value a number? * @method isNumber * @memberof dc.utils * @param {any} n * @returns {Boolean} */ dc.utils.isNumber = function (n) { return n === +n; }; /** * Is the value a float? * @method isFloat * @memberof dc.utils * @param {any} n * @returns {Boolean} */ dc.utils.isFloat = function (n) { return n === +n && n !== (n | 0); }; /** * Is the value an integer? * @method isInteger * @memberof dc.utils * @param {any} n * @returns {Boolean} */ dc.utils.isInteger = function (n) { return n === +n && n === (n | 0); }; /** * Is the value very close to zero? * @method isNegligible * @memberof dc.utils * @param {any} n * @returns {Boolean} */ dc.utils.isNegligible = function (n) { return !dc.utils.isNumber(n) || (n < dc.constants.NEGLIGIBLE_NUMBER && n > -dc.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 dc.utils * @param {any} val * @param {any} min * @param {any} max * @returns {any} */ dc.utils.clamp = function (val, min, max) { return val < min ? min : (val > max ? max : val); }; /** * Using a simple static counter, provide a unique integer id. * @method uniqueId * @memberof dc.utils * @returns {Number} */ var _idCounter = 0; dc.utils.uniqueId = function () { return ++_idCounter; }; /** * Convert a name to an ID. * @method nameToId * @memberof dc.utils * @param {String} name * @returns {String} */ dc.utils.nameToId = function (name) { return name.toLowerCase().replace(/[\s]/g, '_').replace(/[\.']/g, ''); }; /** * Append or select an item on a parent element. * @method appendOrSelect * @memberof dc.utils * @param {d3.selection} parent * @param {String} selector * @param {String} tag * @returns {d3.selection} */ dc.utils.appendOrSelect = function (parent, selector, tag) { tag = tag || selector; var 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 dc.utils * @param {Number|any} n * @returns {Number} */ dc.utils.safeNumber = function (n) { return dc.utils.isNumber(+n) ? +n : 0;}; dc.logger = {}; dc.logger.enableDebugLog = false; dc.logger.warn = function (msg) { if (console) { if (console.warn) { console.warn(msg); } else if (console.log) { console.log(msg); } } return dc.logger; }; dc.logger.debug = function (msg) { if (dc.logger.enableDebugLog && console) { if (console.debug) { console.debug(msg); } else if (console.log) { console.log(msg); } } return dc.logger; }; dc.logger.deprecate = function (fn, msg) { // Allow logging of deprecation var warned = false; function deprecated () { if (!warned) { dc.logger.warn(msg); warned = true; } return fn.apply(this, arguments); } return deprecated; }; dc.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 * @memberof dc * @example * chart.on('renderlet', function(chart) { * // smooth the rendering through event throttling * dc.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] */ dc.events.trigger = function (closure, delay) { if (!delay) { closure(); return; } dc.events.current = closure; setTimeout(function () { if (closure === dc.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 * @memberof dc * @type {{}} */ dc.filters = {}; /** * RangedFilter is a filter which accepts keys between `low` and `high`. It is used to implement X * axis brushing for the {@link dc.coordinateGridMixin coordinate grid charts}. * * Its `filterType` is 'RangedFilter' * @name RangedFilter * @memberof dc.filters * @param {Number} low * @param {Number} high * @returns {Array<Number>} * @constructor */ dc.filters.RangedFilter = function (low, high) { var 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 dc.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 dc.filters * @param {Array<Number>} filter * @returns {Array<Number>} * @constructor */ dc.filters.TwoDimensionalFilter = function (filter) { if (filter === null) { return null; } var 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 dc.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 dc.filters * @param {Array<Array<Number>>} filter * @returns {Array<Array<Number>>} * @constructor */ dc.filters.RangedTwoDimensionalFilter = function (filter) { if (filter === null) { return null; } var f = filter; var 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) { var 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; }; /** * `dc.baseMixin` is an abstract functional object representing a basic `dc` chart object * for all chart and widget implementations. Methods from the {@link #dc.baseMixin dc.baseMixin} are inherited * and available on all chart implementations in the `dc` library. * @name baseMixin * @memberof dc * @mixin * @param {Object} _chart * @returns {dc.baseMixin} */ dc.baseMixin = function (_chart) { _chart.__dcFlag__ = dc.utils.uniqueId(); var _dimension; var _group; var _anchor; var _root; var _svg; var _isChild; var _minWidth = 200; var _defaultWidthCalc = function (element) { var width = element && element.getBoundingClientRect && element.getBoundingClientRect().width; return (width && width > _minWidth) ? width : _minWidth; }; var _widthCalc = _defaultWidthCalc; var _minHeight = 200; var _defaultHeightCalc = function (element) { var height = element && element.getBoundingClientRect && element.getBoundingClientRect().height; return (height && height > _minHeight) ? height : _minHeight; }; var _heightCalc = _defaultHeightCalc; var _width, _height; var _useViewBoxResizing = false; var _keyAccessor = dc.pluck('key'); var _valueAccessor = dc.pluck('value'); var _label = dc.pluck('key'); var _ordering = dc.pluck('key'); var _orderSort; var _renderLabel = false; var _title = function (d) { return _chart.keyAccessor()(d) + ': ' + _chart.valueAccessor()(d); }; var _renderTitle = true; var _controlsUseVisibility = false; var _transitionDuration = 750; var _transitionDelay = 0; var _filterPrinter = dc.printers.filters; var _mandatoryAttributes = ['dimension', 'group']; var _chartGroup = dc.constants.DEFAULT_CHART_GROUP; var _listeners = d3.dispatch( 'preRender', 'postRender', 'preRedraw', 'postRedraw', 'filtered', 'zoomed', 'renderlet', 'pretransition'); var _legend; var _commitHandler; var _filters = []; var _filterHandler = function (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(function (d) { for (var i = 0; i < filters.length; i++) { var filter = filters[i]; if (filter.isFiltered && filter.isFiltered(d)) { return true; } else if (filter <= d && filter >= d) { return true; } } return false; }); } return filters; }; var _data = function (group) { return group.all(); }; /** * 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 dc.baseMixin#minHeight minHeight} property). Setting the value falsy will return * the chart to the default behavior. * @method height * @memberof dc.baseMixin * @instance * @see {@link dc.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|dc.baseMixin} */ _chart.height = function (height) { if (!arguments.length) { if (!dc.utils.isNumber(_height)) { // only calculate once _height = _heightCalc(_root.node()); } return _height; } _heightCalc = d3.functor(height || _defaultHeightCalc); _height = undefined; return _chart; }; /** * Set or get the width attribute of a chart. * @method width * @memberof dc.baseMixin * @instance * @see {@link dc.baseMixin#height height} * @see {@link dc.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|dc.baseMixin} */ _chart.width = function (width) { if (!arguments.length) { if (!dc.utils.isNumber(_width)) { // only calculate once _width = _widthCalc(_root.node()); } return _width; } _widthCalc = d3.functor(width || _defaultWidthCalc); _width = undefined; return _chart; }; /** * Set or get the minimum width attribute of a chart. This only has effect when used with the default * {@link dc.baseMixin#width width} function. * @method minWidth * @memberof dc.baseMixin * @instance * @see {@link dc.baseMixin#width width} * @param {Number} [minWidth=200] * @returns {Number|dc.baseMixin} */ _chart.minWidth = function (minWidth) { if (!arguments.length) { return _minWidth; } _minWidth = minWidth; return _chart; }; /** * Set or get the minimum height attribute of a chart. This only has effect when used with the default * {@link dc.baseMixin#height height} function. * @method minHeight * @memberof dc.baseMixin * @instance * @see {@link dc.baseMixin#height height} * @param {Number} [minHeight=200] * @returns {Number|dc.baseMixin} */ _chart.minHeight = function (minHeight) { if (!arguments.length) { return _minHeight; } _minHeight = minHeight; return _chart; }; /** * 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`. * @method useViewBoxResizing * @memberof dc.baseMixin * @instance * @param {Boolean} [useViewBoxResizing=false] * @returns {Boolean|dc.baseMixin} */ _chart.useViewBoxResizing = function (useViewBoxResizing) { if (!arguments.length) { return _useViewBoxResizing; } _useViewBoxResizing = useViewBoxResizing; return _chart; }; /** * **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 returned. * @method dimension * @memberof dc.baseMixin * @instance * @see {@link https://github.com/crossfilter/crossfilter/wiki/API-Reference#dimension crossfilter.dimension} * @example * var index = crossfilter([]); * var dimension = index.dimension(dc.pluck('key')); * chart.dimension(dimension); * @param {crossfilter.dimension} [dimension] * @returns {crossfilter.dimension|dc.baseMixin} */ _chart.dimension = function (dimension) { if (!arguments.length) { return _dimension; } _dimension = dimension; _chart.expireCache(); return _chart; }; /** * Set the data callback or retrieve the chart's data set. The data callback is passed the chart's * group and by default will return * {@link https://github.com/crossfilter/crossfilter/wiki/API-Reference#group_all group.all}. * This behavior may be modified to, for instance, return only the top 5 groups. * @method data * @memberof dc.baseMixin * @instance * @example * // Default data function * chart.data(function (group) { return group.all(); }); * * chart.data(function (group) { return group.top(5); }); * @param {Function} [callback] * @returns {*|dc.baseMixin} */ _chart.data = function (callback) { if (!arguments.length) { return _data.call(_chart, _group); } _data = d3.functor(callback); _chart.expireCache(); return _chart; }; /** * **mandatory** * * Set or get the group attribute of a chart. In `dc` a group is a * {@link https://github.com/crossfilter/crossfilter/wiki/API-Reference#group-map-reduce crossfilter group}. * Usually the group should be created from the particular dimension associated with the same chart. If a value is * given, then it will be used as the new group. * * If no value specified then the current group will be returned. * If `name` is specified then it will be used to generate legend label. * @method group * @memberof dc.baseMixin * @instance * @see {@link https://github.com/crossfilter/crossfilter/wiki/API-Reference#group-map-reduce crossfilter.group} * @example * var index = crossfilter([]); * var dimension = index.dimension(dc.pluck('key')); * chart.dimension(dimension); * chart.group(dimension.group(crossfilter.reduceSum())); * @param {crossfilter.group} [group] * @param {String} [name] * @returns {crossfilter.group|dc.baseMixin} */ _chart.group = function (group, name) { if (!arguments.length) { return _group; } _group = group; _chart._groupName = name; _chart.expireCache(); return _chart; }; /** * Get or set an accessor to order ordinal dimensions. The chart uses * {@link https://github.com/crossfilter/crossfilter/wiki/API-Reference#quicksort_by crossfilter.quicksort.by} * to sort elements; this accessor returns the value to order on. * @method ordering * @memberof dc.baseMixin * @instance * @see {@link https://github.com/crossfilter/crossfilter/wiki/API-Reference#quicksort_by crossfilter.quicksort.by} * @example * // Default ordering accessor * _chart.ordering(dc.pluck('key')); * @param {Function} [orderFunction] * @returns {Function|dc.baseMixin} */ _chart.ordering = function (orderFunction) { if (!arguments.length) { return _ordering; } _ordering = orderFunction; _orderSort = crossfilter.quicksort.by(_ordering); _chart.expireCache(); return _chart; }; _chart._computeOrderedGroups = function (data) { var dataCopy = data.slice(0); if (dataCopy.length <= 1) { return dataCopy; } if (!_orderSort) { _orderSort = crossfilter.quicksort.by(_ordering); } return _orderSort(dataCopy, 0, dataCopy.length); }; /** * Clear all filters associated with this chart. The same effect can be achieved by calling * {@link dc.baseMixin#filter chart.filter(null)}. * @method filterAll * @memberof dc.baseMixin * @instance * @returns {dc.baseMixin} */ _chart.filterAll = function () { return _chart.filter(null); }; /** * Execute d3 single selection in the chart's scope using the given selector and return the d3 * selection. * * This function is **not chainable** since it does not return a chart instance; however the d3 * selection result can be chained to d3 function calls. * @method select * @memberof dc.baseMixin * @instance * @see {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Selections.md#d3_select d3.select} * @example * // Has the same effect as d3.select('#chart-id').select(selector) * chart.select(selector) * @returns {d3.selection} */ _chart.select = function (s) { return _root.select(s); }; /** * Execute in scope d3 selectAll using the given selector and return d3 selection result. * * This function is **not chainable** since it does not return a chart instance; however the d3 * selection result can be chained to d3 function calls. * @method selectAll * @memberof dc.baseMixin * @instance * @see {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Selections.md#d3_selectAll d3.selectAll} * @example * // Has the same effect as d3.select('#chart-id').selectAll(selector) * chart.selectAll(selector) * @returns {d3.selection} */ _chart.selectAll = function (s) { return _root ? _root.selectAll(s) : null; }; /** * Set the root SVGElement to either be an existing chart's root; or any valid [d3 single * selector](https://github.com/d3/d3-3.x-api-reference/blob/master/Selections.md#selecting-elements) specifying a dom * block element such as a div; or a dom element or d3 selection. Optionally registers the chart * within the chartGroup. This class is called internally on chart initialization, but be called * again to relocate the chart. However, it will orphan any previously created SVGElements. * @method anchor * @memberof dc.baseMixin * @instance * @param {anchorChart|anchorSelector|anchorNode} [parent] * @param {String} [chartGroup] * @returns {String|node|d3.selection|dc.baseMixin} */ _chart.anchor = function (parent, chartGroup) { if (!arguments.length) { return _anchor; } if (dc.instanceOfChart(parent)) { _anchor = parent.anchor(); _root = parent.root(); _isChild = true; } else if (parent) { if (parent.select && parent.classed) { // detect d3 selection _anchor = parent.node(); } else { _anchor = parent; } _root = d3.select(_anchor); _root.classed(dc.constants.CHART_CLASS, true); dc.registerChart(_chart, chartGroup); _isChild = false; } else { throw new dc.errors.BadArgumentException('parent must be defined'); } _chartGroup = chartGroup; return _chart; }; /** * Returns the DOM id for the chart's anchored location. * @method anchorName * @memberof dc.baseMixin * @instance * @returns {String} */ _chart.anchorName = function () { var a = _chart.anchor(); if (a && a.id) { return a.id; } if (a && a.replace) { return a.replace('#', ''); } return 'dc-chart' + _chart.chartID(); }; /** * Returns the root element where a chart resides. Usually it will be the parent div element where * the SVGElement was created. You can also pass in a new root element however this is usually handled by * dc internally. Resetting the root element on a chart outside of dc internals may have * unexpected consequences. * @method root * @memberof dc.baseMixin * @instance * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement HTMLElement} * @param {HTMLElement} [rootElement] * @returns {HTMLElement|dc.baseMixin} */ _chart.root = function (rootElement) { if (!arguments.length) { return _root; } _root = rootElement; return _chart; }; /** * Returns the top SVGElement for this specific chart. You can also pass in a new SVGElement, * however this is usually handled by dc internally. Resetting the SVGElement on a chart outside * of dc internals may have unexpected consequences. * @method svg * @memberof dc.baseMixin * @instance * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/SVGElement SVGElement} * @param {SVGElement|d3.selection} [svgElement] * @returns {SVGElement|d3.selection|dc.baseMixin} */ _chart.svg = function (svgElement) { if (!arguments.length) { return _svg; } _svg = svgElement; return _chart; }; /** * Remove the chart's SVGElements from the dom and recreate the container SVGElement. * @method resetSvg * @memberof dc.baseMixin * @instance * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/SVGElement SVGElement} * @returns {SVGElement} */ _chart.resetSvg = function () { _chart.select('svg').remove(); return generateSvg(); }; function sizeSvg () { if (_svg) { if (!_useViewBoxResizing) { _svg .attr('width', _chart.width()) .attr('height', _chart.height()); } else if (!_svg.attr('viewBox')) { _svg .attr('viewBox', '0 0 ' + _chart.width() + ' ' + _chart.height()); } } } function generateSvg () { _svg = _chart.root().append('svg'); sizeSvg(); return _svg; } /** * Set or get the filter printer function. The filter printer function is used to generate human * friendly text for filter value(s) associated with the chart instance. The text will get shown * in the `.filter element; see {@link dc.baseMixin#turnOnControls turnOnControls}. * * By default dc charts use a default filter printer {@link dc.printers.filters dc.printers.filters} * that provides simple printing support for both single value and ranged filters. * @method filterPrinter * @memberof dc.baseMixin * @instance * @example * // for a chart with an ordinal brush, print the filters in upper case * chart.filterPrinter(function(filters) { * return filters.map(function(f) { return f.toUpperCase(); }).join(', '); * }); * // for a chart with a range brush, print the filter as start and extent * chart.filterPrinter(function(filters) { * return 'start ' + dc.utils.printSingleValue(filters[0][0]) + *