/*! * dc 2.1.9 * 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.9 * @example * // Example chaining * chart.width(300) * .height(300) * .filter('sunday'); */ /*jshint -W079*/ var dc = { version: '2.1.9', 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} */ 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} domain * @returns {Array} */ 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} 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} 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} * @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} filter * @returns {Array} * @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>} filter * @returns {Array>} * @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]) + * ' extent ' + dc.utils.printSingleValue(filters[0][1] - filters[0][0]); * }); * @param {Function} [filterPrinterFunction=dc.printers.filters] * @returns {Function|dc.baseMixin} */ _chart.filterPrinter = function (filterPrinterFunction) { if (!arguments.length) { return _filterPrinter; } _filterPrinter = filterPrinterFunction; return _chart; }; /** * If set, use the `visibility` attribute instead of the `display` attribute for showing/hiding * chart reset and filter controls, for less disruption to the layout. * @method controlsUseVisibility * @memberof dc.baseMixin * @instance * @param {Boolean} [controlsUseVisibility=false] * @returns {Boolean|dc.baseMixin} **/ _chart.controlsUseVisibility = function (useVisibility) { if (!arguments.length) { return _controlsUseVisibility; } _controlsUseVisibility = useVisibility; return _chart; }; /** * Turn on optional control elements within the root element. dc currently supports the * following html control elements. * * root.selectAll('.reset') - elements are turned on if the chart has an active filter. This type * of control element is usually used to store a reset link to allow user to reset filter on a * certain chart. This element will be turned off automatically if the filter is cleared. * * root.selectAll('.filter') elements are turned on if the chart has an active filter. The text * content of this element is then replaced with the current filter value using the filter printer * function. This type of element will be turned off automatically if the filter is cleared. * @method turnOnControls * @memberof dc.baseMixin * @instance * @returns {dc.baseMixin} */ _chart.turnOnControls = function () { if (_root) { var attribute = _chart.controlsUseVisibility() ? 'visibility' : 'display'; _chart.selectAll('.reset').style(attribute, null); _chart.selectAll('.filter').text(_filterPrinter(_chart.filters())).style(attribute, null); } return _chart; }; /** * Turn off optional control elements within the root element. * @method turnOffControls * @memberof dc.baseMixin * @see {@link dc.baseMixin#turnOnControls turnOnControls} * @instance * @returns {dc.baseMixin} */ _chart.turnOffControls = function () { if (_root) { var attribute = _chart.controlsUseVisibility() ? 'visibility' : 'display'; var value = _chart.controlsUseVisibility() ? 'hidden' : 'none'; _chart.selectAll('.reset').style(attribute, value); _chart.selectAll('.filter').style(attribute, value).text(_chart.filter()); } return _chart; }; /** * Set or get the animation transition duration (in milliseconds) for this chart instance. * @method transitionDuration * @memberof dc.baseMixin * @instance * @param {Number} [duration=750] * @returns {Number|dc.baseMixin} */ _chart.transitionDuration = function (duration) { if (!arguments.length) { return _transitionDuration; } _transitionDuration = duration; return _chart; }; /** * Set or get the animation transition delay (in milliseconds) for this chart instance. * @method transitionDelay * @memberof dc.baseMixin * @instance * @param {Number} [delay=0] * @returns {Number|dc.baseMixin} */ _chart.transitionDelay = function (delay) { if (!arguments.length) { return _transitionDelay; } _transitionDelay = delay; return _chart; }; _chart._mandatoryAttributes = function (_) { if (!arguments.length) { return _mandatoryAttributes; } _mandatoryAttributes = _; return _chart; }; function checkForMandatoryAttributes (a) { if (!_chart[a] || !_chart[a]()) { throw new dc.errors.InvalidStateException('Mandatory attribute chart.' + a + ' is missing on chart[#' + _chart.anchorName() + ']'); } } /** * Invoking this method will force the chart to re-render everything from scratch. Generally it * should only be used to render the chart for the first time on the page or if you want to make * sure everything is redrawn from scratch instead of relying on the default incremental redrawing * behaviour. * @method render * @memberof dc.baseMixin * @instance * @returns {dc.baseMixin} */ _chart.render = function () { _height = _width = undefined; // force recalculate _listeners.preRender(_chart); if (_mandatoryAttributes) { _mandatoryAttributes.forEach(checkForMandatoryAttributes); } var result = _chart._doRender(); if (_legend) { _legend.render(); } _chart._activateRenderlets('postRender'); return result; }; _chart._activateRenderlets = function (event) { _listeners.pretransition(_chart); if (_chart.transitionDuration() > 0 && _svg) { _svg.transition().duration(_chart.transitionDuration()).delay(_chart.transitionDelay()) .each('end', function () { _listeners.renderlet(_chart); if (event) { _listeners[event](_chart); } }); } else { _listeners.renderlet(_chart); if (event) { _listeners[event](_chart); } } }; /** * Calling redraw will cause the chart to re-render data changes incrementally. If there is no * change in the underlying data dimension then calling this method will have no effect on the * chart. Most chart interaction in dc will automatically trigger this method through internal * events (in particular {@link dc.redrawAll dc.redrawAll}); therefore, you only need to * manually invoke this function if data is manipulated outside of dc's control (for example if * data is loaded in the background using * {@link https://github.com/crossfilter/crossfilter/wiki/API-Reference#crossfilter_add crossfilter.add}). * @method redraw * @memberof dc.baseMixin * @instance * @returns {dc.baseMixin} */ _chart.redraw = function () { sizeSvg(); _listeners.preRedraw(_chart); var result = _chart._doRedraw(); if (_legend) { _legend.render(); } _chart._activateRenderlets('postRedraw'); return result; }; /** * Gets/sets the commit handler. If the chart has a commit handler, the handler will be called when * the chart's filters have changed, in order to send the filter data asynchronously to a server. * * Unlike other functions in dc.js, the commit handler is asynchronous. It takes two arguments: * a flag indicating whether this is a render (true) or a redraw (false), and a callback to be * triggered once the commit is filtered. The callback has the standard node.js continuation signature * with error first and result second. * @method commitHandler * @memberof dc.baseMixin * @instance * @returns {dc.baseMixin} */ _chart.commitHandler = function (commitHandler) { if (!arguments.length) { return _commitHandler; } _commitHandler = commitHandler; return _chart; }; /** * Redraws all charts in the same group as this chart, typically in reaction to a filter * change. If the chart has a {@link dc.baseMixin.commitFilter commitHandler}, it will * be executed and waited for. * @method redrawGroup * @memberof dc.baseMixin * @instance * @returns {dc.baseMixin} */ _chart.redrawGroup = function () { if (_commitHandler) { _commitHandler(false, function (error, result) { if (error) { console.log(error); } else { dc.redrawAll(_chart.chartGroup()); } }); } else { dc.redrawAll(_chart.chartGroup()); } return _chart; }; /** * Renders all charts in the same group as this chart. If the chart has a * {@link dc.baseMixin.commitFilter commitHandler}, it will be executed and waited for * @method renderGroup * @memberof dc.baseMixin * @instance * @returns {dc.baseMixin} */ _chart.renderGroup = function () { if (_commitHandler) { _commitHandler(false, function (error, result) { if (error) { console.log(error); } else { dc.renderAll(_chart.chartGroup()); } }); } else { dc.renderAll(_chart.chartGroup()); } return _chart; }; _chart._invokeFilteredListener = function (f) { if (f !== undefined) { _listeners.filtered(_chart, f); } }; _chart._invokeZoomedListener = function () { _listeners.zoomed(_chart); }; var _hasFilterHandler = function (filters, filter) { if (filter === null || typeof(filter) === 'undefined') { return filters.length > 0; } return filters.some(function (f) { return filter <= f && filter >= f; }); }; /** * Set or get the has-filter handler. The has-filter handler is a function that checks to see if * the chart's current filters (first argument) include a specific filter (second argument). Using a custom has-filter handler allows * you to change the way filters are checked for and replaced. * @method hasFilterHandler * @memberof dc.baseMixin * @instance * @example * // default has-filter handler * chart.hasFilterHandler(function (filters, filter) { * if (filter === null || typeof(filter) === 'undefined') { * return filters.length > 0; * } * return filters.some(function (f) { * return filter <= f && filter >= f; * }); * }); * * // custom filter handler (no-op) * chart.hasFilterHandler(function(filters, filter) { * return false; * }); * @param {Function} [hasFilterHandler] * @returns {Function|dc.baseMixin} */ _chart.hasFilterHandler = function (hasFilterHandler) { if (!arguments.length) { return _hasFilterHandler; } _hasFilterHandler = hasFilterHandler; return _chart; }; /** * Check whether any active filter or a specific filter is associated with particular chart instance. * This function is **not chainable**. * @method hasFilter * @memberof dc.baseMixin * @instance * @see {@link dc.baseMixin#hasFilterHandler hasFilterHandler} * @param {*} [filter] * @returns {Boolean} */ _chart.hasFilter = function (filter) { return _hasFilterHandler(_filters, filter); }; var _removeFilterHandler = function (filters, filter) { for (var i = 0; i < filters.length; i++) { if (filters[i] <= filter && filters[i] >= filter) { filters.splice(i, 1); break; } } return filters; }; /** * Set or get the remove filter handler. The remove filter handler is a function that removes a * filter from the chart's current filters. Using a custom remove filter handler allows you to * change how filters are removed or perform additional work when removing a filter, e.g. when * using a filter server other than crossfilter. * * The handler should return a new or modified array as the result. * @method removeFilterHandler * @memberof dc.baseMixin * @instance * @example * // default remove filter handler * chart.removeFilterHandler(function (filters, filter) { * for (var i = 0; i < filters.length; i++) { * if (filters[i] <= filter && filters[i] >= filter) { * filters.splice(i, 1); * break; * } * } * return filters; * }); * * // custom filter handler (no-op) * chart.removeFilterHandler(function(filters, filter) { * return filters; * }); * @param {Function} [removeFilterHandler] * @returns {Function|dc.baseMixin} */ _chart.removeFilterHandler = function (removeFilterHandler) { if (!arguments.length) { return _removeFilterHandler; } _removeFilterHandler = removeFilterHandler; return _chart; }; var _addFilterHandler = function (filters, filter) { filters.push(filter); return filters; }; /** * Set or get the add filter handler. The add filter handler is a function that adds a filter to * the chart's filter list. Using a custom add filter handler allows you to change the way filters * are added or perform additional work when adding a filter, e.g. when using a filter server other * than crossfilter. * * The handler should return a new or modified array as the result. * @method addFilterHandler * @memberof dc.baseMixin * @instance * @example * // default add filter handler * chart.addFilterHandler(function (filters, filter) { * filters.push(filter); * return filters; * }); * * // custom filter handler (no-op) * chart.addFilterHandler(function(filters, filter) { * return filters; * }); * @param {Function} [addFilterHandler] * @returns {Function|dc.baseMixin} */ _chart.addFilterHandler = function (addFilterHandler) { if (!arguments.length) { return _addFilterHandler; } _addFilterHandler = addFilterHandler; return _chart; }; var _resetFilterHandler = function (filters) { return []; }; /** * Set or get the reset filter handler. The reset filter handler is a function that resets the * chart's filter list by returning a new list. Using a custom reset filter handler allows you to * change the way filters are reset, or perform additional work when resetting the filters, * e.g. when using a filter server other than crossfilter. * * The handler should return a new or modified array as the result. * @method resetFilterHandler * @memberof dc.baseMixin * @instance * @example * // default remove filter handler * function (filters) { * return []; * } * * // custom filter handler (no-op) * chart.resetFilterHandler(function(filters) { * return filters; * }); * @param {Function} [resetFilterHandler] * @returns {dc.baseMixin} */ _chart.resetFilterHandler = function (resetFilterHandler) { if (!arguments.length) { return _resetFilterHandler; } _resetFilterHandler = resetFilterHandler; return _chart; }; function applyFilters (filters) { if (_chart.dimension() && _chart.dimension().filter) { var fs = _filterHandler(_chart.dimension(), filters); if (fs) { filters = fs; } } return filters; } /** * Replace the chart filter. This is equivalent to calling `chart.filter(null).filter(filter)` * but more efficient because the filter is only applied once. * * @method replaceFilter * @memberof dc.baseMixin * @instance * @param {*} [filter] * @returns {dc.baseMixin} **/ _chart.replaceFilter = function (filter) { _filters = _resetFilterHandler(_filters); _chart.filter(filter); return _chart; }; /** * Filter the chart by the given parameter, or return the current filter if no input parameter * is given. * * The filter parameter can take one of these forms: * * A single value: the value will be toggled (added if it is not present in the current * filters, removed if it is present) * * An array containing a single array of values (`[[value,value,value]]`): each value is * toggled * * When appropriate for the chart, a {@link dc.filters dc filter object} such as * * {@link dc.filters.RangedFilter `dc.filters.RangedFilter`} for the * {@link dc.coordinateGridMixin dc.coordinateGridMixin} charts * * {@link dc.filters.TwoDimensionalFilter `dc.filters.TwoDimensionalFilter`} for the * {@link dc.heatMap heat map} * * {@link dc.filters.RangedTwoDimensionalFilter `dc.filters.RangedTwoDimensionalFilter`} * for the {@link dc.scatterPlot scatter plot} * * `null`: the filter will be reset using the * {@link dc.baseMixin#resetFilterHandler resetFilterHandler} * * Note that this is always a toggle (even when it doesn't make sense for the filter type). If * you wish to replace the current filter, either call `chart.filter(null)` first - or it's more * efficient to call {@link dc.baseMixin#replaceFilter `chart.replaceFilter(filter)`} instead. * * Each toggle is executed by checking if the value is already present using the * {@link dc.baseMixin#hasFilterHandler hasFilterHandler}; if it is not present, it is added * using the {@link dc.baseMixin#addFilterHandler addFilterHandler}; if it is already present, * it is removed using the {@link dc.baseMixin#removeFilterHandler removeFilterHandler}. * * Once the filters array has been updated, the filters are applied to the * crossfilter dimension, using the {@link dc.baseMixin#filterHandler filterHandler}. * * Once you have set the filters, call {@link dc.baseMixin#redrawGroup `chart.redrawGroup()`} * (or {@link dc.redrawAll `dc.redrawAll()`}) to redraw the chart's group. * @method filter * @memberof dc.baseMixin * @instance * @see {@link dc.baseMixin#addFilterHandler addFilterHandler} * @see {@link dc.baseMixin#removeFilterHandler removeFilterHandler} * @see {@link dc.baseMixin#resetFilterHandler resetFilterHandler} * @see {@link dc.baseMixin#filterHandler filterHandler} * @example * // filter by a single string * chart.filter('Sunday'); * // filter by a single age * chart.filter(18); * // filter by a set of states * chart.filter([['MA', 'TX', 'ND', 'WA']]); * // filter by range -- note the use of dc.filters.RangedFilter, which is different * // from the syntax for filtering a crossfilter dimension directly, dimension.filter([15,20]) * chart.filter(dc.filters.RangedFilter(15,20)); * @param {*} [filter] * @returns {dc.baseMixin} */ _chart.filter = function (filter) { if (!arguments.length) { return _filters.length > 0 ? _filters[0] : null; } var filters = _filters; if (filter instanceof Array && filter[0] instanceof Array && !filter.isFiltered) { // toggle each filter filter[0].forEach(function (f) { if (_hasFilterHandler(filters, f)) { filters = _removeFilterHandler(filters, f); } else { filters = _addFilterHandler(filters, f); } }); } else if (filter === null) { filters = _resetFilterHandler(filters); } else { if (_hasFilterHandler(filters, filter)) { filters = _removeFilterHandler(filters, filter); } else { filters = _addFilterHandler(filters, filter); } } _filters = applyFilters(filters); _chart._invokeFilteredListener(filter); if (_root !== null && _chart.hasFilter()) { _chart.turnOnControls(); } else { _chart.turnOffControls(); } return _chart; }; /** * Returns all current filters. This method does not perform defensive cloning of the internal * filter array before returning, therefore any modification of the returned array will effect the * chart's internal filter storage. * @method filters * @memberof dc.baseMixin * @instance * @returns {Array<*>} */ _chart.filters = function () { return _filters; }; _chart.highlightSelected = function (e) { d3.select(e).classed(dc.constants.SELECTED_CLASS, true); d3.select(e).classed(dc.constants.DESELECTED_CLASS, false); }; _chart.fadeDeselected = function (e) { d3.select(e).classed(dc.constants.SELECTED_CLASS, false); d3.select(e).classed(dc.constants.DESELECTED_CLASS, true); }; _chart.resetHighlight = function (e) { d3.select(e).classed(dc.constants.SELECTED_CLASS, false); d3.select(e).classed(dc.constants.DESELECTED_CLASS, false); }; /** * This function is passed to d3 as the onClick handler for each chart. The default behavior is to * filter on the clicked datum (passed to the callback) and redraw the chart group. * @method onClick * @memberof dc.baseMixin * @instance * @param {*} datum */ _chart.onClick = function (datum) { var filter = _chart.keyAccessor()(datum); dc.events.trigger(function () { _chart.filter(filter); _chart.redrawGroup(); }); }; /** * Set or get the filter handler. The filter handler is a function that performs the filter action * on a specific dimension. Using a custom filter handler allows you to perform additional logic * before or after filtering. * @method filterHandler * @memberof dc.baseMixin * @instance * @see {@link https://github.com/crossfilter/crossfilter/wiki/API-Reference#dimension_filter crossfilter.dimension.filter} * @example * // the default filter handler handles all possible cases for the charts in dc.js * // you can replace it with something more specialized for your own chart * chart.filterHandler(function (dimension, filters) { * if (filters.length === 0) { * // the empty case (no filtering) * 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 { * // an array of values, or an array of filter objects * 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; * }); * * // custom filter handler * chart.filterHandler(function(dimension, filter){ * var newFilter = filter + 10; * dimension.filter(newFilter); * return newFilter; // set the actual filter value to the new value * }); * @param {Function} [filterHandler] * @returns {Function|dc.baseMixin} */ _chart.filterHandler = function (filterHandler) { if (!arguments.length) { return _filterHandler; } _filterHandler = filterHandler; return _chart; }; // abstract function stub _chart._doRender = function () { // do nothing in base, should be overridden by sub-function return _chart; }; _chart._doRedraw = function () { // do nothing in base, should be overridden by sub-function return _chart; }; _chart.legendables = function () { // do nothing in base, should be overridden by sub-function return []; }; _chart.legendHighlight = function () { // do nothing in base, should be overridden by sub-function }; _chart.legendReset = function () { // do nothing in base, should be overridden by sub-function }; _chart.legendToggle = function () { // do nothing in base, should be overriden by sub-function }; _chart.isLegendableHidden = function () { // do nothing in base, should be overridden by sub-function return false; }; /** * Set or get the key accessor function. The key accessor function is used to retrieve the key * value from the crossfilter group. Key values are used differently in different charts, for * example keys correspond to slices in a pie chart and x axis positions in a grid coordinate chart. * @method keyAccessor * @memberof dc.baseMixin * @instance * @example * // default key accessor * chart.keyAccessor(function(d) { return d.key; }); * // custom key accessor for a multi-value crossfilter reduction * chart.keyAccessor(function(p) { return p.value.absGain; }); * @param {Function} [keyAccessor] * @returns {Function|dc.baseMixin} */ _chart.keyAccessor = function (keyAccessor) { if (!arguments.length) { return _keyAccessor; } _keyAccessor = keyAccessor; return _chart; }; /** * Set or get the value accessor function. The value accessor function is used to retrieve the * value from the crossfilter group. Group values are used differently in different charts, for * example values correspond to slice sizes in a pie chart and y axis positions in a grid * coordinate chart. * @method valueAccessor * @memberof dc.baseMixin * @instance * @example * // default value accessor * chart.valueAccessor(function(d) { return d.value; }); * // custom value accessor for a multi-value crossfilter reduction * chart.valueAccessor(function(p) { return p.value.percentageGain; }); * @param {Function} [valueAccessor] * @returns {Function|dc.baseMixin} */ _chart.valueAccessor = function (valueAccessor) { if (!arguments.length) { return _valueAccessor; } _valueAccessor = valueAccessor; return _chart; }; /** * Set or get the label function. The chart class will use this function to render labels for each * child element in the chart, e.g. slices in a pie chart or bubbles in a bubble chart. Not every * chart supports the label function, for example line chart does not use this function * at all. By default, enables labels; pass false for the second parameter if this is not desired. * @method label * @memberof dc.baseMixin * @instance * @example * // default label function just return the key * chart.label(function(d) { return d.key; }); * // label function has access to the standard d3 data binding and can get quite complicated * chart.label(function(d) { return d.data.key + '(' + Math.floor(d.data.value / all.value() * 100) + '%)'; }); * @param {Function} [labelFunction] * @param {Boolean} [enableLabels=true] * @returns {Function|dc.baseMixin} */ _chart.label = function (labelFunction, enableLabels) { if (!arguments.length) { return _label; } _label = labelFunction; if ((enableLabels === undefined) || enableLabels) { _renderLabel = true; } return _chart; }; /** * Turn on/off label rendering * @method renderLabel * @memberof dc.baseMixin * @instance * @param {Boolean} [renderLabel=false] * @returns {Boolean|dc.baseMixin} */ _chart.renderLabel = function (renderLabel) { if (!arguments.length) { return _renderLabel; } _renderLabel = renderLabel; return _chart; }; /** * Set or get the title function. The chart class will use this function to render the SVGElement title * (usually interpreted by browser as tooltips) for each child element in the chart, e.g. a slice * in a pie chart or a bubble in a bubble chart. Almost every chart supports the title function; * however in grid coordinate charts you need to turn off the brush in order to see titles, because * otherwise the brush layer will block tooltip triggering. * @method title * @memberof dc.baseMixin * @instance * @example * // default title function shows "key: value" * chart.title(function(d) { return d.key + ': ' + d.value; }); * // title function has access to the standard d3 data binding and can get quite complicated * chart.title(function(p) { * return p.key.getFullYear() * + '\n' * + 'Index Gain: ' + numberFormat(p.value.absGain) + '\n' * + 'Index Gain in Percentage: ' + numberFormat(p.value.percentageGain) + '%\n' * + 'Fluctuation / Index Ratio: ' + numberFormat(p.value.fluctuationPercentage) + '%'; * }); * @param {Function} [titleFunction] * @returns {Function|dc.baseMixin} */ _chart.title = function (titleFunction) { if (!arguments.length) { return _title; } _title = titleFunction; return _chart; }; /** * Turn on/off title rendering, or return the state of the render title flag if no arguments are * given. * @method renderTitle * @memberof dc.baseMixin * @instance * @param {Boolean} [renderTitle=true] * @returns {Boolean|dc.baseMixin} */ _chart.renderTitle = function (renderTitle) { if (!arguments.length) { return _renderTitle; } _renderTitle = renderTitle; return _chart; }; /** * A renderlet is similar to an event listener on rendering event. Multiple renderlets can be added * to an individual chart. Each time a chart is rerendered or redrawn the renderlets are invoked * right after the chart finishes its transitions, giving you a way to modify the SVGElements. * Renderlet functions take the chart instance as the only input parameter and you can * use the dc API or use raw d3 to achieve pretty much any effect. * * Use {@link dc.baseMixin#on on} with a 'renderlet' prefix. * Generates a random key for the renderlet, which makes it hard to remove. * @method renderlet * @memberof dc.baseMixin * @instance * @deprecated * @example * // do this instead of .renderlet(function(chart) { ... }) * chart.on("renderlet", function(chart){ * // mix of dc API and d3 manipulation * chart.select('g.y').style('display', 'none'); * // its a closure so you can also access other chart variable available in the closure scope * moveChart.filter(chart.filter()); * }); * @param {Function} renderletFunction * @returns {dc.baseMixin} */ _chart.renderlet = dc.logger.deprecate(function (renderletFunction) { _chart.on('renderlet.' + dc.utils.uniqueId(), renderletFunction); return _chart; }, 'chart.renderlet has been deprecated. Please use chart.on("renderlet.", renderletFunction)'); /** * Get or set the chart group to which this chart belongs. Chart groups are rendered or redrawn * together since it is expected they share the same underlying crossfilter data set. * @method chartGroup * @memberof dc.baseMixin * @instance * @param {String} [chartGroup] * @returns {String|dc.baseMixin} */ _chart.chartGroup = function (chartGroup) { if (!arguments.length) { return _chartGroup; } if (!_isChild) { dc.deregisterChart(_chart, _chartGroup); } _chartGroup = chartGroup; if (!_isChild) { dc.registerChart(_chart, _chartGroup); } return _chart; }; /** * Expire the internal chart cache. dc charts cache some data internally on a per chart basis to * speed up rendering and avoid unnecessary calculation; however it might be useful to clear the * cache if you have changed state which will affect rendering. For example, if you invoke * {@link https://github.com/crossfilter/crossfilter/wiki/API-Reference#crossfilter_add crossfilter.add} * function or reset group or dimension after rendering, it is a good idea to * clear the cache to make sure charts are rendered properly. * @method expireCache * @memberof dc.baseMixin * @instance * @returns {dc.baseMixin} */ _chart.expireCache = function () { // do nothing in base, should be overridden by sub-function return _chart; }; /** * Attach a dc.legend widget to this chart. The legend widget will automatically draw legend labels * based on the color setting and names associated with each group. * @method legend * @memberof dc.baseMixin * @instance * @example * chart.legend(dc.legend().x(400).y(10).itemHeight(13).gap(5)) * @param {dc.legend} [legend] * @returns {dc.legend|dc.baseMixin} */ _chart.legend = function (legend) { if (!arguments.length) { return _legend; } _legend = legend; _legend.parent(_chart); return _chart; }; /** * Returns the internal numeric ID of the chart. * @method chartID * @memberof dc.baseMixin * @instance * @returns {String} */ _chart.chartID = function () { return _chart.__dcFlag__; }; /** * Set chart options using a configuration object. Each key in the object will cause the method of * the same name to be called with the value to set that attribute for the chart. * @method options * @memberof dc.baseMixin * @instance * @example * chart.options({dimension: myDimension, group: myGroup}); * @param {{}} opts * @returns {dc.baseMixin} */ _chart.options = function (opts) { var applyOptions = [ 'anchor', 'group', 'xAxisLabel', 'yAxisLabel', 'stack', 'title', 'point', 'getColor', 'overlayGeoJson' ]; for (var o in opts) { if (typeof(_chart[o]) === 'function') { if (opts[o] instanceof Array && applyOptions.indexOf(o) !== -1) { _chart[o].apply(_chart, opts[o]); } else { _chart[o].call(_chart, opts[o]); } } else { dc.logger.debug('Not a valid option setter name: ' + o); } } return _chart; }; /** * All dc chart instance supports the following listeners. * Supports the following events: * * `renderlet` - This listener function will be invoked after transitions after redraw and render. Replaces the * deprecated {@link dc.baseMixin#renderlet renderlet} method. * * `pretransition` - Like `.on('renderlet', ...)` but the event is fired before transitions start. * * `preRender` - This listener function will be invoked before chart rendering. * * `postRender` - This listener function will be invoked after chart finish rendering including * all renderlets' logic. * * `preRedraw` - This listener function will be invoked before chart redrawing. * * `postRedraw` - This listener function will be invoked after chart finish redrawing * including all renderlets' logic. * * `filtered` - This listener function will be invoked after a filter is applied, added or removed. * * `zoomed` - This listener function will be invoked after a zoom is triggered. * @method on * @memberof dc.baseMixin * @instance * @see {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Internals.md#dispatch_on d3.dispatch.on} * @example * .on('renderlet', function(chart, filter){...}) * .on('pretransition', function(chart, filter){...}) * .on('preRender', function(chart){...}) * .on('postRender', function(chart){...}) * .on('preRedraw', function(chart){...}) * .on('postRedraw', function(chart){...}) * .on('filtered', function(chart, filter){...}) * .on('zoomed', function(chart, filter){...}) * @param {String} event * @param {Function} listener * @returns {dc.baseMixin} */ _chart.on = function (event, listener) { _listeners.on(event, listener); return _chart; }; return _chart; }; /** * Margin is a mixin that provides margin utility functions for both the Row Chart and Coordinate Grid * Charts. * @name marginMixin * @memberof dc * @mixin * @param {Object} _chart * @returns {dc.marginMixin} */ dc.marginMixin = function (_chart) { var _margin = {top: 10, right: 50, bottom: 30, left: 30}; /** * Get or set the margins for a particular coordinate grid chart instance. The margins is stored as * an associative Javascript array. * @method margins * @memberof dc.marginMixin * @instance * @example * var leftMargin = chart.margins().left; // 30 by default * chart.margins().left = 50; * leftMargin = chart.margins().left; // now 50 * @param {{top: Number, right: Number, left: Number, bottom: Number}} [margins={top: 10, right: 50, bottom: 30, left: 30}] * @returns {{top: Number, right: Number, left: Number, bottom: Number}|dc.marginMixin} */ _chart.margins = function (margins) { if (!arguments.length) { return _margin; } _margin = margins; return _chart; }; _chart.effectiveWidth = function () { return _chart.width() - _chart.margins().left - _chart.margins().right; }; _chart.effectiveHeight = function () { return _chart.height() - _chart.margins().top - _chart.margins().bottom; }; return _chart; }; /** * The Color Mixin is an abstract chart functional class providing universal coloring support * as a mix-in for any concrete chart implementation. * @name colorMixin * @memberof dc * @mixin * @param {Object} _chart * @returns {dc.colorMixin} */ dc.colorMixin = function (_chart) { var _colors = d3.scale.category20c(); var _defaultAccessor = true; var _colorAccessor = function (d) { return _chart.keyAccessor()(d); }; /** * Retrieve current color scale or set a new color scale. This methods accepts any function that * operates like a d3 scale. * @method colors * @memberof dc.colorMixin * @instance * @see {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Scales.md d3.scale} * @example * // alternate categorical scale * chart.colors(d3.scale.category20b()); * // ordinal scale * chart.colors(d3.scale.ordinal().range(['red','green','blue'])); * // convenience method, the same as above * chart.ordinalColors(['red','green','blue']); * // set a linear scale * chart.linearColors(["#4575b4", "#ffffbf", "#a50026"]); * @param {d3.scale} [colorScale=d3.scale.category20c()] * @returns {d3.scale|dc.colorMixin} */ _chart.colors = function (colorScale) { if (!arguments.length) { return _colors; } if (colorScale instanceof Array) { _colors = d3.scale.quantize().range(colorScale); // deprecated legacy support, note: this fails for ordinal domains } else { _colors = d3.functor(colorScale); } return _chart; }; /** * Convenience method to set the color scale to * {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Ordinal-Scales.md#ordinal d3.scale.ordinal} with * range `r`. * @method ordinalColors * @memberof dc.colorMixin * @instance * @param {Array} r * @returns {dc.colorMixin} */ _chart.ordinalColors = function (r) { return _chart.colors(d3.scale.ordinal().range(r)); }; /** * Convenience method to set the color scale to an Hcl interpolated linear scale with range `r`. * @method linearColors * @memberof dc.colorMixin * @instance * @param {Array} r * @returns {dc.colorMixin} */ _chart.linearColors = function (r) { return _chart.colors(d3.scale.linear() .range(r) .interpolate(d3.interpolateHcl)); }; /** * Set or the get color accessor function. This function will be used to map a data point in a * crossfilter group to a color value on the color scale. The default function uses the key * accessor. * @method colorAccessor * @memberof dc.colorMixin * @instance * @example * // default index based color accessor * .colorAccessor(function (d, i){return i;}) * // color accessor for a multi-value crossfilter reduction * .colorAccessor(function (d){return d.value.absGain;}) * @param {Function} [colorAccessor] * @returns {Function|dc.colorMixin} */ _chart.colorAccessor = function (colorAccessor) { if (!arguments.length) { return _colorAccessor; } _colorAccessor = colorAccessor; _defaultAccessor = false; return _chart; }; // what is this? _chart.defaultColorAccessor = function () { return _defaultAccessor; }; /** * Set or get the current domain for the color mapping function. The domain must be supplied as an * array. * * Note: previously this method accepted a callback function. Instead you may use a custom scale * set by {@link dc.colorMixin#colors .colors}. * @method colorDomain * @memberof dc.colorMixin * @instance * @param {Array} [domain] * @returns {Array|dc.colorMixin} */ _chart.colorDomain = function (domain) { if (!arguments.length) { return _colors.domain(); } _colors.domain(domain); return _chart; }; /** * Set the domain by determining the min and max values as retrieved by * {@link dc.colorMixin#colorAccessor .colorAccessor} over the chart's dataset. * @method calculateColorDomain * @memberof dc.colorMixin * @instance * @returns {dc.colorMixin} */ _chart.calculateColorDomain = function () { var newDomain = [d3.min(_chart.data(), _chart.colorAccessor()), d3.max(_chart.data(), _chart.colorAccessor())]; _colors.domain(newDomain); return _chart; }; /** * Get the color for the datum d and counter i. This is used internally by charts to retrieve a color. * @method getColor * @memberof dc.colorMixin * @instance * @param {*} d * @param {Number} [i] * @returns {String} */ _chart.getColor = function (d, i) { return _colors(_colorAccessor.call(this, d, i)); }; /** * **Deprecated.** Get/set the color calculator. This actually replaces the * {@link dc.colorMixin#getColor getColor} method! * * This is not recommended, since using a {@link dc.colorMixin#colorAccessor colorAccessor} and * color scale ({@link dc.colorMixin#colors .colors}) is more powerful and idiomatic d3. * @method colorCalculator * @memberof dc.colorMixin * @instance * @param {*} [colorCalculator] * @returns {Function|dc.colorMixin} */ _chart.colorCalculator = dc.logger.deprecate(function (colorCalculator) { if (!arguments.length) { return _chart.getColor; } _chart.getColor = colorCalculator; return _chart; }, 'colorMixin.colorCalculator has been deprecated. Please colorMixin.colors and colorMixin.colorAccessor instead'); return _chart; }; /** * Coordinate Grid is an abstract base chart designed to support a number of coordinate grid based * concrete chart types, e.g. bar chart, line chart, and bubble chart. * @name coordinateGridMixin * @memberof dc * @mixin * @mixes dc.colorMixin * @mixes dc.marginMixin * @mixes dc.baseMixin * @param {Object} _chart * @returns {dc.coordinateGridMixin} */ dc.coordinateGridMixin = function (_chart) { var GRID_LINE_CLASS = 'grid-line'; var HORIZONTAL_CLASS = 'horizontal'; var VERTICAL_CLASS = 'vertical'; var Y_AXIS_LABEL_CLASS = 'y-axis-label'; var X_AXIS_LABEL_CLASS = 'x-axis-label'; var DEFAULT_AXIS_LABEL_PADDING = 12; _chart = dc.colorMixin(dc.marginMixin(dc.baseMixin(_chart))); _chart.colors(d3.scale.category10()); _chart._mandatoryAttributes().push('x'); var _parent; var _g; var _chartBodyG; var _x; var _xOriginalDomain; var _xAxis = d3.svg.axis().orient('bottom'); var _xUnits = dc.units.integers; var _xAxisPadding = 0; var _xAxisPaddingUnit = 'day'; var _xElasticity = false; var _xAxisLabel; var _xAxisLabelPadding = 0; var _lastXDomain; var _y; var _yAxis = d3.svg.axis().orient('left'); var _yAxisPadding = 0; var _yElasticity = false; var _yAxisLabel; var _yAxisLabelPadding = 0; var _brush = d3.svg.brush(); var _brushOn = true; var _round; var _renderHorizontalGridLine = false; var _renderVerticalGridLine = false; var _refocused = false, _resizing = false; var _unitCount; var _zoomScale = [1, Infinity]; var _zoomOutRestrict = true; var _zoom = d3.behavior.zoom().on('zoom', zoomHandler); var _nullZoom = d3.behavior.zoom().on('zoom', null); var _hasBeenMouseZoomable = false; var _rangeChart; var _focusChart; var _mouseZoomable = false; var _clipPadding = 0; var _outerRangeBandPadding = 0.5; var _rangeBandPadding = 0; var _useRightYAxis = false; /** * When changing the domain of the x or y scale, it is necessary to tell the chart to recalculate * and redraw the axes. (`.rescale()` is called automatically when the x or y scale is replaced * with {@link dc.coordinateGridMixin+x .x()} or {@link dc.coordinateGridMixin#y .y()}, and has * no effect on elastic scales.) * @method rescale * @memberof dc.coordinateGridMixin * @instance * @returns {dc.coordinateGridMixin} */ _chart.rescale = function () { _unitCount = undefined; _resizing = true; return _chart; }; _chart.resizing = function () { return _resizing; }; /** * Get or set the range selection chart associated with this instance. Setting the range selection * chart using this function will automatically update its selection brush when the current chart * zooms in. In return the given range chart will also automatically attach this chart as its focus * chart hence zoom in when range brush updates. * * Usually the range and focus charts will share a dimension. The range chart will set the zoom * boundaries for the focus chart, so its dimension values must be compatible with the domain of * the focus chart. * * See the [Nasdaq 100 Index](http://dc-js.github.com/dc.js/) example for this effect in action. * @method rangeChart * @memberof dc.coordinateGridMixin * @instance * @param {dc.coordinateGridMixin} [rangeChart] * @returns {dc.coordinateGridMixin} */ _chart.rangeChart = function (rangeChart) { if (!arguments.length) { return _rangeChart; } _rangeChart = rangeChart; _rangeChart.focusChart(_chart); return _chart; }; /** * Get or set the scale extent for mouse zooms. * @method zoomScale * @memberof dc.coordinateGridMixin * @instance * @param {Array} [extent=[1, Infinity]] * @returns {Array|dc.coordinateGridMixin} */ _chart.zoomScale = function (extent) { if (!arguments.length) { return _zoomScale; } _zoomScale = extent; return _chart; }; /** * Get or set the zoom restriction for the chart. If true limits the zoom to origional domain of the chart. * @method zoomOutRestrict * @memberof dc.coordinateGridMixin * @instance * @param {Boolean} [zoomOutRestrict=true] * @returns {Boolean|dc.coordinateGridMixin} */ _chart.zoomOutRestrict = function (zoomOutRestrict) { if (!arguments.length) { return _zoomOutRestrict; } _zoomScale[0] = zoomOutRestrict ? 1 : 0; _zoomOutRestrict = zoomOutRestrict; return _chart; }; _chart._generateG = function (parent) { if (parent === undefined) { _parent = _chart.svg(); } else { _parent = parent; } var href = window.location.href.split('#')[0]; _g = _parent.append('g'); _chartBodyG = _g.append('g').attr('class', 'chart-body') .attr('transform', 'translate(' + _chart.margins().left + ', ' + _chart.margins().top + ')') .attr('clip-path', 'url(' + href + '#' + getClipPathId() + ')'); return _g; }; /** * Get or set the root g element. This method is usually used to retrieve the g element in order to * overlay custom svg drawing programatically. **Caution**: The root g element is usually generated * by dc.js internals, and resetting it might produce unpredictable result. * @method g * @memberof dc.coordinateGridMixin * @instance * @param {SVGElement} [gElement] * @returns {SVGElement|dc.coordinateGridMixin} */ _chart.g = function (gElement) { if (!arguments.length) { return _g; } _g = gElement; return _chart; }; /** * Set or get mouse zoom capability flag (default: false). When turned on the chart will be * zoomable using the mouse wheel. If the range selector chart is attached zooming will also update * the range selection brush on the associated range selector chart. * @method mouseZoomable * @memberof dc.coordinateGridMixin * @instance * @param {Boolean} [mouseZoomable=false] * @returns {Boolean|dc.coordinateGridMixin} */ _chart.mouseZoomable = function (mouseZoomable) { if (!arguments.length) { return _mouseZoomable; } _mouseZoomable = mouseZoomable; return _chart; }; /** * Retrieve the svg group for the chart body. * @method chartBodyG * @memberof dc.coordinateGridMixin * @instance * @param {SVGElement} [chartBodyG] * @returns {SVGElement} */ _chart.chartBodyG = function (chartBodyG) { if (!arguments.length) { return _chartBodyG; } _chartBodyG = chartBodyG; return _chart; }; /** * **mandatory** * * Get or set the x scale. The x scale can be any d3 * {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Quantitative-Scales.md quantitive scale} or * {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Ordinal-Scales.md ordinal scale}. * @method x * @memberof dc.coordinateGridMixin * @instance * @see {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Scales.md d3.scale} * @example * // set x to a linear scale * chart.x(d3.scale.linear().domain([-2500, 2500])) * // set x to a time scale to generate histogram * chart.x(d3.time.scale().domain([new Date(1985, 0, 1), new Date(2012, 11, 31)])) * @param {d3.scale} [xScale] * @returns {d3.scale|dc.coordinateGridMixin} */ _chart.x = function (xScale) { if (!arguments.length) { return _x; } _x = xScale; _xOriginalDomain = _x.domain(); _chart.rescale(); return _chart; }; _chart.xOriginalDomain = function () { return _xOriginalDomain; }; /** * Set or get the xUnits function. The coordinate grid chart uses the xUnits function to calculate * the number of data projections on x axis such as the number of bars for a bar chart or the * number of dots for a line chart. This function is expected to return a Javascript array of all * data points on x axis, or the number of points on the axis. [d3 time range functions * d3.time.days, d3.time.months, and * d3.time.years](https://github.com/d3/d3-3.x-api-reference/blob/master/Time-Intervals.md#aliases) are all valid xUnits * function. dc.js also provides a few units function, see the {@link dc.units Units Namespace} for * a list of built-in units functions. * @method xUnits * @memberof dc.coordinateGridMixin * @instance * @todo Add docs for utilities * @example * // set x units to count days * chart.xUnits(d3.time.days); * // set x units to count months * chart.xUnits(d3.time.months); * * // A custom xUnits function can be used as long as it follows the following interface: * // units in integer * function(start, end, xDomain) { * // simply calculates how many integers in the domain * return Math.abs(end - start); * }; * * // fixed units * function(start, end, xDomain) { * // be aware using fixed units will disable the focus/zoom ability on the chart * return 1000; * @param {Function} [xUnits=dc.units.integers] * @returns {Function|dc.coordinateGridMixin} */ _chart.xUnits = function (xUnits) { if (!arguments.length) { return _xUnits; } _xUnits = xUnits; return _chart; }; /** * Set or get the x axis used by a particular coordinate grid chart instance. This function is most * useful when x axis customization is required. The x axis in dc.js is an instance of a * {@link https://github.com/d3/d3-3.x-api-reference/blob/master/SVG-Axes.md#axis d3 axis object}; * therefore it supports any valid d3 axis manipulation. * * **Caution**: The x axis is usually generated internally by dc; resetting it may cause * unexpected results. Note also that when used as a getter, this function is not chainable: * it returns the axis, not the chart, * {@link https://github.com/dc-js/dc.js/wiki/FAQ#why-does-everything-break-after-a-call-to-xaxis-or-yaxis * so attempting to call chart functions after calling `.xAxis()` will fail}. * @method xAxis * @memberof dc.coordinateGridMixin * @instance * @see {@link https://github.com/d3/d3-3.x-api-reference/blob/master/SVG-Axes.md#axis d3.svg.axis} * @example * // customize x axis tick format * chart.xAxis().tickFormat(function(v) {return v + '%';}); * // customize x axis tick values * chart.xAxis().tickValues([0, 100, 200, 300]); * @param {d3.svg.axis} [xAxis=d3.svg.axis().orient('bottom')] * @returns {d3.svg.axis|dc.coordinateGridMixin} */ _chart.xAxis = function (xAxis) { if (!arguments.length) { return _xAxis; } _xAxis = xAxis; return _chart; }; /** * Turn on/off elastic x axis behavior. If x axis elasticity is turned on, then the grid chart will * attempt to recalculate the x axis range whenever a redraw event is triggered. * @method elasticX * @memberof dc.coordinateGridMixin * @instance * @param {Boolean} [elasticX=false] * @returns {Boolean|dc.coordinateGridMixin} */ _chart.elasticX = function (elasticX) { if (!arguments.length) { return _xElasticity; } _xElasticity = elasticX; return _chart; }; /** * Set or get x axis padding for the elastic x axis. The padding will be added to both end of the x * axis if elasticX is turned on; otherwise it is ignored. * * Padding can be an integer or percentage in string (e.g. '10%'). Padding can be applied to * number or date x axes. When padding a date axis, an integer represents number of units being padded * and a percentage string will be treated the same as an integer. The unit will be determined by the * xAxisPaddingUnit variable. * @method xAxisPadding * @memberof dc.coordinateGridMixin * @instance * @param {Number|String} [padding=0] * @returns {Number|String|dc.coordinateGridMixin} */ _chart.xAxisPadding = function (padding) { if (!arguments.length) { return _xAxisPadding; } _xAxisPadding = padding; return _chart; }; /** * Set or get x axis padding unit for the elastic x axis. The padding unit will determine which unit to * use when applying xAxis padding if elasticX is turned on and if x-axis uses a time dimension; * otherwise it is ignored. * * Padding unit is a string that will be used when the padding is calculated. Available parameters are * the available d3 time intervals; see * {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Time-Intervals.md#interval d3.time.interval}. * @method xAxisPaddingUnit * @memberof dc.coordinateGridMixin * @instance * @param {String} [unit='days'] * @returns {String|dc.coordinateGridMixin} */ _chart.xAxisPaddingUnit = function (unit) { if (!arguments.length) { return _xAxisPaddingUnit; } _xAxisPaddingUnit = unit; return _chart; }; /** * Returns the number of units displayed on the x axis using the unit measure configured by * {@link dc.coordinateGridMixin#xUnits xUnits}. * @method xUnitCount * @memberof dc.coordinateGridMixin * @instance * @returns {Number} */ _chart.xUnitCount = function () { if (_unitCount === undefined) { var units = _chart.xUnits()(_chart.x().domain()[0], _chart.x().domain()[1], _chart.x().domain()); if (units instanceof Array) { _unitCount = units.length; } else { _unitCount = units; } } return _unitCount; }; /** * Gets or sets whether the chart should be drawn with a right axis instead of a left axis. When * used with a chart in a composite chart, allows both left and right Y axes to be shown on a * chart. * @method useRightYAxis * @memberof dc.coordinateGridMixin * @instance * @param {Boolean} [useRightYAxis=false] * @returns {Boolean|dc.coordinateGridMixin} */ _chart.useRightYAxis = function (useRightYAxis) { if (!arguments.length) { return _useRightYAxis; } _useRightYAxis = useRightYAxis; return _chart; }; /** * Returns true if the chart is using ordinal xUnits ({@link dc.units.ordinal dc.units.ordinal}, or false * otherwise. Most charts behave differently with ordinal data and use the result of this method to * trigger the appropriate logic. * @method isOrdinal * @memberof dc.coordinateGridMixin * @instance * @returns {Boolean} */ _chart.isOrdinal = function () { return _chart.xUnits() === dc.units.ordinal; }; _chart._useOuterPadding = function () { return true; }; _chart._ordinalXDomain = function () { var groups = _chart._computeOrderedGroups(_chart.data()); return groups.map(_chart.keyAccessor()); }; function compareDomains (d1, d2) { return !d1 || !d2 || d1.length !== d2.length || d1.some(function (elem, i) { return (elem && d2[i]) ? elem.toString() !== d2[i].toString() : elem === d2[i]; }); } function prepareXAxis (g, render) { if (!_chart.isOrdinal()) { if (_chart.elasticX()) { _x.domain([_chart.xAxisMin(), _chart.xAxisMax()]); } } else { // _chart.isOrdinal() if (_chart.elasticX() || _x.domain().length === 0) { _x.domain(_chart._ordinalXDomain()); } } // has the domain changed? var xdom = _x.domain(); if (render || compareDomains(_lastXDomain, xdom)) { _chart.rescale(); } _lastXDomain = xdom; // please can't we always use rangeBands for bar charts? if (_chart.isOrdinal()) { _x.rangeBands([0, _chart.xAxisLength()], _rangeBandPadding, _chart._useOuterPadding() ? _outerRangeBandPadding : 0); } else { _x.range([0, _chart.xAxisLength()]); } _xAxis = _xAxis.scale(_chart.x()); renderVerticalGridLines(g); } _chart.renderXAxis = function (g) { var axisXG = g.select('g.x'); if (axisXG.empty()) { axisXG = g.append('g') .attr('class', 'axis x') .attr('transform', 'translate(' + _chart.margins().left + ',' + _chart._xAxisY() + ')'); } var axisXLab = g.select('text.' + X_AXIS_LABEL_CLASS); if (axisXLab.empty() && _chart.xAxisLabel()) { axisXLab = g.append('text') .attr('class', X_AXIS_LABEL_CLASS) .attr('transform', 'translate(' + (_chart.margins().left + _chart.xAxisLength() / 2) + ',' + (_chart.height() - _xAxisLabelPadding) + ')') .attr('text-anchor', 'middle'); } if (_chart.xAxisLabel() && axisXLab.text() !== _chart.xAxisLabel()) { axisXLab.text(_chart.xAxisLabel()); } dc.transition(axisXG, _chart.transitionDuration(), _chart.transitionDelay()) .attr('transform', 'translate(' + _chart.margins().left + ',' + _chart._xAxisY() + ')') .call(_xAxis); dc.transition(axisXLab, _chart.transitionDuration(), _chart.transitionDelay()) .attr('transform', 'translate(' + (_chart.margins().left + _chart.xAxisLength() / 2) + ',' + (_chart.height() - _xAxisLabelPadding) + ')'); }; function renderVerticalGridLines (g) { var gridLineG = g.select('g.' + VERTICAL_CLASS); if (_renderVerticalGridLine) { if (gridLineG.empty()) { gridLineG = g.insert('g', ':first-child') .attr('class', GRID_LINE_CLASS + ' ' + VERTICAL_CLASS) .attr('transform', 'translate(' + _chart.margins().left + ',' + _chart.margins().top + ')'); } var ticks = _xAxis.tickValues() ? _xAxis.tickValues() : (typeof _x.ticks === 'function' ? _x.ticks(_xAxis.ticks()[0]) : _x.domain()); var lines = gridLineG.selectAll('line') .data(ticks); // enter var linesGEnter = lines.enter() .append('line') .attr('x1', function (d) { return _x(d); }) .attr('y1', _chart._xAxisY() - _chart.margins().top) .attr('x2', function (d) { return _x(d); }) .attr('y2', 0) .attr('opacity', 0); dc.transition(linesGEnter, _chart.transitionDuration(), _chart.transitionDelay()) .attr('opacity', 1); // update dc.transition(lines, _chart.transitionDuration(), _chart.transitionDelay()) .attr('x1', function (d) { return _x(d); }) .attr('y1', _chart._xAxisY() - _chart.margins().top) .attr('x2', function (d) { return _x(d); }) .attr('y2', 0); // exit lines.exit().remove(); } else { gridLineG.selectAll('line').remove(); } } _chart._xAxisY = function () { return (_chart.height() - _chart.margins().bottom); }; _chart.xAxisLength = function () { return _chart.effectiveWidth(); }; /** * Set or get the x axis label. If setting the label, you may optionally include additional padding to * the margin to make room for the label. By default the padded is set to 12 to accomodate the text height. * @method xAxisLabel * @memberof dc.coordinateGridMixin * @instance * @param {String} [labelText] * @param {Number} [padding=12] * @returns {String} */ _chart.xAxisLabel = function (labelText, padding) { if (!arguments.length) { return _xAxisLabel; } _xAxisLabel = labelText; _chart.margins().bottom -= _xAxisLabelPadding; _xAxisLabelPadding = (padding === undefined) ? DEFAULT_AXIS_LABEL_PADDING : padding; _chart.margins().bottom += _xAxisLabelPadding; return _chart; }; _chart._prepareYAxis = function (g) { if (_y === undefined || _chart.elasticY()) { if (_y === undefined) { _y = d3.scale.linear(); } var min = _chart.yAxisMin() || 0, max = _chart.yAxisMax() || 0; _y.domain([min, max]).rangeRound([_chart.yAxisHeight(), 0]); } _y.range([_chart.yAxisHeight(), 0]); _yAxis = _yAxis.scale(_y); if (_useRightYAxis) { _yAxis.orient('right'); } _chart._renderHorizontalGridLinesForAxis(g, _y, _yAxis); }; _chart.renderYAxisLabel = function (axisClass, text, rotation, labelXPosition) { labelXPosition = labelXPosition || _yAxisLabelPadding; var axisYLab = _chart.g().select('text.' + Y_AXIS_LABEL_CLASS + '.' + axisClass + '-label'); var labelYPosition = (_chart.margins().top + _chart.yAxisHeight() / 2); if (axisYLab.empty() && text) { axisYLab = _chart.g().append('text') .attr('transform', 'translate(' + labelXPosition + ',' + labelYPosition + '),rotate(' + rotation + ')') .attr('class', Y_AXIS_LABEL_CLASS + ' ' + axisClass + '-label') .attr('text-anchor', 'middle') .text(text); } if (text && axisYLab.text() !== text) { axisYLab.text(text); } dc.transition(axisYLab, _chart.transitionDuration(), _chart.transitionDelay()) .attr('transform', 'translate(' + labelXPosition + ',' + labelYPosition + '),rotate(' + rotation + ')'); }; _chart.renderYAxisAt = function (axisClass, axis, position) { var axisYG = _chart.g().select('g.' + axisClass); if (axisYG.empty()) { axisYG = _chart.g().append('g') .attr('class', 'axis ' + axisClass) .attr('transform', 'translate(' + position + ',' + _chart.margins().top + ')'); } dc.transition(axisYG, _chart.transitionDuration(), _chart.transitionDelay()) .attr('transform', 'translate(' + position + ',' + _chart.margins().top + ')') .call(axis); }; _chart.renderYAxis = function () { var axisPosition = _useRightYAxis ? (_chart.width() - _chart.margins().right) : _chart._yAxisX(); _chart.renderYAxisAt('y', _yAxis, axisPosition); var labelPosition = _useRightYAxis ? (_chart.width() - _yAxisLabelPadding) : _yAxisLabelPadding; var rotation = _useRightYAxis ? 90 : -90; _chart.renderYAxisLabel('y', _chart.yAxisLabel(), rotation, labelPosition); }; _chart._renderHorizontalGridLinesForAxis = function (g, scale, axis) { var gridLineG = g.select('g.' + HORIZONTAL_CLASS); if (_renderHorizontalGridLine) { var ticks = axis.tickValues() ? axis.tickValues() : scale.ticks(axis.ticks()[0]); if (gridLineG.empty()) { gridLineG = g.insert('g', ':first-child') .attr('class', GRID_LINE_CLASS + ' ' + HORIZONTAL_CLASS) .attr('transform', 'translate(' + _chart.margins().left + ',' + _chart.margins().top + ')'); } var lines = gridLineG.selectAll('line') .data(ticks); // enter var linesGEnter = lines.enter() .append('line') .attr('x1', 1) .attr('y1', function (d) { return scale(d); }) .attr('x2', _chart.xAxisLength()) .attr('y2', function (d) { return scale(d); }) .attr('opacity', 0); dc.transition(linesGEnter, _chart.transitionDuration(), _chart.transitionDelay()) .attr('opacity', 1); // update dc.transition(lines, _chart.transitionDuration(), _chart.transitionDelay()) .attr('x1', 1) .attr('y1', function (d) { return scale(d); }) .attr('x2', _chart.xAxisLength()) .attr('y2', function (d) { return scale(d); }); // exit lines.exit().remove(); } else { gridLineG.selectAll('line').remove(); } }; _chart._yAxisX = function () { return _chart.useRightYAxis() ? _chart.width() - _chart.margins().right : _chart.margins().left; }; /** * Set or get the y axis label. If setting the label, you may optionally include additional padding * to the margin to make room for the label. By default the padding is set to 12 to accommodate the * text height. * @method yAxisLabel * @memberof dc.coordinateGridMixin * @instance * @param {String} [labelText] * @param {Number} [padding=12] * @returns {String|dc.coordinateGridMixin} */ _chart.yAxisLabel = function (labelText, padding) { if (!arguments.length) { return _yAxisLabel; } _yAxisLabel = labelText; _chart.margins().left -= _yAxisLabelPadding; _yAxisLabelPadding = (padding === undefined) ? DEFAULT_AXIS_LABEL_PADDING : padding; _chart.margins().left += _yAxisLabelPadding; return _chart; }; /** * Get or set the y scale. The y scale is typically automatically determined by the chart implementation. * @method y * @memberof dc.coordinateGridMixin * @instance * @see {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Scales.md d3.scale} * @param {d3.scale} [yScale] * @returns {d3.scale|dc.coordinateGridMixin} */ _chart.y = function (yScale) { if (!arguments.length) { return _y; } _y = yScale; _chart.rescale(); return _chart; }; /** * Set or get the y axis used by the coordinate grid chart instance. This function is most useful * when y axis customization is required. The y axis in dc.js is simply an instance of a [d3 axis * object](https://github.com/d3/d3-3.x-api-reference/blob/master/SVG-Axes.md#axis); therefore it supports any * valid d3 axis manipulation. * * **Caution**: The y axis is usually generated internally by dc; resetting it may cause * unexpected results. Note also that when used as a getter, this function is not chainable: it * returns the axis, not the chart, * {@link https://github.com/dc-js/dc.js/wiki/FAQ#why-does-everything-break-after-a-call-to-xaxis-or-yaxis * so attempting to call chart functions after calling `.yAxis()` will fail}. * @method yAxis * @memberof dc.coordinateGridMixin * @instance * @see {@link https://github.com/d3/d3-3.x-api-reference/blob/master/SVG-Axes.md#axis d3.svg.axis} * @example * // customize y axis tick format * chart.yAxis().tickFormat(function(v) {return v + '%';}); * // customize y axis tick values * chart.yAxis().tickValues([0, 100, 200, 300]); * @param {d3.svg.axis} [yAxis=d3.svg.axis().orient('left')] * @returns {d3.svg.axis|dc.coordinateGridMixin} */ _chart.yAxis = function (yAxis) { if (!arguments.length) { return _yAxis; } _yAxis = yAxis; return _chart; }; /** * Turn on/off elastic y axis behavior. If y axis elasticity is turned on, then the grid chart will * attempt to recalculate the y axis range whenever a redraw event is triggered. * @method elasticY * @memberof dc.coordinateGridMixin * @instance * @param {Boolean} [elasticY=false] * @returns {Boolean|dc.coordinateGridMixin} */ _chart.elasticY = function (elasticY) { if (!arguments.length) { return _yElasticity; } _yElasticity = elasticY; return _chart; }; /** * Turn on/off horizontal grid lines. * @method renderHorizontalGridLines * @memberof dc.coordinateGridMixin * @instance * @param {Boolean} [renderHorizontalGridLines=false] * @returns {Boolean|dc.coordinateGridMixin} */ _chart.renderHorizontalGridLines = function (renderHorizontalGridLines) { if (!arguments.length) { return _renderHorizontalGridLine; } _renderHorizontalGridLine = renderHorizontalGridLines; return _chart; }; /** * Turn on/off vertical grid lines. * @method renderVerticalGridLines * @memberof dc.coordinateGridMixin * @instance * @param {Boolean} [renderVerticalGridLines=false] * @returns {Boolean|dc.coordinateGridMixin} */ _chart.renderVerticalGridLines = function (renderVerticalGridLines) { if (!arguments.length) { return _renderVerticalGridLine; } _renderVerticalGridLine = renderVerticalGridLines; return _chart; }; /** * Calculates the minimum x value to display in the chart. Includes xAxisPadding if set. * @method xAxisMin * @memberof dc.coordinateGridMixin * @instance * @returns {*} */ _chart.xAxisMin = function () { var min = d3.min(_chart.data(), function (e) { return _chart.keyAccessor()(e); }); return dc.utils.subtract(min, _xAxisPadding, _xAxisPaddingUnit); }; /** * Calculates the maximum x value to display in the chart. Includes xAxisPadding if set. * @method xAxisMax * @memberof dc.coordinateGridMixin * @instance * @returns {*} */ _chart.xAxisMax = function () { var max = d3.max(_chart.data(), function (e) { return _chart.keyAccessor()(e); }); return dc.utils.add(max, _xAxisPadding, _xAxisPaddingUnit); }; /** * Calculates the minimum y value to display in the chart. Includes yAxisPadding if set. * @method yAxisMin * @memberof dc.coordinateGridMixin * @instance * @returns {*} */ _chart.yAxisMin = function () { var min = d3.min(_chart.data(), function (e) { return _chart.valueAccessor()(e); }); return dc.utils.subtract(min, _yAxisPadding); }; /** * Calculates the maximum y value to display in the chart. Includes yAxisPadding if set. * @method yAxisMax * @memberof dc.coordinateGridMixin * @instance * @returns {*} */ _chart.yAxisMax = function () { var max = d3.max(_chart.data(), function (e) { return _chart.valueAccessor()(e); }); return dc.utils.add(max, _yAxisPadding); }; /** * Set or get y axis padding for the elastic y axis. The padding will be added to the top and * bottom of the y axis if elasticY is turned on; otherwise it is ignored. * * Padding can be an integer or percentage in string (e.g. '10%'). Padding can be applied to * number or date axes. When padding a date axis, an integer represents number of days being padded * and a percentage string will be treated the same as an integer. * @method yAxisPadding * @memberof dc.coordinateGridMixin * @instance * @param {Number|String} [padding=0] * @returns {Number|dc.coordinateGridMixin} */ _chart.yAxisPadding = function (padding) { if (!arguments.length) { return _yAxisPadding; } _yAxisPadding = padding; return _chart; }; _chart.yAxisHeight = function () { return _chart.effectiveHeight(); }; /** * Set or get the rounding function used to quantize the selection when brushing is enabled. * @method round * @memberof dc.coordinateGridMixin * @instance * @example * // set x unit round to by month, this will make sure range selection brush will * // select whole months * chart.round(d3.time.month.round); * @param {Function} [round] * @returns {Function|dc.coordinateGridMixin} */ _chart.round = function (round) { if (!arguments.length) { return _round; } _round = round; return _chart; }; _chart._rangeBandPadding = function (_) { if (!arguments.length) { return _rangeBandPadding; } _rangeBandPadding = _; return _chart; }; _chart._outerRangeBandPadding = function (_) { if (!arguments.length) { return _outerRangeBandPadding; } _outerRangeBandPadding = _; return _chart; }; dc.override(_chart, 'filter', function (_) { if (!arguments.length) { return _chart._filter(); } _chart._filter(_); if (_) { _chart.brush().extent(_); } else { _chart.brush().clear(); } return _chart; }); _chart.brush = function (_) { if (!arguments.length) { return _brush; } _brush = _; return _chart; }; function brushHeight () { return _chart._xAxisY() - _chart.margins().top; } _chart.renderBrush = function (g) { if (_brushOn) { _brush.on('brush', _chart._brushing); _brush.on('brushstart', _chart._disableMouseZoom); _brush.on('brushend', configureMouseZoom); var gBrush = g.append('g') .attr('class', 'brush') .attr('transform', 'translate(' + _chart.margins().left + ',' + _chart.margins().top + ')') .call(_brush.x(_chart.x())); _chart.setBrushY(gBrush, false); _chart.setHandlePaths(gBrush); if (_chart.hasFilter()) { _chart.redrawBrush(g, false); } } }; _chart.setHandlePaths = function (gBrush) { gBrush.selectAll('.resize').append('path').attr('d', _chart.resizeHandlePath); }; _chart.setBrushY = function (gBrush) { gBrush.selectAll('rect') .attr('height', brushHeight()); gBrush.selectAll('.resize path') .attr('d', _chart.resizeHandlePath); }; _chart.extendBrush = function () { var extent = _brush.extent(); if (_chart.round()) { extent[0] = extent.map(_chart.round())[0]; extent[1] = extent.map(_chart.round())[1]; _g.select('.brush') .call(_brush.extent(extent)); } return extent; }; _chart.brushIsEmpty = function (extent) { return _brush.empty() || !extent || extent[1] <= extent[0]; }; _chart._brushing = function () { var extent = _chart.extendBrush(); _chart.redrawBrush(_g, false); if (_chart.brushIsEmpty(extent)) { dc.events.trigger(function () { _chart.filter(null); _chart.redrawGroup(); }, dc.constants.EVENT_DELAY); } else { var rangedFilter = dc.filters.RangedFilter(extent[0], extent[1]); dc.events.trigger(function () { _chart.replaceFilter(rangedFilter); _chart.redrawGroup(); }, dc.constants.EVENT_DELAY); } }; _chart.redrawBrush = function (g, doTransition) { if (_brushOn) { if (_chart.filter() && _chart.brush().empty()) { _chart.brush().extent(_chart.filter()); } var gBrush = dc.optionalTransition(doTransition, _chart.transitionDuration(), _chart.transitionDelay())(g.select('g.brush')); _chart.setBrushY(gBrush); gBrush.call(_chart.brush() .x(_chart.x()) .extent(_chart.brush().extent())); } _chart.fadeDeselectedArea(); }; _chart.fadeDeselectedArea = function () { // do nothing, sub-chart should override this function }; // borrowed from Crossfilter example _chart.resizeHandlePath = function (d) { var e = +(d === 'e'), x = e ? 1 : -1, y = brushHeight() / 3; return 'M' + (0.5 * x) + ',' + y + 'A6,6 0 0 ' + e + ' ' + (6.5 * x) + ',' + (y + 6) + 'V' + (2 * y - 6) + 'A6,6 0 0 ' + e + ' ' + (0.5 * x) + ',' + (2 * y) + 'Z' + 'M' + (2.5 * x) + ',' + (y + 8) + 'V' + (2 * y - 8) + 'M' + (4.5 * x) + ',' + (y + 8) + 'V' + (2 * y - 8); }; function getClipPathId () { return _chart.anchorName().replace(/[ .#=\[\]"]/g, '-') + '-clip'; } /** * Get or set the padding in pixels for the clip path. Once set padding will be applied evenly to * the top, left, right, and bottom when the clip path is generated. If set to zero, the clip area * will be exactly the chart body area minus the margins. * @method clipPadding * @memberof dc.coordinateGridMixin * @instance * @param {Number} [padding=5] * @returns {Number|dc.coordinateGridMixin} */ _chart.clipPadding = function (padding) { if (!arguments.length) { return _clipPadding; } _clipPadding = padding; return _chart; }; function generateClipPath () { var defs = dc.utils.appendOrSelect(_parent, 'defs'); // cannot select elements; bug in WebKit, must select by id // https://groups.google.com/forum/#!topic/d3-js/6EpAzQ2gU9I var id = getClipPathId(); var chartBodyClip = dc.utils.appendOrSelect(defs, '#' + id, 'clipPath').attr('id', id); var padding = _clipPadding * 2; dc.utils.appendOrSelect(chartBodyClip, 'rect') .attr('width', _chart.xAxisLength() + padding) .attr('height', _chart.yAxisHeight() + padding) .attr('transform', 'translate(-' + _clipPadding + ', -' + _clipPadding + ')'); } _chart._preprocessData = function () {}; _chart._doRender = function () { _chart.resetSvg(); _chart._preprocessData(); _chart._generateG(); generateClipPath(); drawChart(true); configureMouseZoom(); return _chart; }; _chart._doRedraw = function () { _chart._preprocessData(); drawChart(false); generateClipPath(); return _chart; }; function drawChart (render) { if (_chart.isOrdinal()) { _brushOn = false; } prepareXAxis(_chart.g(), render); _chart._prepareYAxis(_chart.g()); _chart.plotData(); if (_chart.elasticX() || _resizing || render) { _chart.renderXAxis(_chart.g()); } if (_chart.elasticY() || _resizing || render) { _chart.renderYAxis(_chart.g()); } if (render) { _chart.renderBrush(_chart.g(), false); } else { _chart.redrawBrush(_chart.g(), _resizing); } _chart.fadeDeselectedArea(); _resizing = false; } function configureMouseZoom () { if (_mouseZoomable) { _chart._enableMouseZoom(); } else if (_hasBeenMouseZoomable) { _chart._disableMouseZoom(); } } _chart._enableMouseZoom = function () { _hasBeenMouseZoomable = true; _zoom.x(_chart.x()) .scaleExtent(_zoomScale) .size([_chart.width(), _chart.height()]) .duration(_chart.transitionDuration()); _chart.root().call(_zoom); }; _chart._disableMouseZoom = function () { _chart.root().call(_nullZoom); }; function zoomHandler () { _refocused = true; if (_zoomOutRestrict) { var constraint = _xOriginalDomain; if (_rangeChart) { constraint = intersectExtents(constraint, _rangeChart.x().domain()); } var constrained = constrainExtent(_chart.x().domain(), constraint); if (constrained) { _chart.x().domain(constrained); } } var domain = _chart.x().domain(); var domFilter = dc.filters.RangedFilter(domain[0], domain[1]); _chart.replaceFilter(domFilter); _chart.rescale(); _chart.redraw(); if (_rangeChart && !rangesEqual(_chart.filter(), _rangeChart.filter())) { dc.events.trigger(function () { _rangeChart.replaceFilter(domFilter); _rangeChart.redraw(); }); } _chart._invokeZoomedListener(); dc.events.trigger(function () { _chart.redrawGroup(); }, dc.constants.EVENT_DELAY); _refocused = !rangesEqual(domain, _xOriginalDomain); } function intersectExtents (ext1, ext2) { if (ext1[0] > ext2[1] || ext1[1] < ext2[0]) { console.warn('could not intersect extents'); } return [Math.max(ext1[0], ext2[0]), Math.min(ext1[1], ext2[1])]; } function constrainExtent (extent, constraint) { var size = extent[1] - extent[0]; if (extent[0] < constraint[0]) { return [constraint[0], Math.min(constraint[1], dc.utils.add(constraint[0], size, 'millis'))]; } else if (extent[1] > constraint[1]) { return [Math.max(constraint[0], dc.utils.subtract(constraint[1], size, 'millis')), constraint[1]]; } else { return null; } } /** * Zoom this chart to focus on the given range. The given range should be an array containing only * 2 elements (`[start, end]`) defining a range in the x domain. If the range is not given or set * to null, then the zoom will be reset. _For focus to work elasticX has to be turned off; * otherwise focus will be ignored. * @method focus * @memberof dc.coordinateGridMixin * @instance * @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 {Array} [range] */ _chart.focus = function (range) { if (hasRangeSelected(range)) { _chart.x().domain(range); } else { _chart.x().domain(_xOriginalDomain); } _zoom.x(_chart.x()); zoomHandler(); }; _chart.refocused = function () { return _refocused; }; _chart.focusChart = function (c) { if (!arguments.length) { return _focusChart; } _focusChart = c; _chart.on('filtered', function (chart) { if (!chart.filter()) { dc.events.trigger(function () { _focusChart.x().domain(_focusChart.xOriginalDomain()); }); } else if (!rangesEqual(chart.filter(), _focusChart.filter())) { dc.events.trigger(function () { _focusChart.focus(chart.filter()); }); } }); return _chart; }; function rangesEqual (range1, range2) { if (!range1 && !range2) { return true; } else if (!range1 || !range2) { return false; } else if (range1.length === 0 && range2.length === 0) { return true; } else if (range1[0].valueOf() === range2[0].valueOf() && range1[1].valueOf() === range2[1].valueOf()) { return true; } return false; } /** * Turn on/off the brush-based range filter. When brushing is on then user can drag the mouse * across a chart with a quantitative scale to perform range filtering based on the extent of the * brush, or click on the bars of an ordinal bar chart or slices of a pie chart to filter and * un-filter them. However turning on the brush filter will disable other interactive elements on * the chart such as highlighting, tool tips, and reference lines. Zooming will still be possible * if enabled, but only via scrolling (panning will be disabled.) * @method brushOn * @memberof dc.coordinateGridMixin * @instance * @param {Boolean} [brushOn=true] * @returns {Boolean|dc.coordinateGridMixin} */ _chart.brushOn = function (brushOn) { if (!arguments.length) { return _brushOn; } _brushOn = brushOn; return _chart; }; function hasRangeSelected (range) { return range instanceof Array && range.length > 1; } return _chart; }; /** * Stack Mixin is an mixin that provides cross-chart support of stackability using d3.layout.stack. * @name stackMixin * @memberof dc * @mixin * @param {Object} _chart * @returns {dc.stackMixin} */ dc.stackMixin = function (_chart) { function prepareValues (layer, layerIdx) { var valAccessor = layer.accessor || _chart.valueAccessor(); layer.name = String(layer.name || layerIdx); layer.values = layer.group.all().map(function (d, i) { return { x: _chart.keyAccessor()(d, i), y: layer.hidden ? null : valAccessor(d, i), data: d, layer: layer.name, hidden: layer.hidden }; }); layer.values = layer.values.filter(domainFilter()); return layer.values; } var _stackLayout = d3.layout.stack() .values(prepareValues); var _stack = []; var _titles = {}; var _hidableStacks = false; var _evadeDomainFilter = false; function domainFilter () { if (!_chart.x() || _evadeDomainFilter) { return d3.functor(true); } var xDomain = _chart.x().domain(); if (_chart.isOrdinal()) { // TODO #416 //var domainSet = d3.set(xDomain); return function () { return true; //domainSet.has(p.x); }; } if (_chart.elasticX()) { return function () { return true; }; } return function (p) { //return true; return p.x >= xDomain[0] && p.x <= xDomain[xDomain.length - 1]; }; } /** * Stack a new crossfilter group onto this chart with an optional custom value accessor. All stacks * in the same chart will share the same key accessor and therefore the same set of keys. * * For example, in a stacked bar chart, the bars of each stack will be positioned using the same set * of keys on the x axis, while stacked vertically. If name is specified then it will be used to * generate the legend label. * @method stack * @memberof dc.stackMixin * @instance * @see {@link https://github.com/crossfilter/crossfilter/wiki/API-Reference#group-map-reduce crossfilter.group} * @example * // stack group using default accessor * chart.stack(valueSumGroup) * // stack group using custom accessor * .stack(avgByDayGroup, function(d){return d.value.avgByDay;}); * @param {crossfilter.group} group * @param {String} [name] * @param {Function} [accessor] * @returns {Array<{group: crossfilter.group, name: String, accessor: Function}>|dc.stackMixin} */ _chart.stack = function (group, name, accessor) { if (!arguments.length) { return _stack; } if (arguments.length <= 2) { accessor = name; } var layer = {group: group}; if (typeof name === 'string') { layer.name = name; } if (typeof accessor === 'function') { layer.accessor = accessor; } _stack.push(layer); return _chart; }; dc.override(_chart, 'group', function (g, n, f) { if (!arguments.length) { return _chart._group(); } _stack = []; _titles = {}; _chart.stack(g, n); if (f) { _chart.valueAccessor(f); } return _chart._group(g, n); }); /** * Allow named stacks to be hidden or shown by clicking on legend items. * This does not affect the behavior of hideStack or showStack. * @method hidableStacks * @memberof dc.stackMixin * @instance * @param {Boolean} [hidableStacks=false] * @returns {Boolean|dc.stackMixin} */ _chart.hidableStacks = function (hidableStacks) { if (!arguments.length) { return _hidableStacks; } _hidableStacks = hidableStacks; return _chart; }; function findLayerByName (n) { var i = _stack.map(dc.pluck('name')).indexOf(n); return _stack[i]; } /** * Hide all stacks on the chart with the given name. * The chart must be re-rendered for this change to appear. * @method hideStack * @memberof dc.stackMixin * @instance * @param {String} stackName * @returns {dc.stackMixin} */ _chart.hideStack = function (stackName) { var layer = findLayerByName(stackName); if (layer) { layer.hidden = true; } return _chart; }; /** * Show all stacks on the chart with the given name. * The chart must be re-rendered for this change to appear. * @method showStack * @memberof dc.stackMixin * @instance * @param {String} stackName * @returns {dc.stackMixin} */ _chart.showStack = function (stackName) { var layer = findLayerByName(stackName); if (layer) { layer.hidden = false; } return _chart; }; _chart.getValueAccessorByIndex = function (index) { return _stack[index].accessor || _chart.valueAccessor(); }; _chart.yAxisMin = function () { var min = d3.min(flattenStack(), function (p) { return (p.y < 0) ? (p.y + p.y0) : p.y0; }); return dc.utils.subtract(min, _chart.yAxisPadding()); }; _chart.yAxisMax = function () { var max = d3.max(flattenStack(), function (p) { return (p.y > 0) ? (p.y + p.y0) : p.y0; }); return dc.utils.add(max, _chart.yAxisPadding()); }; function flattenStack () { var valueses = _chart.data().map(function (layer) { return layer.values; }); return Array.prototype.concat.apply([], valueses); } _chart.xAxisMin = function () { var min = d3.min(flattenStack(), dc.pluck('x')); return dc.utils.subtract(min, _chart.xAxisPadding(), _chart.xAxisPaddingUnit()); }; _chart.xAxisMax = function () { var max = d3.max(flattenStack(), dc.pluck('x')); return dc.utils.add(max, _chart.xAxisPadding(), _chart.xAxisPaddingUnit()); }; /** * Set or get the title function. Chart class will use this function to render svg title (usually interpreted by * browser as tooltips) for each child element in the chart, i.e. a slice in a pie chart or a bubble in a bubble chart. * Almost every chart supports title function however in grid coordinate chart you need to turn off brush in order to * use title otherwise the brush layer will block tooltip trigger. * * If the first argument is a stack name, the title function will get or set the title for that stack. If stackName * is not provided, the first stack is implied. * @method title * @memberof dc.stackMixin * @instance * @example * // set a title function on 'first stack' * chart.title('first stack', function(d) { return d.key + ': ' + d.value; }); * // get a title function from 'second stack' * var secondTitleFunction = chart.title('second stack'); * @param {String} [stackName] * @param {Function} [titleAccessor] * @returns {String|dc.stackMixin} */ dc.override(_chart, 'title', function (stackName, titleAccessor) { if (!stackName) { return _chart._title(); } if (typeof stackName === 'function') { return _chart._title(stackName); } if (stackName === _chart._groupName && typeof titleAccessor === 'function') { return _chart._title(titleAccessor); } if (typeof titleAccessor !== 'function') { return _titles[stackName] || _chart._title(); } _titles[stackName] = titleAccessor; return _chart; }); /** * Gets or sets the stack layout algorithm, which computes a baseline for each stack and * propagates it to the next. * @method stackLayout * @memberof dc.stackMixin * @instance * @see {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Stack-Layout.md d3.layout.stack} * @param {Function} [stack=d3.layout.stack] * @returns {Function|dc.stackMixin} */ _chart.stackLayout = function (stack) { if (!arguments.length) { return _stackLayout; } _stackLayout = stack; if (_stackLayout.values() === d3.layout.stack().values()) { _stackLayout.values(prepareValues); } return _chart; }; /** * Since dc.js 2.0, there has been {@link https://github.com/dc-js/dc.js/issues/949 an issue} * where points are filtered to the current domain. While this is a useful optimization, it is * incorrectly implemented: the next point outside the domain is required in order to draw lines * that are clipped to the bounds, as well as bars that are partly clipped. * * A fix will be included in dc.js 2.1.x, but a workaround is needed for dc.js 2.0 and until * that fix is published, so set this flag to skip any filtering of points. * * Once the bug is fixed, this flag will have no effect, and it will be deprecated. * @method evadeDomainFilter * @memberof dc.stackMixin * @instance * @param {Boolean} [evadeDomainFilter=false] * @returns {Boolean|dc.stackMixin} */ _chart.evadeDomainFilter = function (evadeDomainFilter) { if (!arguments.length) { return _evadeDomainFilter; } _evadeDomainFilter = evadeDomainFilter; return _chart; }; function visability (l) { return !l.hidden; } _chart.data(function () { var layers = _stack.filter(visability); return layers.length ? _chart.stackLayout()(layers) : []; }); _chart._ordinalXDomain = function () { var flat = flattenStack().map(dc.pluck('data')); var ordered = _chart._computeOrderedGroups(flat); return ordered.map(_chart.keyAccessor()); }; _chart.colorAccessor(function (d) { var layer = this.layer || this.name || d.name || d.layer; return layer; }); _chart.legendables = function () { return _stack.map(function (layer, i) { return { chart: _chart, name: layer.name, hidden: layer.hidden || false, color: _chart.getColor.call(layer, layer.values, i) }; }); }; _chart.isLegendableHidden = function (d) { var layer = findLayerByName(d.name); return layer ? layer.hidden : false; }; _chart.legendToggle = function (d) { if (_hidableStacks) { if (_chart.isLegendableHidden(d)) { _chart.showStack(d.name); } else { _chart.hideStack(d.name); } //_chart.redraw(); _chart.renderGroup(); } }; return _chart; }; /** * Cap is a mixin that groups small data elements below a _cap_ into an *others* grouping for both the * Row and Pie Charts. * * The top ordered elements in the group up to the cap amount will be kept in the chart, and the rest * will be replaced with an *others* element, with value equal to the sum of the replaced values. The * keys of the elements below the cap limit are recorded in order to filter by those keys when the * others* element is clicked. * @name capMixin * @memberof dc * @mixin * @param {Object} _chart * @returns {dc.capMixin} */ dc.capMixin = function (_chart) { var _cap = Infinity, _takeFront = true; var _othersLabel = 'Others'; // emulate old group.top(N) ordering _chart.ordering(function (kv) { return -kv.value; }); var _othersGrouper = function (topItems, restItems) { var restItemsSum = d3.sum(restItems, _chart.valueAccessor()), restKeys = restItems.map(_chart.keyAccessor()); if (restItemsSum > 0) { return topItems.concat([{ others: restKeys, key: _chart.othersLabel(), value: restItemsSum }]); } return topItems; }; _chart.cappedKeyAccessor = function (d, i) { if (d.others) { return d.key; } return _chart.keyAccessor()(d, i); }; _chart.cappedValueAccessor = function (d, i) { if (d.others) { return d.value; } return _chart.valueAccessor()(d, i); }; // return N "top" groups, where N is the cap, sorted by baseMixin.ordering // whether top means front or back depends on takeFront _chart.data(function (group) { if (_cap === Infinity) { return _chart._computeOrderedGroups(group.all()); } else { var items = group.all(), rest; items = _chart._computeOrderedGroups(items); // sort by baseMixin.ordering if (_cap) { if (_takeFront) { rest = items.slice(_cap); items = items.slice(0, _cap); } else { var start = Math.max(0, items.length - _cap); rest = items.slice(0, start); items = items.slice(start); } } if (_othersGrouper) { return _othersGrouper(items, rest); } return items; } }); /** * Get or set the count of elements to that will be included in the cap. If there is an * {@link dc.capMixin#othersGrouper othersGrouper}, any further elements will be combined in an * extra element with its name determined by {@link dc.capMixin#othersLabel othersLabel}. * * As of dc.js 2.1 and onward, the capped charts use * {@link https://github.com/crossfilter/crossfilter/wiki/API-Reference#group_all group.all()} * and {@link dc.baseMixin#ordering baseMixin.ordering()} to determine the order of * elements. Then `cap` and {@link dc.capMixin#takeFront takeFront} determine how many elements * to keep, from which end of the resulting array. * * **Migration note:** Up through dc.js 2.0.*, capping used * {@link https://github.com/crossfilter/crossfilter/wiki/API-Reference#group_top group.top(N)}, * which selects the largest items according to * {@link https://github.com/crossfilter/crossfilter/wiki/API-Reference#group_order group.order()}. * The chart then sorted the items according to {@link dc.baseMixin#ordering baseMixin.ordering()}. * So the two values essentially had to agree, but if the `group.order()` was incorrect (it's * easy to forget about), the wrong rows or slices would be displayed, in the correct order. * * If your chart previously relied on `group.order()`, use `chart.ordering()` instead. As of * 2.1.5, the ordering defaults to sorting from greatest to least like `group.top(N)` did. * * If you want to cap by one ordering but sort by another, please * [file an issue](https://github.com/dc-js/dc.js/issues/new) - it's still possible but we'll * need to work up an example. * @method cap * @memberof dc.capMixin * @instance * @param {Number} [count=Infinity] * @returns {Number|dc.capMixin} */ _chart.cap = function (count) { if (!arguments.length) { return _cap; } _cap = count; return _chart; }; /** * Get or set the direction of capping. If set, the chart takes the first * {@link dc.capMixin#cap cap} elements from the sorted array of elements; otherwise * it takes the last `cap` elements. * @method takeFront * @memberof dc.capMixin * @instance * @param {Boolean} [takeFront=true] * @returns {Boolean|dc.capMixin} */ _chart.takeFront = function (takeFront) { if (!arguments.length) { return _takeFront; } _takeFront = takeFront; return _chart; }; /** * Get or set the label for *Others* slice when slices cap is specified. * @method othersLabel * @memberof dc.capMixin * @instance * @param {String} [label="Others"] * @returns {String|dc.capMixin} */ _chart.othersLabel = function (label) { if (!arguments.length) { return _othersLabel; } _othersLabel = label; return _chart; }; /** * Get or set the grouper function that will perform the insertion of data for the *Others* slice * if the slices cap is specified. If set to a falsy value, no others will be added. * * The grouper function takes an array of included ("top") items, and an array of the rest of * the items. By default the grouper function computes the sum of the rest. * @method othersGrouper * @memberof dc.capMixin * @instance * @example * // Do not show others * chart.othersGrouper(null); * // Default others grouper * chart.othersGrouper(function (topItems, restItems) { * var restItemsSum = d3.sum(restItems, _chart.valueAccessor()), * restKeys = restItems.map(_chart.keyAccessor()); * if (restItemsSum > 0) { * return topItems.concat([{ * others: restKeys, * key: _chart.othersLabel(), * value: restItemsSum * }]); * } * return topItems; * }); * @param {Function} [grouperFunction] * @returns {Function|dc.capMixin} */ _chart.othersGrouper = function (grouperFunction) { if (!arguments.length) { return _othersGrouper; } _othersGrouper = grouperFunction; return _chart; }; dc.override(_chart, 'onClick', function (d) { if (d.others) { _chart.filter([d.others]); } _chart._onClick(d); }); return _chart; }; /** * This Mixin provides reusable functionalities for any chart that needs to visualize data using bubbles. * @name bubbleMixin * @memberof dc * @mixin * @mixes dc.colorMixin * @param {Object} _chart * @returns {dc.bubbleMixin} */ dc.bubbleMixin = function (_chart) { var _maxBubbleRelativeSize = 0.3; var _minRadiusWithLabel = 10; var _sortBubbleSize = false; var _elasticRadius = false; _chart.BUBBLE_NODE_CLASS = 'node'; _chart.BUBBLE_CLASS = 'bubble'; _chart.MIN_RADIUS = 10; _chart = dc.colorMixin(_chart); _chart.renderLabel(true); _chart.data(function (group) { var data = group.all(); if (_sortBubbleSize) { // sort descending so smaller bubbles are on top var radiusAccessor = _chart.radiusValueAccessor(); data.sort(function (a, b) { return d3.descending(radiusAccessor(a), radiusAccessor(b)); }); } return data; }); var _r = d3.scale.linear().domain([0, 100]); var _rValueAccessor = function (d) { return d.r; }; /** * Get or set the bubble radius scale. By default the bubble chart uses * {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Quantitative-Scales.md#linear d3.scale.linear().domain([0, 100])} * as its radius scale. * @method r * @memberof dc.bubbleMixin * @instance * @see {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Scales.md d3.scale} * @param {d3.scale} [bubbleRadiusScale=d3.scale.linear().domain([0, 100])] * @returns {d3.scale|dc.bubbleMixin} */ _chart.r = function (bubbleRadiusScale) { if (!arguments.length) { return _r; } _r = bubbleRadiusScale; return _chart; }; /** * Turn on or off the elastic bubble radius feature, or return the value of the flag. If this * feature is turned on, then bubble radii will be automatically rescaled to fit the chart better. * @method elasticRadius * @memberof dc.bubbleChart * @instance * @param {Boolean} [elasticRadius=false] * @returns {Boolean|dc.bubbleChart} */ _chart.elasticRadius = function (elasticRadius) { if (!arguments.length) { return _elasticRadius; } _elasticRadius = elasticRadius; return _chart; }; _chart.calculateRadiusDomain = function () { if (_elasticRadius) { _chart.r().domain([_chart.rMin(), _chart.rMax()]); } }; /** * Get or set the radius value accessor function. If set, the radius value accessor function will * be used to retrieve a data value for each bubble. The data retrieved then will be mapped using * the r scale to the actual bubble radius. This allows you to encode a data dimension using bubble * size. * @method radiusValueAccessor * @memberof dc.bubbleMixin * @instance * @param {Function} [radiusValueAccessor] * @returns {Function|dc.bubbleMixin} */ _chart.radiusValueAccessor = function (radiusValueAccessor) { if (!arguments.length) { return _rValueAccessor; } _rValueAccessor = radiusValueAccessor; return _chart; }; _chart.rMin = function () { var min = d3.min(_chart.data(), function (e) { return _chart.radiusValueAccessor()(e); }); return min; }; _chart.rMax = function () { var max = d3.max(_chart.data(), function (e) { return _chart.radiusValueAccessor()(e); }); return max; }; _chart.bubbleR = function (d) { var value = _chart.radiusValueAccessor()(d); var r = _chart.r()(value); if (isNaN(r) || value <= 0) { r = 0; } return r; }; var labelFunction = function (d) { return _chart.label()(d); }; var shouldLabel = function (d) { return (_chart.bubbleR(d) > _minRadiusWithLabel); }; var labelOpacity = function (d) { return shouldLabel(d) ? 1 : 0; }; var labelPointerEvent = function (d) { return shouldLabel(d) ? 'all' : 'none'; }; _chart._doRenderLabel = function (bubbleGEnter) { if (_chart.renderLabel()) { var label = bubbleGEnter.select('text'); if (label.empty()) { label = bubbleGEnter.append('text') .attr('text-anchor', 'middle') .attr('dy', '.3em') .on('click', _chart.onClick); } label .attr('opacity', 0) .attr('pointer-events', labelPointerEvent) .text(labelFunction); dc.transition(label, _chart.transitionDuration(), _chart.transitionDelay()) .attr('opacity', labelOpacity); } }; _chart.doUpdateLabels = function (bubbleGEnter) { if (_chart.renderLabel()) { var labels = bubbleGEnter.select('text') .attr('pointer-events', labelPointerEvent) .text(labelFunction); dc.transition(labels, _chart.transitionDuration(), _chart.transitionDelay()) .attr('opacity', labelOpacity); } }; var titleFunction = function (d) { return _chart.title()(d); }; _chart._doRenderTitles = function (g) { if (_chart.renderTitle()) { var title = g.select('title'); if (title.empty()) { g.append('title').text(titleFunction); } } }; _chart.doUpdateTitles = function (g) { if (_chart.renderTitle()) { g.select('title').text(titleFunction); } }; /** * Turn on or off the bubble sorting feature, or return the value of the flag. If enabled, * bubbles will be sorted by their radius, with smaller bubbles in front. * @method sortBubbleSize * @memberof dc.bubbleChart * @instance * @param {Boolean} [sortBubbleSize=false] * @returns {Boolean|dc.bubbleChart} */ _chart.sortBubbleSize = function (sortBubbleSize) { if (!arguments.length) { return _sortBubbleSize; } _sortBubbleSize = sortBubbleSize; return _chart; }; /** * Get or set the minimum radius. This will be used to initialize the radius scale's range. * @method minRadius * @memberof dc.bubbleMixin * @instance * @param {Number} [radius=10] * @returns {Number|dc.bubbleMixin} */ _chart.minRadius = function (radius) { if (!arguments.length) { return _chart.MIN_RADIUS; } _chart.MIN_RADIUS = radius; return _chart; }; /** * Get or set the minimum radius for label rendering. If a bubble's radius is less than this value * then no label will be rendered. * @method minRadiusWithLabel * @memberof dc.bubbleMixin * @instance * @param {Number} [radius=10] * @returns {Number|dc.bubbleMixin} */ _chart.minRadiusWithLabel = function (radius) { if (!arguments.length) { return _minRadiusWithLabel; } _minRadiusWithLabel = radius; return _chart; }; /** * Get or set the maximum relative size of a bubble to the length of x axis. This value is useful * when the difference in radius between bubbles is too great. * @method maxBubbleRelativeSize * @memberof dc.bubbleMixin * @instance * @param {Number} [relativeSize=0.3] * @returns {Number|dc.bubbleMixin} */ _chart.maxBubbleRelativeSize = function (relativeSize) { if (!arguments.length) { return _maxBubbleRelativeSize; } _maxBubbleRelativeSize = relativeSize; return _chart; }; _chart.fadeDeselectedArea = function () { if (_chart.hasFilter()) { _chart.selectAll('g.' + _chart.BUBBLE_NODE_CLASS).each(function (d) { if (_chart.isSelectedNode(d)) { _chart.highlightSelected(this); } else { _chart.fadeDeselected(this); } }); } else { _chart.selectAll('g.' + _chart.BUBBLE_NODE_CLASS).each(function () { _chart.resetHighlight(this); }); } }; _chart.isSelectedNode = function (d) { return _chart.hasFilter(d.key); }; _chart.onClick = function (d) { var filter = d.key; dc.events.trigger(function () { _chart.filter(filter); _chart.redrawGroup(); }); }; return _chart; }; /** * The pie chart implementation is usually used to visualize a small categorical distribution. The pie * chart uses keyAccessor to determine the slices, and valueAccessor to calculate the size of each * slice relative to the sum of all values. Slices are ordered by {@link dc.baseMixin#ordering ordering} * which defaults to sorting by key. * * Examples: * - {@link http://dc-js.github.com/dc.js/ Nasdaq 100 Index} * @class pieChart * @memberof dc * @mixes dc.capMixin * @mixes dc.colorMixin * @mixes dc.baseMixin * @example * // create a pie chart under #chart-container1 element using the default global chart group * var chart1 = dc.pieChart('#chart-container1'); * // create a pie chart under #chart-container2 element using chart group A * var chart2 = dc.pieChart('#chart-container2', 'chartGroupA'); * @param {String|node|d3.selection} parent - Any valid * {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Selections.md#selecting-elements d3 single selector} specifying * a dom block element such as a div; or a dom element or d3 selection. * @param {String} [chartGroup] - The name of the chart group this chart instance should be placed in. * Interaction with a chart will only trigger events and redraws within the chart's group. * @returns {dc.pieChart} */ dc.pieChart = function (parent, chartGroup) { var DEFAULT_MIN_ANGLE_FOR_LABEL = 0.5; var _sliceCssClass = 'pie-slice'; var _labelCssClass = 'pie-label'; var _sliceGroupCssClass = 'pie-slice-group'; var _labelGroupCssClass = 'pie-label-group'; var _emptyCssClass = 'empty-chart'; var _emptyTitle = 'empty'; var _radius, _givenRadius, // specified radius, if any _innerRadius = 0, _externalRadiusPadding = 0; var _g; var _cx; var _cy; var _minAngleForLabel = DEFAULT_MIN_ANGLE_FOR_LABEL; var _externalLabelRadius; var _drawPaths = false; var _chart = dc.capMixin(dc.colorMixin(dc.baseMixin({}))); _chart.colorAccessor(_chart.cappedKeyAccessor); _chart.title(function (d) { return _chart.cappedKeyAccessor(d) + ': ' + _chart.cappedValueAccessor(d); }); /** * Get or set the maximum number of slices the pie chart will generate. The top slices are determined by * value from high to low. Other slices exeeding the cap will be rolled up into one single *Others* slice. * @method slicesCap * @memberof dc.pieChart * @instance * @param {Number} [cap] * @returns {Number|dc.pieChart} */ _chart.slicesCap = _chart.cap; _chart.label(_chart.cappedKeyAccessor); _chart.renderLabel(true); _chart.transitionDuration(350); _chart.transitionDelay(0); _chart._doRender = function () { _chart.resetSvg(); _g = _chart.svg() .append('g') .attr('transform', 'translate(' + _chart.cx() + ',' + _chart.cy() + ')'); _g.append('g').attr('class', _sliceGroupCssClass); _g.append('g').attr('class', _labelGroupCssClass); drawChart(); return _chart; }; function drawChart () { // set radius from chart size if none given, or if given radius is too large var maxRadius = d3.min([_chart.width(), _chart.height()]) / 2; _radius = _givenRadius && _givenRadius < maxRadius ? _givenRadius : maxRadius; var arc = buildArcs(); var pie = pieLayout(); var pieData; // if we have data... if (d3.sum(_chart.data(), _chart.valueAccessor())) { pieData = pie(_chart.data()); _g.classed(_emptyCssClass, false); } else { // otherwise we'd be getting NaNs, so override // note: abuse others for its ignoring the value accessor pieData = pie([{key: _emptyTitle, value: 1, others: [_emptyTitle]}]); _g.classed(_emptyCssClass, true); } if (_g) { var slices = _g.select('g.' + _sliceGroupCssClass) .selectAll('g.' + _sliceCssClass) .data(pieData); var labels = _g.select('g.' + _labelGroupCssClass) .selectAll('text.' + _labelCssClass) .data(pieData); createElements(slices, labels, arc, pieData); updateElements(pieData, arc); removeElements(slices, labels); highlightFilter(); dc.transition(_g, _chart.transitionDuration(), _chart.transitionDelay()) .attr('transform', 'translate(' + _chart.cx() + ',' + _chart.cy() + ')'); } } function createElements (slices, labels, arc, pieData) { var slicesEnter = createSliceNodes(slices); createSlicePath(slicesEnter, arc); createTitles(slicesEnter); createLabels(labels, pieData, arc); } function createSliceNodes (slices) { var slicesEnter = slices .enter() .append('g') .attr('class', function (d, i) { return _sliceCssClass + ' _' + i; }); return slicesEnter; } function createSlicePath (slicesEnter, arc) { var slicePath = slicesEnter.append('path') .attr('fill', fill) .on('click', onClick) .attr('d', function (d, i) { return safeArc(d, i, arc); }); var transition = dc.transition(slicePath, _chart.transitionDuration(), _chart.transitionDelay()); if (transition.attrTween) { transition.attrTween('d', tweenPie); } } function createTitles (slicesEnter) { if (_chart.renderTitle()) { slicesEnter.append('title').text(function (d) { return _chart.title()(d.data); }); } } _chart._applyLabelText = function (labels) { labels .text(function (d) { var data = d.data; if ((sliceHasNoData(data) || sliceTooSmall(d)) && !isSelectedSlice(d)) { return ''; } return _chart.label()(d.data); }); }; function positionLabels (labels, arc) { _chart._applyLabelText(labels); dc.transition(labels, _chart.transitionDuration(), _chart.transitionDelay()) .attr('transform', function (d) { return labelPosition(d, arc); }) .attr('text-anchor', 'middle'); } function highlightSlice (i, whether) { _chart.select('g.pie-slice._' + i) .classed('highlight', whether); } function createLabels (labels, pieData, arc) { if (_chart.renderLabel()) { var labelsEnter = labels .enter() .append('text') .attr('class', function (d, i) { var classes = _sliceCssClass + ' ' + _labelCssClass + ' _' + i; if (_externalLabelRadius) { classes += ' external'; } return classes; }) .on('click', onClick) .on('mouseover', function (d, i) { highlightSlice(i, true); }) .on('mouseout', function (d, i) { highlightSlice(i, false); }); positionLabels(labelsEnter, arc); if (_externalLabelRadius && _drawPaths) { updateLabelPaths(pieData, arc); } } } function updateLabelPaths (pieData, arc) { var polyline = _g.selectAll('polyline.' + _sliceCssClass) .data(pieData); polyline .enter() .append('polyline') .attr('class', function (d, i) { return 'pie-path _' + i + ' ' + _sliceCssClass; }) .on('click', onClick) .on('mouseover', function (d, i) { highlightSlice(i, true); }) .on('mouseout', function (d, i) { highlightSlice(i, false); }); polyline.exit().remove(); var arc2 = d3.svg.arc() .outerRadius(_radius - _externalRadiusPadding + _externalLabelRadius) .innerRadius(_radius - _externalRadiusPadding); var transition = dc.transition(polyline, _chart.transitionDuration(), _chart.transitionDelay()); // this is one rare case where d3.selection differs from d3.transition if (transition.attrTween) { transition .attrTween('points', function (d) { var current = this._current || d; current = {startAngle: current.startAngle, endAngle: current.endAngle}; var interpolate = d3.interpolate(current, d); this._current = interpolate(0); return function (t) { var d2 = interpolate(t); return [arc.centroid(d2), arc2.centroid(d2)]; }; }); } else { transition.attr('points', function (d) { return [arc.centroid(d), arc2.centroid(d)]; }); } transition.style('visibility', function (d) { return d.endAngle - d.startAngle < 0.0001 ? 'hidden' : 'visible'; }); } function updateElements (pieData, arc) { updateSlicePaths(pieData, arc); updateLabels(pieData, arc); updateTitles(pieData); } function updateSlicePaths (pieData, arc) { var slicePaths = _g.selectAll('g.' + _sliceCssClass) .data(pieData) .select('path') .attr('d', function (d, i) { return safeArc(d, i, arc); }); var transition = dc.transition(slicePaths, _chart.transitionDuration(), _chart.transitionDelay()); if (transition.attrTween) { transition.attrTween('d', tweenPie); } transition.attr('fill', fill); } function updateLabels (pieData, arc) { if (_chart.renderLabel()) { var labels = _g.selectAll('text.' + _labelCssClass) .data(pieData); positionLabels(labels, arc); if (_externalLabelRadius && _drawPaths) { updateLabelPaths(pieData, arc); } } } function updateTitles (pieData) { if (_chart.renderTitle()) { _g.selectAll('g.' + _sliceCssClass) .data(pieData) .select('title') .text(function (d) { return _chart.title()(d.data); }); } } function removeElements (slices, labels) { slices.exit().remove(); labels.exit().remove(); } function highlightFilter () { if (_chart.hasFilter()) { _chart.selectAll('g.' + _sliceCssClass).each(function (d) { if (isSelectedSlice(d)) { _chart.highlightSelected(this); } else { _chart.fadeDeselected(this); } }); } else { _chart.selectAll('g.' + _sliceCssClass).each(function () { _chart.resetHighlight(this); }); } } /** * Get or set the external radius padding of the pie chart. This will force the radius of the * pie chart to become smaller or larger depending on the value. * @method externalRadiusPadding * @memberof dc.pieChart * @instance * @param {Number} [externalRadiusPadding=0] * @returns {Number|dc.pieChart} */ _chart.externalRadiusPadding = function (externalRadiusPadding) { if (!arguments.length) { return _externalRadiusPadding; } _externalRadiusPadding = externalRadiusPadding; return _chart; }; /** * Get or set the inner radius of the pie chart. If the inner radius is greater than 0px then the * pie chart will be rendered as a doughnut chart. * @method innerRadius * @memberof dc.pieChart * @instance * @param {Number} [innerRadius=0] * @returns {Number|dc.pieChart} */ _chart.innerRadius = function (innerRadius) { if (!arguments.length) { return _innerRadius; } _innerRadius = innerRadius; return _chart; }; /** * Get or set the outer radius. If the radius is not set, it will be half of the minimum of the * chart width and height. * @method radius * @memberof dc.pieChart * @instance * @param {Number} [radius] * @returns {Number|dc.pieChart} */ _chart.radius = function (radius) { if (!arguments.length) { return _givenRadius; } _givenRadius = radius; return _chart; }; /** * Get or set center x coordinate position. Default is center of svg. * @method cx * @memberof dc.pieChart * @instance * @param {Number} [cx] * @returns {Number|dc.pieChart} */ _chart.cx = function (cx) { if (!arguments.length) { return (_cx || _chart.width() / 2); } _cx = cx; return _chart; }; /** * Get or set center y coordinate position. Default is center of svg. * @method cy * @memberof dc.pieChart * @instance * @param {Number} [cy] * @returns {Number|dc.pieChart} */ _chart.cy = function (cy) { if (!arguments.length) { return (_cy || _chart.height() / 2); } _cy = cy; return _chart; }; function buildArcs () { return d3.svg.arc() .outerRadius(_radius - _externalRadiusPadding) .innerRadius(_innerRadius); } function isSelectedSlice (d) { return _chart.hasFilter(_chart.cappedKeyAccessor(d.data)); } _chart._doRedraw = function () { drawChart(); return _chart; }; /** * Get or set the minimal slice angle for label rendering. Any slice with a smaller angle will not * display a slice label. * @method minAngleForLabel * @memberof dc.pieChart * @instance * @param {Number} [minAngleForLabel=0.5] * @returns {Number|dc.pieChart} */ _chart.minAngleForLabel = function (minAngleForLabel) { if (!arguments.length) { return _minAngleForLabel; } _minAngleForLabel = minAngleForLabel; return _chart; }; function pieLayout () { return d3.layout.pie().sort(null).value(_chart.cappedValueAccessor); } function sliceTooSmall (d) { var angle = (d.endAngle - d.startAngle); return isNaN(angle) || angle < _minAngleForLabel; } function sliceHasNoData (d) { return _chart.cappedValueAccessor(d) === 0; } function tweenPie (b) { b.innerRadius = _innerRadius; var current = this._current; if (isOffCanvas(current)) { current = {startAngle: 0, endAngle: 0}; } else { // only interpolate startAngle & endAngle, not the whole data object current = {startAngle: current.startAngle, endAngle: current.endAngle}; } var i = d3.interpolate(current, b); this._current = i(0); return function (t) { return safeArc(i(t), 0, buildArcs()); }; } function isOffCanvas (current) { return !current || isNaN(current.startAngle) || isNaN(current.endAngle); } function fill (d, i) { return _chart.getColor(d.data, i); } function onClick (d, i) { if (_g.attr('class') !== _emptyCssClass) { _chart.onClick(d.data, i); } } function safeArc (d, i, arc) { var path = arc(d, i); if (path.indexOf('NaN') >= 0) { path = 'M0,0'; } return path; } /** * Title to use for the only slice when there is no data. * @method emptyTitle * @memberof dc.pieChart * @instance * @param {String} [title] * @returns {String|dc.pieChart} */ _chart.emptyTitle = function (title) { if (arguments.length === 0) { return _emptyTitle; } _emptyTitle = title; return _chart; }; /** * Position slice labels offset from the outer edge of the chart. * * The argument specifies the extra radius to be added for slice labels. * @method externalLabels * @memberof dc.pieChart * @instance * @param {Number} [externalLabelRadius] * @returns {Number|dc.pieChart} */ _chart.externalLabels = function (externalLabelRadius) { if (arguments.length === 0) { return _externalLabelRadius; } else if (externalLabelRadius) { _externalLabelRadius = externalLabelRadius; } else { _externalLabelRadius = undefined; } return _chart; }; /** * Get or set whether to draw lines from pie slices to their labels. * * @method drawPaths * @memberof dc.pieChart * @instance * @param {Boolean} [drawPaths] * @returns {Boolean|dc.pieChart} */ _chart.drawPaths = function (drawPaths) { if (arguments.length === 0) { return _drawPaths; } _drawPaths = drawPaths; return _chart; }; function labelPosition (d, arc) { var centroid; if (_externalLabelRadius) { centroid = d3.svg.arc() .outerRadius(_radius - _externalRadiusPadding + _externalLabelRadius) .innerRadius(_radius - _externalRadiusPadding + _externalLabelRadius) .centroid(d); } else { centroid = arc.centroid(d); } if (isNaN(centroid[0]) || isNaN(centroid[1])) { return 'translate(0,0)'; } else { return 'translate(' + centroid + ')'; } } _chart.legendables = function () { return _chart.data().map(function (d, i) { var legendable = {name: d.key, data: d.value, others: d.others, chart: _chart}; legendable.color = _chart.getColor(d, i); return legendable; }); }; _chart.legendHighlight = function (d) { highlightSliceFromLegendable(d, true); }; _chart.legendReset = function (d) { highlightSliceFromLegendable(d, false); }; _chart.legendToggle = function (d) { _chart.onClick({key: d.name, others: d.others}); }; function highlightSliceFromLegendable (legendable, highlighted) { _chart.selectAll('g.pie-slice').each(function (d) { if (legendable.name === d.data.key) { d3.select(this).classed('highlight', highlighted); } }); } return _chart.anchor(parent, chartGroup); }; /** * Concrete bar chart/histogram implementation. * * Examples: * - {@link http://dc-js.github.com/dc.js/ Nasdaq 100 Index} * - {@link http://dc-js.github.com/dc.js/crime/index.html Canadian City Crime Stats} * @class barChart * @memberof dc * @mixes dc.stackMixin * @mixes dc.coordinateGridMixin * @example * // create a bar chart under #chart-container1 element using the default global chart group * var chart1 = dc.barChart('#chart-container1'); * // create a bar chart under #chart-container2 element using chart group A * var chart2 = dc.barChart('#chart-container2', 'chartGroupA'); * // create a sub-chart under a composite parent chart * var chart3 = dc.barChart(compositeChart); * @param {String|node|d3.selection|dc.compositeChart} parent - Any valid * {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Selections.md#selecting-elements d3 single selector} * specifying a dom block element such as a div; or a dom element or d3 selection. If the bar * chart is a sub-chart in a {@link dc.compositeChart Composite Chart} then pass in the parent * composite chart instance instead. * @param {String} [chartGroup] - The name of the chart group this chart instance should be placed in. * Interaction with a chart will only trigger events and redraws within the chart's group. * @returns {dc.barChart} */ dc.barChart = function (parent, chartGroup) { var MIN_BAR_WIDTH = 1; var DEFAULT_GAP_BETWEEN_BARS = 2; var LABEL_PADDING = 3; var _chart = dc.stackMixin(dc.coordinateGridMixin({})); var _gap = DEFAULT_GAP_BETWEEN_BARS; var _centerBar = false; var _alwaysUseRounding = false; var _barWidth; dc.override(_chart, 'rescale', function () { _chart._rescale(); _barWidth = undefined; return _chart; }); dc.override(_chart, 'render', function () { if (_chart.round() && _centerBar && !_alwaysUseRounding) { dc.logger.warn('By default, brush rounding is disabled if bars are centered. ' + 'See dc.js bar chart API documentation for details.'); } return _chart._render(); }); _chart.label(function (d) { return dc.utils.printSingleValue(d.y0 + d.y); }, false); _chart.plotData = function () { var layers = _chart.chartBodyG().selectAll('g.stack') .data(_chart.data()); calculateBarWidth(); layers .enter() .append('g') .attr('class', function (d, i) { return 'stack ' + '_' + i; }); var last = layers.size() - 1; layers.each(function (d, i) { var layer = d3.select(this); renderBars(layer, i, d); if (_chart.renderLabel() && last === i) { renderLabels(layer, i, d); } }); }; function barHeight (d) { return dc.utils.safeNumber(Math.abs(_chart.y()(d.y + d.y0) - _chart.y()(d.y0))); } function renderLabels (layer, layerIndex, d) { var labels = layer.selectAll('text.barLabel') .data(d.values, dc.pluck('x')); labels.enter() .append('text') .attr('class', 'barLabel') .attr('text-anchor', 'middle'); if (_chart.isOrdinal()) { labels.on('click', _chart.onClick); labels.attr('cursor', 'pointer'); } dc.transition(labels, _chart.transitionDuration(), _chart.transitionDelay()) .attr('x', function (d) { var x = _chart.x()(d.x); if (!_centerBar) { x += _barWidth / 2; } return dc.utils.safeNumber(x); }) .attr('y', function (d) { var y = _chart.y()(d.y + d.y0); if (d.y < 0) { y -= barHeight(d); } return dc.utils.safeNumber(y - LABEL_PADDING); }) .text(function (d) { return _chart.label()(d); }); dc.transition(labels.exit(), _chart.transitionDuration(), _chart.transitionDelay()) .attr('height', 0) .remove(); } function renderBars (layer, layerIndex, d) { var bars = layer.selectAll('rect.bar') .data(d.values, dc.pluck('x')); var enter = bars.enter() .append('rect') .attr('class', 'bar') .attr('fill', dc.pluck('data', _chart.getColor)) .attr('y', _chart.yAxisHeight()) .attr('height', 0); if (_chart.renderTitle()) { enter.append('title').text(dc.pluck('data', _chart.title(d.name))); } if (_chart.isOrdinal()) { bars.on('click', _chart.onClick); } dc.transition(bars, _chart.transitionDuration(), _chart.transitionDelay()) .attr('x', function (d) { var x = _chart.x()(d.x); if (_centerBar) { x -= _barWidth / 2; } if (_chart.isOrdinal() && _gap !== undefined) { x += _gap / 2; } return dc.utils.safeNumber(x); }) .attr('y', function (d) { var y = _chart.y()(d.y + d.y0); if (d.y < 0) { y -= barHeight(d); } return dc.utils.safeNumber(y); }) .attr('width', _barWidth) .attr('height', function (d) { return barHeight(d); }) .attr('fill', dc.pluck('data', _chart.getColor)) .select('title').text(dc.pluck('data', _chart.title(d.name))); dc.transition(bars.exit(), _chart.transitionDuration(), _chart.transitionDelay()) .attr('x', function (d) { return _chart.x()(d.x); }) .attr('width', _barWidth * 0.9) .remove(); } function calculateBarWidth () { if (_barWidth === undefined) { var numberOfBars = _chart.xUnitCount(); // please can't we always use rangeBands for bar charts? if (_chart.isOrdinal() && _gap === undefined) { _barWidth = Math.floor(_chart.x().rangeBand()); } else if (_gap) { _barWidth = Math.floor((_chart.xAxisLength() - (numberOfBars - 1) * _gap) / numberOfBars); } else { _barWidth = Math.floor(_chart.xAxisLength() / (1 + _chart.barPadding()) / numberOfBars); } if (_barWidth === Infinity || isNaN(_barWidth) || _barWidth < MIN_BAR_WIDTH) { _barWidth = MIN_BAR_WIDTH; } } } _chart.fadeDeselectedArea = function () { var bars = _chart.chartBodyG().selectAll('rect.bar'); var extent = _chart.brush().extent(); if (_chart.isOrdinal()) { if (_chart.hasFilter()) { bars.classed(dc.constants.SELECTED_CLASS, function (d) { return _chart.hasFilter(d.x); }); bars.classed(dc.constants.DESELECTED_CLASS, function (d) { return !_chart.hasFilter(d.x); }); } else { bars.classed(dc.constants.SELECTED_CLASS, false); bars.classed(dc.constants.DESELECTED_CLASS, false); } } else { if (!_chart.brushIsEmpty(extent)) { var start = extent[0]; var end = extent[1]; bars.classed(dc.constants.DESELECTED_CLASS, function (d) { return d.x < start || d.x >= end; }); } else { bars.classed(dc.constants.DESELECTED_CLASS, false); } } }; /** * Whether the bar chart will render each bar centered around the data position on the x-axis. * @method centerBar * @memberof dc.barChart * @instance * @param {Boolean} [centerBar=false] * @returns {Boolean|dc.barChart} */ _chart.centerBar = function (centerBar) { if (!arguments.length) { return _centerBar; } _centerBar = centerBar; return _chart; }; dc.override(_chart, 'onClick', function (d) { _chart._onClick(d.data); }); /** * Get or set the spacing between bars as a fraction of bar size. Valid values are between 0-1. * Setting this value will also remove any previously set {@link dc.barChart#gap gap}. See the * {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Ordinal-Scales.md#ordinal_rangeBands d3 docs} * for a visual description of how the padding is applied. * @method barPadding * @memberof dc.barChart * @instance * @param {Number} [barPadding=0] * @returns {Number|dc.barChart} */ _chart.barPadding = function (barPadding) { if (!arguments.length) { return _chart._rangeBandPadding(); } _chart._rangeBandPadding(barPadding); _gap = undefined; return _chart; }; _chart._useOuterPadding = function () { return _gap === undefined; }; /** * Get or set the outer padding on an ordinal bar chart. This setting has no effect on non-ordinal charts. * Will pad the width by `padding * barWidth` on each side of the chart. * @method outerPadding * @memberof dc.barChart * @instance * @param {Number} [padding=0.5] * @returns {Number|dc.barChart} */ _chart.outerPadding = _chart._outerRangeBandPadding; /** * Manually set fixed gap (in px) between bars instead of relying on the default auto-generated * gap. By default the bar chart implementation will calculate and set the gap automatically * based on the number of data points and the length of the x axis. * @method gap * @memberof dc.barChart * @instance * @param {Number} [gap=2] * @returns {Number|dc.barChart} */ _chart.gap = function (gap) { if (!arguments.length) { return _gap; } _gap = gap; return _chart; }; _chart.extendBrush = function () { var extent = _chart.brush().extent(); if (_chart.round() && (!_centerBar || _alwaysUseRounding)) { extent[0] = extent.map(_chart.round())[0]; extent[1] = extent.map(_chart.round())[1]; _chart.chartBodyG().select('.brush') .call(_chart.brush().extent(extent)); } return extent; }; /** * Set or get whether rounding is enabled when bars are centered. If false, using * rounding with centered bars will result in a warning and rounding will be ignored. This flag * has no effect if bars are not {@link dc.barChart#centerBar centered}. * When using standard d3.js rounding methods, the brush often doesn't align correctly with * centered bars since the bars are offset. The rounding function must add an offset to * compensate, such as in the following example. * @method alwaysUseRounding * @memberof dc.barChart * @instance * @example * chart.round(function(n) { return Math.floor(n) + 0.5; }); * @param {Boolean} [alwaysUseRounding=false] * @returns {Boolean|dc.barChart} */ _chart.alwaysUseRounding = function (alwaysUseRounding) { if (!arguments.length) { return _alwaysUseRounding; } _alwaysUseRounding = alwaysUseRounding; return _chart; }; function colorFilter (color, inv) { return function () { var item = d3.select(this); var match = item.attr('fill') === color; return inv ? !match : match; }; } _chart.legendHighlight = function (d) { if (!_chart.isLegendableHidden(d)) { _chart.g().selectAll('rect.bar') .classed('highlight', colorFilter(d.color)) .classed('fadeout', colorFilter(d.color, true)); } }; _chart.legendReset = function () { _chart.g().selectAll('rect.bar') .classed('highlight', false) .classed('fadeout', false); }; dc.override(_chart, 'xAxisMax', function () { var max = this._xAxisMax(); if ('resolution' in _chart.xUnits()) { var res = _chart.xUnits().resolution; max += res; } return max; }); return _chart.anchor(parent, chartGroup); }; /** * Concrete line/area chart implementation. * * Examples: * - {@link http://dc-js.github.com/dc.js/ Nasdaq 100 Index} * - {@link http://dc-js.github.com/dc.js/crime/index.html Canadian City Crime Stats} * @class lineChart * @memberof dc * @mixes dc.stackMixin * @mixes dc.coordinateGridMixin * @example * // create a line chart under #chart-container1 element using the default global chart group * var chart1 = dc.lineChart('#chart-container1'); * // create a line chart under #chart-container2 element using chart group A * var chart2 = dc.lineChart('#chart-container2', 'chartGroupA'); * // create a sub-chart under a composite parent chart * var chart3 = dc.lineChart(compositeChart); * @param {String|node|d3.selection|dc.compositeChart} parent - Any valid * {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Selections.md#selecting-elements d3 single selector} * specifying a dom block element such as a div; or a dom element or d3 selection. If the line * chart is a sub-chart in a {@link dc.compositeChart Composite Chart} then pass in the parent * composite chart instance instead. * @param {String} [chartGroup] - The name of the chart group this chart instance should be placed in. * Interaction with a chart will only trigger events and redraws within the chart's group. * @returns {dc.lineChart} */ dc.lineChart = function (parent, chartGroup) { var DEFAULT_DOT_RADIUS = 5; var TOOLTIP_G_CLASS = 'dc-tooltip'; var DOT_CIRCLE_CLASS = 'dot'; var Y_AXIS_REF_LINE_CLASS = 'yRef'; var X_AXIS_REF_LINE_CLASS = 'xRef'; var DEFAULT_DOT_OPACITY = 1e-6; var LABEL_PADDING = 3; var _chart = dc.stackMixin(dc.coordinateGridMixin({})); var _renderArea = false; var _dotRadius = DEFAULT_DOT_RADIUS; var _dataPointRadius = null; var _dataPointFillOpacity = DEFAULT_DOT_OPACITY; var _dataPointStrokeOpacity = DEFAULT_DOT_OPACITY; var _interpolate = 'linear'; var _tension = 0.7; var _defined; var _dashStyle; var _xyTipsOn = true; _chart.transitionDuration(500); _chart.transitionDelay(0); _chart._rangeBandPadding(1); _chart.plotData = function () { var chartBody = _chart.chartBodyG(); var layersList = chartBody.select('g.stack-list'); if (layersList.empty()) { layersList = chartBody.append('g').attr('class', 'stack-list'); } var layers = layersList.selectAll('g.stack').data(_chart.data()); var layersEnter = layers .enter() .append('g') .attr('class', function (d, i) { return 'stack ' + '_' + i; }); drawLine(layersEnter, layers); drawArea(layersEnter, layers); drawDots(chartBody, layers); if (_chart.renderLabel()) { drawLabels(layers); } }; /** * Gets or sets the interpolator to use for lines drawn, by string name, allowing e.g. step * functions, splines, and cubic interpolation. This is passed to * {@link https://github.com/d3/d3-3.x-api-reference/blob/master/SVG-Shapes.md#line_interpolate d3.svg.line.interpolate} and * {@link https://github.com/d3/d3-3.x-api-reference/blob/master/SVG-Shapes.md#area_interpolate d3.svg.area.interpolate}, * where you can find a complete list of valid arguments. * @method interpolate * @memberof dc.lineChart * @instance * @see {@link https://github.com/d3/d3-3.x-api-reference/blob/master/SVG-Shapes.md#line_interpolate d3.svg.line.interpolate} * @see {@link https://github.com/d3/d3-3.x-api-reference/blob/master/SVG-Shapes.md#area_interpolate d3.svg.area.interpolate} * @param {String} [interpolate='linear'] * @returns {String|dc.lineChart} */ _chart.interpolate = function (interpolate) { if (!arguments.length) { return _interpolate; } _interpolate = interpolate; return _chart; }; /** * Gets or sets the tension to use for lines drawn, in the range 0 to 1. * This parameter further customizes the interpolation behavior. It is passed to * {@link https://github.com/d3/d3-3.x-api-reference/blob/master/SVG-Shapes.md#line_tension d3.svg.line.tension} and * {@link https://github.com/d3/d3-3.x-api-reference/blob/master/SVG-Shapes.md#area_tension d3.svg.area.tension}. * @method tension * @memberof dc.lineChart * @instance * @see {@link https://github.com/d3/d3-3.x-api-reference/blob/master/SVG-Shapes.md#line_interpolate d3.svg.line.interpolate} * @see {@link https://github.com/d3/d3-3.x-api-reference/blob/master/SVG-Shapes.md#area_interpolate d3.svg.area.interpolate} * @param {Number} [tension=0.7] * @returns {Number|dc.lineChart} */ _chart.tension = function (tension) { if (!arguments.length) { return _tension; } _tension = tension; return _chart; }; /** * Gets or sets a function that will determine discontinuities in the line which should be * skipped: the path will be broken into separate subpaths if some points are undefined. * This function is passed to * {@link https://github.com/d3/d3-3.x-api-reference/blob/master/SVG-Shapes.md#line_defined d3.svg.line.defined} * * Note: crossfilter will sometimes coerce nulls to 0, so you may need to carefully write * custom reduce functions to get this to work, depending on your data. See * {@link https://github.com/dc-js/dc.js/issues/615#issuecomment-49089248 this GitHub comment} * for more details and an example. * @method defined * @memberof dc.lineChart * @instance * @see {@link https://github.com/d3/d3-3.x-api-reference/blob/master/SVG-Shapes.md#line_defined d3.svg.line.defined} * @param {Function} [defined] * @returns {Function|dc.lineChart} */ _chart.defined = function (defined) { if (!arguments.length) { return _defined; } _defined = defined; return _chart; }; /** * Set the line's d3 dashstyle. This value becomes the 'stroke-dasharray' of line. Defaults to empty * array (solid line). * @method dashStyle * @memberof dc.lineChart * @instance * @see {@link https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-dasharray stroke-dasharray} * @example * // create a Dash Dot Dot Dot * chart.dashStyle([3,1,1,1]); * @param {Array} [dashStyle=[]] * @returns {Array|dc.lineChart} */ _chart.dashStyle = function (dashStyle) { if (!arguments.length) { return _dashStyle; } _dashStyle = dashStyle; return _chart; }; /** * Get or set render area flag. If the flag is set to true then the chart will render the area * beneath each line and the line chart effectively becomes an area chart. * @method renderArea * @memberof dc.lineChart * @instance * @param {Boolean} [renderArea=false] * @returns {Boolean|dc.lineChart} */ _chart.renderArea = function (renderArea) { if (!arguments.length) { return _renderArea; } _renderArea = renderArea; return _chart; }; function colors (d, i) { return _chart.getColor.call(d, d.values, i); } function drawLine (layersEnter, layers) { var line = d3.svg.line() .x(function (d) { return _chart.x()(d.x); }) .y(function (d) { return _chart.y()(d.y + d.y0); }) .interpolate(_interpolate) .tension(_tension); if (_defined) { line.defined(_defined); } var path = layersEnter.append('path') .attr('class', 'line') .attr('stroke', colors); if (_dashStyle) { path.attr('stroke-dasharray', _dashStyle); } dc.transition(layers.select('path.line'), _chart.transitionDuration(), _chart.transitionDelay()) //.ease('linear') .attr('stroke', colors) .attr('d', function (d) { return safeD(line(d.values)); }); } function drawArea (layersEnter, layers) { if (_renderArea) { var area = d3.svg.area() .x(function (d) { return _chart.x()(d.x); }) .y(function (d) { return _chart.y()(d.y + d.y0); }) .y0(function (d) { return _chart.y()(d.y0); }) .interpolate(_interpolate) .tension(_tension); if (_defined) { area.defined(_defined); } layersEnter.append('path') .attr('class', 'area') .attr('fill', colors) .attr('d', function (d) { return safeD(area(d.values)); }); dc.transition(layers.select('path.area'), _chart.transitionDuration(), _chart.transitionDelay()) //.ease('linear') .attr('fill', colors) .attr('d', function (d) { return safeD(area(d.values)); }); } } function safeD (d) { return (!d || d.indexOf('NaN') >= 0) ? 'M0,0' : d; } function drawDots (chartBody, layers) { if (_chart.xyTipsOn() === 'always' || (!_chart.brushOn() && _chart.xyTipsOn())) { var tooltipListClass = TOOLTIP_G_CLASS + '-list'; var tooltips = chartBody.select('g.' + tooltipListClass); if (tooltips.empty()) { tooltips = chartBody.append('g').attr('class', tooltipListClass); } layers.each(function (d, layerIndex) { var points = d.values; if (_defined) { points = points.filter(_defined); } var g = tooltips.select('g.' + TOOLTIP_G_CLASS + '._' + layerIndex); if (g.empty()) { g = tooltips.append('g').attr('class', TOOLTIP_G_CLASS + ' _' + layerIndex); } createRefLines(g); var dots = g.selectAll('circle.' + DOT_CIRCLE_CLASS) .data(points, dc.pluck('x')); dots.enter() .append('circle') .attr('class', DOT_CIRCLE_CLASS) .attr('r', getDotRadius()) .style('fill-opacity', _dataPointFillOpacity) .style('stroke-opacity', _dataPointStrokeOpacity) .attr('fill', _chart.getColor) .on('mousemove', function () { var dot = d3.select(this); showDot(dot); showRefLines(dot, g); }) .on('mouseout', function () { var dot = d3.select(this); hideDot(dot); hideRefLines(g); }); dots.call(renderTitle, d); dc.transition(dots, _chart.transitionDuration()) .attr('cx', function (d) { return dc.utils.safeNumber(_chart.x()(d.x)); }) .attr('cy', function (d) { return dc.utils.safeNumber(_chart.y()(d.y + d.y0)); }) .attr('fill', _chart.getColor); dots.exit().remove(); }); } } _chart.label(function (d) { return dc.utils.printSingleValue(d.y0 + d.y); }, false); function drawLabels (layers) { layers.each(function (d, layerIndex) { var layer = d3.select(this); var labels = layer.selectAll('text.lineLabel') .data(d.values, dc.pluck('x')); labels.enter() .append('text') .attr('class', 'lineLabel') .attr('text-anchor', 'middle'); dc.transition(labels, _chart.transitionDuration()) .attr('x', function (d) { return dc.utils.safeNumber(_chart.x()(d.x)); }) .attr('y', function (d) { var y = _chart.y()(d.y + d.y0) - LABEL_PADDING; return dc.utils.safeNumber(y); }) .text(function (d) { return _chart.label()(d); }); dc.transition(labels.exit(), _chart.transitionDuration()) .attr('height', 0) .remove(); }); } function createRefLines (g) { var yRefLine = g.select('path.' + Y_AXIS_REF_LINE_CLASS).empty() ? g.append('path').attr('class', Y_AXIS_REF_LINE_CLASS) : g.select('path.' + Y_AXIS_REF_LINE_CLASS); yRefLine.style('display', 'none').attr('stroke-dasharray', '5,5'); var xRefLine = g.select('path.' + X_AXIS_REF_LINE_CLASS).empty() ? g.append('path').attr('class', X_AXIS_REF_LINE_CLASS) : g.select('path.' + X_AXIS_REF_LINE_CLASS); xRefLine.style('display', 'none').attr('stroke-dasharray', '5,5'); } function showDot (dot) { dot.style('fill-opacity', 0.8); dot.style('stroke-opacity', 0.8); dot.attr('r', _dotRadius); return dot; } function showRefLines (dot, g) { var x = dot.attr('cx'); var y = dot.attr('cy'); var yAxisX = (_chart._yAxisX() - _chart.margins().left); var yAxisRefPathD = 'M' + yAxisX + ' ' + y + 'L' + (x) + ' ' + (y); var xAxisRefPathD = 'M' + x + ' ' + _chart.yAxisHeight() + 'L' + x + ' ' + y; g.select('path.' + Y_AXIS_REF_LINE_CLASS).style('display', '').attr('d', yAxisRefPathD); g.select('path.' + X_AXIS_REF_LINE_CLASS).style('display', '').attr('d', xAxisRefPathD); } function getDotRadius () { return _dataPointRadius || _dotRadius; } function hideDot (dot) { dot.style('fill-opacity', _dataPointFillOpacity) .style('stroke-opacity', _dataPointStrokeOpacity) .attr('r', getDotRadius()); } function hideRefLines (g) { g.select('path.' + Y_AXIS_REF_LINE_CLASS).style('display', 'none'); g.select('path.' + X_AXIS_REF_LINE_CLASS).style('display', 'none'); } function renderTitle (dot, d) { if (_chart.renderTitle()) { dot.select('title').remove(); dot.append('title').text(dc.pluck('data', _chart.title(d.name))); } } /** * Turn on/off the mouseover behavior of an individual data point which renders a circle and x/y axis * dashed lines back to each respective axis. This is ignored if the chart * {@link dc.coordinateGridMixin#brushOn brush} is on * @method xyTipsOn * @memberof dc.lineChart * @instance * @param {Boolean} [xyTipsOn=false] * @returns {Boolean|dc.lineChart} */ _chart.xyTipsOn = function (xyTipsOn) { if (!arguments.length) { return _xyTipsOn; } _xyTipsOn = xyTipsOn; return _chart; }; /** * Get or set the radius (in px) for dots displayed on the data points. * @method dotRadius * @memberof dc.lineChart * @instance * @param {Number} [dotRadius=5] * @returns {Number|dc.lineChart} */ _chart.dotRadius = function (dotRadius) { if (!arguments.length) { return _dotRadius; } _dotRadius = dotRadius; return _chart; }; /** * Always show individual dots for each datapoint. * * If `options` is falsy, it disables data point rendering. If no `options` are provided, the * current `options` values are instead returned. * @method renderDataPoints * @memberof dc.lineChart * @instance * @example * chart.renderDataPoints({radius: 2, fillOpacity: 0.8, strokeOpacity: 0.8}) * @param {{fillOpacity: Number, strokeOpacity: Number, radius: Number}} [options={fillOpacity: 0.8, strokeOpacity: 0.8, radius: 2}] * @returns {{fillOpacity: Number, strokeOpacity: Number, radius: Number}|dc.lineChart} */ _chart.renderDataPoints = function (options) { if (!arguments.length) { return { fillOpacity: _dataPointFillOpacity, strokeOpacity: _dataPointStrokeOpacity, radius: _dataPointRadius }; } else if (!options) { _dataPointFillOpacity = DEFAULT_DOT_OPACITY; _dataPointStrokeOpacity = DEFAULT_DOT_OPACITY; _dataPointRadius = null; } else { _dataPointFillOpacity = options.fillOpacity || 0.8; _dataPointStrokeOpacity = options.strokeOpacity || 0.8; _dataPointRadius = options.radius || 2; } return _chart; }; function colorFilter (color, dashstyle, inv) { return function () { var item = d3.select(this); var match = (item.attr('stroke') === color && item.attr('stroke-dasharray') === ((dashstyle instanceof Array) ? dashstyle.join(',') : null)) || item.attr('fill') === color; return inv ? !match : match; }; } _chart.legendHighlight = function (d) { if (!_chart.isLegendableHidden(d)) { _chart.g().selectAll('path.line, path.area') .classed('highlight', colorFilter(d.color, d.dashstyle)) .classed('fadeout', colorFilter(d.color, d.dashstyle, true)); } }; _chart.legendReset = function () { _chart.g().selectAll('path.line, path.area') .classed('highlight', false) .classed('fadeout', false); }; dc.override(_chart, 'legendables', function () { var legendables = _chart._legendables(); if (!_dashStyle) { return legendables; } return legendables.map(function (l) { l.dashstyle = _dashStyle; return l; }); }); return _chart.anchor(parent, chartGroup); }; /** * The data count widget is a simple widget designed to display the number of records selected by the * current filters out of the total number of records in the data set. Once created the data count widget * will automatically update the text content of child elements with the following classes: * * * `.total-count` - total number of records * * `.filter-count` - number of records matched by the current filters * * Note: this widget works best for the specific case of showing the number of records out of a * total. If you want a more general-purpose numeric display, please use the * {@link dc.numberDisplay} widget instead. * * Examples: * - {@link http://dc-js.github.com/dc.js/ Nasdaq 100 Index} * @class dataCount * @memberof dc * @mixes dc.baseMixin * @example * var ndx = crossfilter(data); * var all = ndx.groupAll(); * * dc.dataCount('.dc-data-count') * .dimension(ndx) * .group(all); * @param {String|node|d3.selection} parent - Any valid * {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Selections.md#selecting-elements d3 single selector} specifying * a dom block element such as a div; or a dom element or d3 selection. * @param {String} [chartGroup] - The name of the chart group this chart instance should be placed in. * Interaction with a chart will only trigger events and redraws within the chart's group. * @returns {dc.dataCount} */ dc.dataCount = function (parent, chartGroup) { var _formatNumber = d3.format(',d'); var _chart = dc.baseMixin({}); var _html = {some: '', all: ''}; /** * Gets or sets an optional object specifying HTML templates to use depending how many items are * selected. The text `%total-count` will replaced with the total number of records, and the text * `%filter-count` will be replaced with the number of selected records. * - all: HTML template to use if all items are selected * - some: HTML template to use if not all items are selected * @method html * @memberof dc.dataCount * @instance * @example * counter.html({ * some: '%filter-count out of %total-count records selected', * all: 'All records selected. Click on charts to apply filters' * }) * @param {{some:String, all: String}} [options] * @returns {{some:String, all: String}|dc.dataCount} */ _chart.html = function (options) { if (!arguments.length) { return _html; } if (options.all) { _html.all = options.all; } if (options.some) { _html.some = options.some; } return _chart; }; /** * Gets or sets an optional function to format the filter count and total count. * @method formatNumber * @memberof dc.dataCount * @instance * @see {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Formatting.md d3.format} * @example * counter.formatNumber(d3.format('.2g')) * @param {Function} [formatter=d3.format('.2g')] * @returns {Function|dc.dataCount} */ _chart.formatNumber = function (formatter) { if (!arguments.length) { return _formatNumber; } _formatNumber = formatter; return _chart; }; _chart._doRender = function () { var tot = _chart.dimension().size(), val = _chart.group().value(); var all = _formatNumber(tot); var selected = _formatNumber(val); if ((tot === val) && (_html.all !== '')) { _chart.root().html(_html.all.replace('%total-count', all).replace('%filter-count', selected)); } else if (_html.some !== '') { _chart.root().html(_html.some.replace('%total-count', all).replace('%filter-count', selected)); } else { _chart.selectAll('.total-count').text(all); _chart.selectAll('.filter-count').text(selected); } return _chart; }; _chart._doRedraw = function () { return _chart._doRender(); }; return _chart.anchor(parent, chartGroup); }; /** * The data table is a simple widget designed to list crossfilter focused data set (rows being * filtered) in a good old tabular fashion. * * Note: Unlike other charts, the data table (and data grid chart) use the {@link dc.dataTable#group group} attribute as a * keying function for {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Arrays.md#nest nesting} the data * together in groups. Do not pass in a crossfilter group as this will not work. * * Another interesting feature of the data table is that you can pass a crossfilter group to the `dimension`, as * long as you specify the {@link dc.dataTable#order order} as `d3.descending`, since the data * table will use `dimension.top()` to fetch the data in that case, and the method is equally * supported on the crossfilter group as the crossfilter dimension. * * Examples: * - {@link http://dc-js.github.com/dc.js/ Nasdaq 100 Index} * - {@link http://dc-js.github.io/dc.js/examples/table-on-aggregated-data.html dataTable on a crossfilter group} * ({@link https://github.com/dc-js/dc.js/blob/develop/web/examples/table-on-aggregated-data.html source}) * @class dataTable * @memberof dc * @mixes dc.baseMixin * @param {String|node|d3.selection} parent - Any valid * {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Selections.md#selecting-elements d3 single selector} specifying * a dom block element such as a div; or a dom element or d3 selection. * @param {String} [chartGroup] - The name of the chart group this chart instance should be placed in. * Interaction with a chart will only trigger events and redraws within the chart's group. * @returns {dc.dataTable} */ dc.dataTable = function (parent, chartGroup) { var LABEL_CSS_CLASS = 'dc-table-label'; var ROW_CSS_CLASS = 'dc-table-row'; var COLUMN_CSS_CLASS = 'dc-table-column'; var GROUP_CSS_CLASS = 'dc-table-group'; var HEAD_CSS_CLASS = 'dc-table-head'; var _chart = dc.baseMixin({}); var _size = 25; var _columns = []; var _sortBy = function (d) { return d; }; var _order = d3.ascending; var _beginSlice = 0; var _endSlice; var _showGroups = true; _chart._doRender = function () { _chart.selectAll('tbody').remove(); renderRows(renderGroups()); return _chart; }; _chart._doColumnValueFormat = function (v, d) { return ((typeof v === 'function') ? v(d) : // v as function ((typeof v === 'string') ? d[v] : // v is field name string v.format(d) // v is Object, use fn (element 2) ) ); }; _chart._doColumnHeaderFormat = function (d) { // if 'function', convert to string representation // show a string capitalized // if an object then display its label string as-is. return (typeof d === 'function') ? _chart._doColumnHeaderFnToString(d) : ((typeof d === 'string') ? _chart._doColumnHeaderCapitalize(d) : String(d.label)); }; _chart._doColumnHeaderCapitalize = function (s) { // capitalize return s.charAt(0).toUpperCase() + s.slice(1); }; _chart._doColumnHeaderFnToString = function (f) { // columnString(f) { var s = String(f); var i1 = s.indexOf('return '); if (i1 >= 0) { var i2 = s.lastIndexOf(';'); if (i2 >= 0) { s = s.substring(i1 + 7, i2); var i3 = s.indexOf('numberFormat'); if (i3 >= 0) { s = s.replace('numberFormat', ''); } } } return s; }; function renderGroups () { // The 'original' example uses all 'functions'. // If all 'functions' are used, then don't remove/add a header, and leave // the html alone. This preserves the functionality of earlier releases. // A 2nd option is a string representing a field in the data. // A third option is to supply an Object such as an array of 'information', and // supply your own _doColumnHeaderFormat and _doColumnValueFormat functions to // create what you need. var bAllFunctions = true; _columns.forEach(function (f) { bAllFunctions = bAllFunctions & (typeof f === 'function'); }); if (!bAllFunctions) { // ensure one thead var thead = _chart.selectAll('thead').data([0]); thead.enter().append('thead'); thead.exit().remove(); // with one tr var headrow = thead.selectAll('tr').data([0]); headrow.enter().append('tr'); headrow.exit().remove(); // with a th for each column var headcols = headrow.selectAll('th') .data(_columns); headcols.enter().append('th'); headcols.exit().remove(); headcols .attr('class', HEAD_CSS_CLASS) .html(function (d) { return (_chart._doColumnHeaderFormat(d)); }); } var groups = _chart.root().selectAll('tbody') .data(nestEntries(), function (d) { return _chart.keyAccessor()(d); }); var rowGroup = groups .enter() .append('tbody'); if (_showGroups === true) { rowGroup .append('tr') .attr('class', GROUP_CSS_CLASS) .append('td') .attr('class', LABEL_CSS_CLASS) .attr('colspan', _columns.length) .html(function (d) { return _chart.keyAccessor()(d); }); } groups.exit().remove(); return rowGroup; } function nestEntries () { var entries; if (_order === d3.ascending) { entries = _chart.dimension().bottom(_size); } else { entries = _chart.dimension().top(_size); } return d3.nest() .key(_chart.group()) .sortKeys(_order) .entries(entries.sort(function (a, b) { return _order(_sortBy(a), _sortBy(b)); }).slice(_beginSlice, _endSlice)); } function renderRows (groups) { var rows = groups.order() .selectAll('tr.' + ROW_CSS_CLASS) .data(function (d) { return d.values; }); var rowEnter = rows.enter() .append('tr') .attr('class', ROW_CSS_CLASS); _columns.forEach(function (v, i) { rowEnter.append('td') .attr('class', COLUMN_CSS_CLASS + ' _' + i) .html(function (d) { return _chart._doColumnValueFormat(v, d); }); }); rows.exit().remove(); return rows; } _chart._doRedraw = function () { return _chart._doRender(); }; /** * Get or set the group function for the data table. The group function takes a data row and * returns the key to specify to {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Arrays.md#d3_nest d3.nest} * to split rows into groups. * * Do not pass in a crossfilter group as this will not work. * @method group * @memberof dc.dataTable * @instance * @example * // group rows by the value of their field * chart * .group(function(d) { return d.field; }) * @param {Function} groupFunction Function taking a row of data and returning the nest key. * @returns {Function|dc.dataTable} */ /** * Get or set the table size which determines the number of rows displayed by the widget. * @method size * @memberof dc.dataTable * @instance * @param {Number} [size=25] * @returns {Number|dc.dataTable} */ _chart.size = function (size) { if (!arguments.length) { return _size; } _size = size; return _chart; }; /** * Get or set the index of the beginning slice which determines which entries get displayed * by the widget. Useful when implementing pagination. * * Note: the sortBy function will determine how the rows are ordered for pagination purposes. * See the {@link http://dc-js.github.io/dc.js/examples/table-pagination.html table pagination example} * to see how to implement the pagination user interface using `beginSlice` and `endSlice`. * @method beginSlice * @memberof dc.dataTable * @instance * @param {Number} [beginSlice=0] * @returns {Number|dc.dataTable} */ _chart.beginSlice = function (beginSlice) { if (!arguments.length) { return _beginSlice; } _beginSlice = beginSlice; return _chart; }; /** * Get or set the index of the end slice which determines which entries get displayed by the * widget. Useful when implementing pagination. See {@link dc.dataTable#beginSlice `beginSlice`} for more information. * @method endSlice * @memberof dc.dataTable * @instance * @param {Number|undefined} [endSlice=undefined] * @returns {Number|dc.dataTable} */ _chart.endSlice = function (endSlice) { if (!arguments.length) { return _endSlice; } _endSlice = endSlice; return _chart; }; /** * Get or set column functions. The data table widget supports several methods of specifying the * columns to display. * * The original method uses an array of functions to generate dynamic columns. Column functions * are simple javascript functions with only one input argument `d` which represents a row in * the data set. The return value of these functions will be used to generate the content for * each cell. However, this method requires the HTML for the table to have a fixed set of column * headers. * *
chart.columns([
     *     function(d) { return d.date; },
     *     function(d) { return d.open; },
     *     function(d) { return d.close; },
     *     function(d) { return numberFormat(d.close - d.open); },
     *     function(d) { return d.volume; }
     * ]);
     * 
* * In the second method, you can list the columns to read from the data without specifying it as * a function, except where necessary (ie, computed columns). Note the data element name is * capitalized when displayed in the table header. You can also mix in functions as necessary, * using the third `{label, format}` form, as shown below. * *
chart.columns([
     *     "date",    // d["date"], ie, a field accessor; capitalized automatically
     *     "open",    // ...
     *     "close",   // ...
     *     {
     *         label: "Change",
     *         format: function (d) {
     *             return numberFormat(d.close - d.open);
     *         }
     *     },
     *     "volume"   // d["volume"], ie, a field accessor; capitalized automatically
     * ]);
     * 
* * In the third example, we specify all fields using the `{label, format}` method: *
chart.columns([
     *     {
     *         label: "Date",
     *         format: function (d) { return d.date; }
     *     },
     *     {
     *         label: "Open",
     *         format: function (d) { return numberFormat(d.open); }
     *     },
     *     {
     *         label: "Close",
     *         format: function (d) { return numberFormat(d.close); }
     *     },
     *     {
     *         label: "Change",
     *         format: function (d) { return numberFormat(d.close - d.open); }
     *     },
     *     {
     *         label: "Volume",
     *         format: function (d) { return d.volume; }
     *     }
     * ]);
     * 
* * You may wish to override the dataTable functions `_doColumnHeaderCapitalize` and * `_doColumnHeaderFnToString`, which are used internally to translate the column information or * function into a displayed header. The first one is used on the "string" column specifier; the * second is used to transform a stringified function into something displayable. For the Stock * example, the function for Change becomes the table header **d.close - d.open**. * * Finally, you can even specify a completely different form of column definition. To do this, * override `_chart._doColumnHeaderFormat` and `_chart._doColumnValueFormat` Be aware that * fields without numberFormat specification will be displayed just as they are stored in the * data, unformatted. * @method columns * @memberof dc.dataTable * @instance * @param {Array} [columns=[]] * @returns {Array}|dc.dataTable} */ _chart.columns = function (columns) { if (!arguments.length) { return _columns; } _columns = columns; return _chart; }; /** * Get or set sort-by function. This function works as a value accessor at row level and returns a * particular field to be sorted by. * @method sortBy * @memberof dc.dataTable * @instance * @example * chart.sortBy(function(d) { * return d.date; * }); * @param {Function} [sortBy=identity function] * @returns {Function|dc.dataTable} */ _chart.sortBy = function (sortBy) { if (!arguments.length) { return _sortBy; } _sortBy = sortBy; return _chart; }; /** * Get or set sort order. If the order is `d3.ascending`, the data table will use * `dimension().bottom()` to fetch the data; otherwise it will use `dimension().top()` * @method order * @memberof dc.dataTable * @instance * @see {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Arrays.md#d3_ascending d3.ascending} * @see {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Arrays.md#d3_descending d3.descending} * @example * chart.order(d3.descending); * @param {Function} [order=d3.ascending] * @returns {Function|dc.dataTable} */ _chart.order = function (order) { if (!arguments.length) { return _order; } _order = order; return _chart; }; /** * Get or set if group rows will be shown. The dataTable {@link dc.dataTable#group group} * function must be specified even if groups are not shown. * @method showGroups * @memberof dc.dataTable * @instance * @example * chart * .group([value], [name]) * .showGroups(true|false); * @param {Boolean} [showGroups=true] * @returns {Boolean|dc.dataTable} */ _chart.showGroups = function (showGroups) { if (!arguments.length) { return _showGroups; } _showGroups = showGroups; return _chart; }; return _chart.anchor(parent, chartGroup); }; /** * Data grid is a simple widget designed to list the filtered records, providing * a simple way to define how the items are displayed. * * Note: Unlike other charts, the data grid chart (and data table) use the {@link dc.dataGrid#group group} attribute as a keying function * for {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Arrays.md#nest nesting} the data together in groups. * Do not pass in a crossfilter group as this will not work. * * Examples: * - {@link http://europarl.me/dc.js/web/ep/index.html List of members of the european parliament} * @class dataGrid * @memberof dc * @mixes dc.baseMixin * @param {String|node|d3.selection} parent - Any valid * {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Selections.md#selecting-elements d3 single selector} specifying * a dom block element such as a div; or a dom element or d3 selection. * @param {String} [chartGroup] - The name of the chart group this chart instance should be placed in. * Interaction with a chart will only trigger events and redraws within the chart's group. * @returns {dc.dataGrid} */ dc.dataGrid = function (parent, chartGroup) { var LABEL_CSS_CLASS = 'dc-grid-label'; var ITEM_CSS_CLASS = 'dc-grid-item'; var GROUP_CSS_CLASS = 'dc-grid-group'; var GRID_CSS_CLASS = 'dc-grid-top'; var _chart = dc.baseMixin({}); var _size = 999; // shouldn't be needed, but you might var _html = function (d) { return 'you need to provide an html() handling param: ' + JSON.stringify(d); }; var _sortBy = function (d) { return d; }; var _order = d3.ascending; var _beginSlice = 0, _endSlice; var _htmlGroup = function (d) { return '

' + _chart.keyAccessor()(d) + '

'; }; _chart._doRender = function () { _chart.selectAll('div.' + GRID_CSS_CLASS).remove(); renderItems(renderGroups()); return _chart; }; function renderGroups () { var groups = _chart.root().selectAll('div.' + GRID_CSS_CLASS) .data(nestEntries(), function (d) { return _chart.keyAccessor()(d); }); var itemGroup = groups .enter() .append('div') .attr('class', GRID_CSS_CLASS); if (_htmlGroup) { itemGroup .html(function (d) { return _htmlGroup(d); }); } groups.exit().remove(); return itemGroup; } function nestEntries () { var entries = _chart.dimension().top(_size); return d3.nest() .key(_chart.group()) .sortKeys(_order) .entries(entries.sort(function (a, b) { return _order(_sortBy(a), _sortBy(b)); }).slice(_beginSlice, _endSlice)); } function renderItems (groups) { var items = groups.order() .selectAll('div.' + ITEM_CSS_CLASS) .data(function (d) { return d.values; }); items.enter() .append('div') .attr('class', ITEM_CSS_CLASS) .html(function (d) { return _html(d); }); items.exit().remove(); return items; } _chart._doRedraw = function () { return _chart._doRender(); }; /** * Get or set the group function for the data grid. The group function takes a data row and * returns the key to specify to {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Arrays.md#d3_nest d3.nest} * to split rows into groups. * * Do not pass in a crossfilter group as this will not work. * @method group * @memberof dc.dataGrid * @instance * @example * // group rows by the value of their field * chart * .group(function(d) { return d.field; }) * @param {Function} groupFunction Function taking a row of data and returning the nest key. * @returns {Function|dc.dataTable} */ /** * Get or set the index of the beginning slice which determines which entries get displayed by the widget. * Useful when implementing pagination. * @method beginSlice * @memberof dc.dataGrid * @instance * @param {Number} [beginSlice=0] * @returns {Number|dc.dataGrid} */ _chart.beginSlice = function (beginSlice) { if (!arguments.length) { return _beginSlice; } _beginSlice = beginSlice; return _chart; }; /** * Get or set the index of the end slice which determines which entries get displayed by the widget. * Useful when implementing pagination. * @method endSlice * @memberof dc.dataGrid * @instance * @param {Number} [endSlice] * @returns {Number|dc.dataGrid} */ _chart.endSlice = function (endSlice) { if (!arguments.length) { return _endSlice; } _endSlice = endSlice; return _chart; }; /** * Get or set the grid size which determines the number of items displayed by the widget. * @method size * @memberof dc.dataGrid * @instance * @param {Number} [size=999] * @returns {Number|dc.dataGrid} */ _chart.size = function (size) { if (!arguments.length) { return _size; } _size = size; return _chart; }; /** * Get or set the function that formats an item. The data grid widget uses a * function to generate dynamic html. Use your favourite templating engine or * generate the string directly. * @method html * @memberof dc.dataGrid * @instance * @example * chart.html(function (d) { return '
'+data.exampleString+'
';}); * @param {Function} [html] * @returns {Function|dc.dataGrid} */ _chart.html = function (html) { if (!arguments.length) { return _html; } _html = html; return _chart; }; /** * Get or set the function that formats a group label. * @method htmlGroup * @memberof dc.dataGrid * @instance * @example * chart.htmlGroup (function (d) { return '

'.d.key . 'with ' . d.values.length .' items

'}); * @param {Function} [htmlGroup] * @returns {Function|dc.dataGrid} */ _chart.htmlGroup = function (htmlGroup) { if (!arguments.length) { return _htmlGroup; } _htmlGroup = htmlGroup; return _chart; }; /** * Get or set sort-by function. This function works as a value accessor at the item * level and returns a particular field to be sorted. * @method sortBy * @memberof dc.dataGrid * @instance * @example * chart.sortBy(function(d) { * return d.date; * }); * @param {Function} [sortByFunction] * @returns {Function|dc.dataGrid} */ _chart.sortBy = function (sortByFunction) { if (!arguments.length) { return _sortBy; } _sortBy = sortByFunction; return _chart; }; /** * Get or set sort the order function. * @method order * @memberof dc.dataGrid * @instance * @see {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Arrays.md#d3_ascending d3.ascending} * @see {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Arrays.md#d3_descending d3.descending} * @example * chart.order(d3.descending); * @param {Function} [order=d3.ascending] * @returns {Function|dc.dataGrid} */ _chart.order = function (order) { if (!arguments.length) { return _order; } _order = order; return _chart; }; return _chart.anchor(parent, chartGroup); }; /** * A concrete implementation of a general purpose bubble chart that allows data visualization using the * following dimensions: * - x axis position * - y axis position * - bubble radius * - color * * Examples: * - {@link http://dc-js.github.com/dc.js/ Nasdaq 100 Index} * - {@link http://dc-js.github.com/dc.js/vc/index.html US Venture Capital Landscape 2011} * @class bubbleChart * @memberof dc * @mixes dc.bubbleMixin * @mixes dc.coordinateGridMixin * @example * // create a bubble chart under #chart-container1 element using the default global chart group * var bubbleChart1 = dc.bubbleChart('#chart-container1'); * // create a bubble chart under #chart-container2 element using chart group A * var bubbleChart2 = dc.bubbleChart('#chart-container2', 'chartGroupA'); * @param {String|node|d3.selection} parent - Any valid * {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Selections.md#selecting-elements d3 single selector} specifying * a dom block element such as a div; or a dom element or d3 selection. * @param {String} [chartGroup] - The name of the chart group this chart instance should be placed in. * Interaction with a chart will only trigger events and redraws within the chart's group. * @returns {dc.bubbleChart} */ dc.bubbleChart = function (parent, chartGroup) { var _chart = dc.bubbleMixin(dc.coordinateGridMixin({})); _chart.transitionDuration(750); _chart.transitionDelay(0); var bubbleLocator = function (d) { return 'translate(' + (bubbleX(d)) + ',' + (bubbleY(d)) + ')'; }; _chart.plotData = function () { _chart.calculateRadiusDomain(); _chart.r().range([_chart.MIN_RADIUS, _chart.xAxisLength() * _chart.maxBubbleRelativeSize()]); var data = _chart.data(); var bubbleG = _chart.chartBodyG().selectAll('g.' + _chart.BUBBLE_NODE_CLASS) .data(data, function (d) { return d.key; }); if (_chart.sortBubbleSize()) { // update dom order based on sort bubbleG.order(); } renderNodes(bubbleG); updateNodes(bubbleG); removeNodes(bubbleG); _chart.fadeDeselectedArea(); }; function renderNodes (bubbleG) { var bubbleGEnter = bubbleG.enter().append('g'); bubbleGEnter .attr('class', _chart.BUBBLE_NODE_CLASS) .attr('transform', bubbleLocator) .append('circle').attr('class', function (d, i) { return _chart.BUBBLE_CLASS + ' _' + i; }) .on('click', _chart.onClick) .attr('fill', _chart.getColor) .attr('r', 0); dc.transition(bubbleG, _chart.transitionDuration(), _chart.transitionDelay()) .select('circle.' + _chart.BUBBLE_CLASS) .attr('r', function (d) { return _chart.bubbleR(d); }) .attr('opacity', function (d) { return (_chart.bubbleR(d) > 0) ? 1 : 0; }); _chart._doRenderLabel(bubbleGEnter); _chart._doRenderTitles(bubbleGEnter); } function updateNodes (bubbleG) { dc.transition(bubbleG, _chart.transitionDuration(), _chart.transitionDelay()) .attr('transform', bubbleLocator) .select('circle.' + _chart.BUBBLE_CLASS) .attr('fill', _chart.getColor) .attr('r', function (d) { return _chart.bubbleR(d); }) .attr('opacity', function (d) { return (_chart.bubbleR(d) > 0) ? 1 : 0; }); _chart.doUpdateLabels(bubbleG); _chart.doUpdateTitles(bubbleG); } function removeNodes (bubbleG) { bubbleG.exit().remove(); } function bubbleX (d) { var x = _chart.x()(_chart.keyAccessor()(d)); if (isNaN(x)) { x = 0; } return x; } function bubbleY (d) { var y = _chart.y()(_chart.valueAccessor()(d)); if (isNaN(y)) { y = 0; } return y; } _chart.renderBrush = function () { // override default x axis brush from parent chart }; _chart.redrawBrush = function () { // override default x axis brush from parent chart _chart.fadeDeselectedArea(); }; return _chart.anchor(parent, chartGroup); }; /** * Composite charts are a special kind of chart that render multiple charts on the same Coordinate * Grid. You can overlay (compose) different bar/line/area charts in a single composite chart to * achieve some quite flexible charting effects. * @class compositeChart * @memberof dc * @mixes dc.coordinateGridMixin * @example * // create a composite chart under #chart-container1 element using the default global chart group * var compositeChart1 = dc.compositeChart('#chart-container1'); * // create a composite chart under #chart-container2 element using chart group A * var compositeChart2 = dc.compositeChart('#chart-container2', 'chartGroupA'); * @param {String|node|d3.selection} parent - Any valid * {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Selections.md#selecting-elements d3 single selector} specifying * a dom block element such as a div; or a dom element or d3 selection. * @param {String} [chartGroup] - The name of the chart group this chart instance should be placed in. * Interaction with a chart will only trigger events and redraws within the chart's group. * @returns {dc.compositeChart} */ dc.compositeChart = function (parent, chartGroup) { var SUB_CHART_CLASS = 'sub'; var DEFAULT_RIGHT_Y_AXIS_LABEL_PADDING = 12; var _chart = dc.coordinateGridMixin({}); var _children = []; var _childOptions = {}; var _shareColors = false, _shareTitle = true, _alignYAxes = false; var _rightYAxis = d3.svg.axis(), _rightYAxisLabel = 0, _rightYAxisLabelPadding = DEFAULT_RIGHT_Y_AXIS_LABEL_PADDING, _rightY, _rightAxisGridLines = false; _chart._mandatoryAttributes([]); _chart.transitionDuration(500); _chart.transitionDelay(0); dc.override(_chart, '_generateG', function () { var g = this.__generateG(); for (var i = 0; i < _children.length; ++i) { var child = _children[i]; generateChildG(child, i); if (!child.dimension()) { child.dimension(_chart.dimension()); } if (!child.group()) { child.group(_chart.group()); } child.chartGroup(_chart.chartGroup()); child.svg(_chart.svg()); child.xUnits(_chart.xUnits()); child.transitionDuration(_chart.transitionDuration(), _chart.transitionDelay()); child.brushOn(_chart.brushOn()); child.renderTitle(_chart.renderTitle()); child.elasticX(_chart.elasticX()); } return g; }); _chart._brushing = function () { var extent = _chart.extendBrush(); var brushIsEmpty = _chart.brushIsEmpty(extent); for (var i = 0; i < _children.length; ++i) { _children[i].replaceFilter(brushIsEmpty ? null : extent); } }; _chart._prepareYAxis = function () { var left = (leftYAxisChildren().length !== 0); var right = (rightYAxisChildren().length !== 0); var ranges = calculateYAxisRanges(left, right); if (left) { prepareLeftYAxis(ranges); } if (right) { prepareRightYAxis(ranges); } if (leftYAxisChildren().length > 0 && !_rightAxisGridLines) { _chart._renderHorizontalGridLinesForAxis(_chart.g(), _chart.y(), _chart.yAxis()); } else if (rightYAxisChildren().length > 0) { _chart._renderHorizontalGridLinesForAxis(_chart.g(), _rightY, _rightYAxis); } }; _chart.renderYAxis = function () { if (leftYAxisChildren().length !== 0) { _chart.renderYAxisAt('y', _chart.yAxis(), _chart.margins().left); _chart.renderYAxisLabel('y', _chart.yAxisLabel(), -90); } if (rightYAxisChildren().length !== 0) { _chart.renderYAxisAt('yr', _chart.rightYAxis(), _chart.width() - _chart.margins().right); _chart.renderYAxisLabel('yr', _chart.rightYAxisLabel(), 90, _chart.width() - _rightYAxisLabelPadding); } }; function calculateYAxisRanges (left, right) { var lyAxisMin, lyAxisMax, ryAxisMin, ryAxisMax; var ranges; if (left) { lyAxisMin = yAxisMin(); lyAxisMax = yAxisMax(); } if (right) { ryAxisMin = rightYAxisMin(); ryAxisMax = rightYAxisMax(); } if (_chart.alignYAxes() && left && right) { ranges = alignYAxisRanges(lyAxisMin, lyAxisMax, ryAxisMin, ryAxisMax); } return ranges || { lyAxisMin: lyAxisMin, lyAxisMax: lyAxisMax, ryAxisMin: ryAxisMin, ryAxisMax: ryAxisMax }; } function alignYAxisRanges (lyAxisMin, lyAxisMax, ryAxisMin, ryAxisMax) { // since the two series will share a zero, each Y is just a multiple // of the other. and the ratio should be the ratio of the ranges of the // input data, so that they come out the same height. so we just min/max // note: both ranges already include zero due to the stack mixin (#667) // if #667 changes, we can reconsider whether we want data height or // height from zero to be equal. and it will be possible for the axes // to be aligned but not visible. var extentRatio = (ryAxisMax - ryAxisMin) / (lyAxisMax - lyAxisMin); return { lyAxisMin: Math.min(lyAxisMin, ryAxisMin / extentRatio), lyAxisMax: Math.max(lyAxisMax, ryAxisMax / extentRatio), ryAxisMin: Math.min(ryAxisMin, lyAxisMin * extentRatio), ryAxisMax: Math.max(ryAxisMax, lyAxisMax * extentRatio) }; } function prepareRightYAxis (ranges) { var needDomain = _chart.rightY() === undefined || _chart.elasticY(), needRange = needDomain || _chart.resizing(); if (_chart.rightY() === undefined) { _chart.rightY(d3.scale.linear()); } if (needDomain) { _chart.rightY().domain([ranges.ryAxisMin, ranges.ryAxisMax]); } if (needRange) { _chart.rightY().rangeRound([_chart.yAxisHeight(), 0]); } _chart.rightY().range([_chart.yAxisHeight(), 0]); _chart.rightYAxis(_chart.rightYAxis().scale(_chart.rightY())); _chart.rightYAxis().orient('right'); } function prepareLeftYAxis (ranges) { var needDomain = _chart.y() === undefined || _chart.elasticY(), needRange = needDomain || _chart.resizing(); if (_chart.y() === undefined) { _chart.y(d3.scale.linear()); } if (needDomain) { _chart.y().domain([ranges.lyAxisMin, ranges.lyAxisMax]); } if (needRange) { _chart.y().rangeRound([_chart.yAxisHeight(), 0]); } _chart.y().range([_chart.yAxisHeight(), 0]); _chart.yAxis(_chart.yAxis().scale(_chart.y())); _chart.yAxis().orient('left'); } function generateChildG (child, i) { child._generateG(_chart.g()); child.g().attr('class', SUB_CHART_CLASS + ' _' + i); } _chart.plotData = function () { for (var i = 0; i < _children.length; ++i) { var child = _children[i]; if (!child.g()) { generateChildG(child, i); } if (_shareColors) { child.colors(_chart.colors()); } child.x(_chart.x()); child.xAxis(_chart.xAxis()); if (child.useRightYAxis()) { child.y(_chart.rightY()); child.yAxis(_chart.rightYAxis()); } else { child.y(_chart.y()); child.yAxis(_chart.yAxis()); } child.plotData(); child._activateRenderlets(); } }; /** * Get or set whether to draw gridlines from the right y axis. Drawing from the left y axis is the * default behavior. This option is only respected when subcharts with both left and right y-axes * are present. * @method useRightAxisGridLines * @memberof dc.compositeChart * @instance * @param {Boolean} [useRightAxisGridLines=false] * @returns {Boolean|dc.compositeChart} */ _chart.useRightAxisGridLines = function (useRightAxisGridLines) { if (!arguments) { return _rightAxisGridLines; } _rightAxisGridLines = useRightAxisGridLines; return _chart; }; /** * Get or set chart-specific options for all child charts. This is equivalent to calling * {@link dc.baseMixin#options .options} on each child chart. * @method childOptions * @memberof dc.compositeChart * @instance * @param {Object} [childOptions] * @returns {Object|dc.compositeChart} */ _chart.childOptions = function (childOptions) { if (!arguments.length) { return _childOptions; } _childOptions = childOptions; _children.forEach(function (child) { child.options(_childOptions); }); return _chart; }; _chart.fadeDeselectedArea = function () { for (var i = 0; i < _children.length; ++i) { var child = _children[i]; child.brush(_chart.brush()); child.fadeDeselectedArea(); } }; /** * Set or get the right y axis label. * @method rightYAxisLabel * @memberof dc.compositeChart * @instance * @param {String} [rightYAxisLabel] * @param {Number} [padding] * @returns {String|dc.compositeChart} */ _chart.rightYAxisLabel = function (rightYAxisLabel, padding) { if (!arguments.length) { return _rightYAxisLabel; } _rightYAxisLabel = rightYAxisLabel; _chart.margins().right -= _rightYAxisLabelPadding; _rightYAxisLabelPadding = (padding === undefined) ? DEFAULT_RIGHT_Y_AXIS_LABEL_PADDING : padding; _chart.margins().right += _rightYAxisLabelPadding; return _chart; }; /** * Combine the given charts into one single composite coordinate grid chart. * @method compose * @memberof dc.compositeChart * @instance * @example * moveChart.compose([ * // when creating sub-chart you need to pass in the parent chart * dc.lineChart(moveChart) * .group(indexAvgByMonthGroup) // if group is missing then parent's group will be used * .valueAccessor(function (d){return d.value.avg;}) * // most of the normal functions will continue to work in a composed chart * .renderArea(true) * .stack(monthlyMoveGroup, function (d){return d.value;}) * .title(function (d){ * var value = d.value.avg?d.value.avg:d.value; * if(isNaN(value)) value = 0; * return dateFormat(d.key) + '\n' + numberFormat(value); * }), * dc.barChart(moveChart) * .group(volumeByMonthGroup) * .centerBar(true) * ]); * @param {Array} [subChartArray] * @returns {dc.compositeChart} */ _chart.compose = function (subChartArray) { _children = subChartArray; _children.forEach(function (child) { child.height(_chart.height()); child.width(_chart.width()); child.margins(_chart.margins()); if (_shareTitle) { child.title(_chart.title()); } child.options(_childOptions); }); return _chart; }; /** * Returns the child charts which are composed into the composite chart. * @method children * @memberof dc.compositeChart * @instance * @returns {Array} */ _chart.children = function () { return _children; }; /** * Get or set color sharing for the chart. If set, the {@link dc.colorMixin#colors .colors()} value from this chart * will be shared with composed children. Additionally if the child chart implements * Stackable and has not set a custom .colorAccessor, then it will generate a color * specific to its order in the composition. * @method shareColors * @memberof dc.compositeChart * @instance * @param {Boolean} [shareColors=false] * @returns {Boolean|dc.compositeChart} */ _chart.shareColors = function (shareColors) { if (!arguments.length) { return _shareColors; } _shareColors = shareColors; return _chart; }; /** * Get or set title sharing for the chart. If set, the {@link dc.baseMixin#title .title()} value from * this chart will be shared with composed children. * @method shareTitle * @memberof dc.compositeChart * @instance * @param {Boolean} [shareTitle=true] * @returns {Boolean|dc.compositeChart} */ _chart.shareTitle = function (shareTitle) { if (!arguments.length) { return _shareTitle; } _shareTitle = shareTitle; return _chart; }; /** * Get or set the y scale for the right axis. The right y scale is typically automatically * generated by the chart implementation. * @method rightY * @memberof dc.compositeChart * @instance * @see {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Scales.md d3.scale} * @param {d3.scale} [yScale] * @returns {d3.scale|dc.compositeChart} */ _chart.rightY = function (yScale) { if (!arguments.length) { return _rightY; } _rightY = yScale; _chart.rescale(); return _chart; }; /** * Get or set alignment between left and right y axes. A line connecting '0' on both y axis * will be parallel to x axis. This only has effect when {@link #dc.coordinateGridMixin+elasticY elasticY} is true. * @method alignYAxes * @memberof dc.compositeChart * @instance * @param {Boolean} [alignYAxes=false] * @returns {Chart} */ _chart.alignYAxes = function (alignYAxes) { if (!arguments.length) { return _alignYAxes; } _alignYAxes = alignYAxes; _chart.rescale(); return _chart; }; function leftYAxisChildren () { return _children.filter(function (child) { return !child.useRightYAxis(); }); } function rightYAxisChildren () { return _children.filter(function (child) { return child.useRightYAxis(); }); } function getYAxisMin (charts) { return charts.map(function (c) { return c.yAxisMin(); }); } delete _chart.yAxisMin; function yAxisMin () { return d3.min(getYAxisMin(leftYAxisChildren())); } function rightYAxisMin () { return d3.min(getYAxisMin(rightYAxisChildren())); } function getYAxisMax (charts) { return charts.map(function (c) { return c.yAxisMax(); }); } delete _chart.yAxisMax; function yAxisMax () { return dc.utils.add(d3.max(getYAxisMax(leftYAxisChildren())), _chart.yAxisPadding()); } function rightYAxisMax () { return dc.utils.add(d3.max(getYAxisMax(rightYAxisChildren())), _chart.yAxisPadding()); } function getAllXAxisMinFromChildCharts () { return _children.map(function (c) { return c.xAxisMin(); }); } dc.override(_chart, 'xAxisMin', function () { return dc.utils.subtract(d3.min(getAllXAxisMinFromChildCharts()), _chart.xAxisPadding()); }); function getAllXAxisMaxFromChildCharts () { return _children.map(function (c) { return c.xAxisMax(); }); } dc.override(_chart, 'xAxisMax', function () { return dc.utils.add(d3.max(getAllXAxisMaxFromChildCharts()), _chart.xAxisPadding()); }); _chart.legendables = function () { return _children.reduce(function (items, child) { if (_shareColors) { child.colors(_chart.colors()); } items.push.apply(items, child.legendables()); return items; }, []); }; _chart.legendHighlight = function (d) { for (var j = 0; j < _children.length; ++j) { var child = _children[j]; child.legendHighlight(d); } }; _chart.legendReset = function (d) { for (var j = 0; j < _children.length; ++j) { var child = _children[j]; child.legendReset(d); } }; _chart.legendToggle = function () { console.log('composite should not be getting legendToggle itself'); }; /** * Set or get the right y axis used by the composite chart. This function is most useful when y * axis customization is required. The y axis in dc.js is an instance of a [d3 axis * object](https://github.com/d3/d3-3.x-api-reference/blob/master/SVG-Axes.md#axis) therefore it supports any valid * d3 axis manipulation. * * **Caution**: The y axis is usually generated internally by dc; resetting it may cause * unexpected results. * @method rightYAxis * @memberof dc.compositeChart * @instance * @see {@link https://github.com/d3/d3-3.x-api-reference/blob/master/SVG-Axes.md#axis d3.svg.axis} * @example * // customize y axis tick format * chart.rightYAxis().tickFormat(function (v) {return v + '%';}); * // customize y axis tick values * chart.rightYAxis().tickValues([0, 100, 200, 300]); * @param {d3.svg.axis} [rightYAxis] * @returns {d3.svg.axis|dc.compositeChart} */ _chart.rightYAxis = function (rightYAxis) { if (!arguments.length) { return _rightYAxis; } _rightYAxis = rightYAxis; return _chart; }; return _chart.anchor(parent, chartGroup); }; /** * A series chart is a chart that shows multiple series of data overlaid on one chart, where the * series is specified in the data. It is a specialization of Composite Chart and inherits all * composite features other than recomposing the chart. * * Examples: * - {@link http://dc-js.github.io/dc.js/examples/series.html Series Chart} * @class seriesChart * @memberof dc * @mixes dc.compositeChart * @example * // create a series chart under #chart-container1 element using the default global chart group * var seriesChart1 = dc.seriesChart("#chart-container1"); * // create a series chart under #chart-container2 element using chart group A * var seriesChart2 = dc.seriesChart("#chart-container2", "chartGroupA"); * @param {String|node|d3.selection} parent - Any valid * {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Selections.md#selecting-elements d3 single selector} specifying * a dom block element such as a div; or a dom element or d3 selection. * @param {String} [chartGroup] - The name of the chart group this chart instance should be placed in. * Interaction with a chart will only trigger events and redraws within the chart's group. * @returns {dc.seriesChart} */ dc.seriesChart = function (parent, chartGroup) { var _chart = dc.compositeChart(parent, chartGroup); function keySort (a, b) { return d3.ascending(_chart.keyAccessor()(a), _chart.keyAccessor()(b)); } var _charts = {}; var _chartFunction = dc.lineChart; var _seriesAccessor; var _seriesSort = d3.ascending; var _valueSort = keySort; _chart._mandatoryAttributes().push('seriesAccessor', 'chart'); _chart.shareColors(true); _chart._preprocessData = function () { var keep = []; var childrenChanged; var nester = d3.nest().key(_seriesAccessor); if (_seriesSort) { nester.sortKeys(_seriesSort); } if (_valueSort) { nester.sortValues(_valueSort); } var nesting = nester.entries(_chart.data()); var children = nesting.map(function (sub, i) { var subChart = _charts[sub.key] || _chartFunction.call(_chart, _chart, chartGroup, sub.key, i); if (!_charts[sub.key]) { childrenChanged = true; } _charts[sub.key] = subChart; keep.push(sub.key); return subChart .dimension(_chart.dimension()) .group({all: d3.functor(sub.values)}, sub.key) .keyAccessor(_chart.keyAccessor()) .valueAccessor(_chart.valueAccessor()) .brushOn(_chart.brushOn()); }); // this works around the fact compositeChart doesn't really // have a removal interface Object.keys(_charts) .filter(function (c) {return keep.indexOf(c) === -1;}) .forEach(function (c) { clearChart(c); childrenChanged = true; }); _chart._compose(children); if (childrenChanged && _chart.legend()) { _chart.legend().render(); } }; function clearChart (c) { if (_charts[c].g()) { _charts[c].g().remove(); } delete _charts[c]; } function resetChildren () { Object.keys(_charts).map(clearChart); _charts = {}; } /** * Get or set the chart function, which generates the child charts. * @method chart * @memberof dc.seriesChart * @instance * @example * // put interpolation on the line charts used for the series * chart.chart(function(c) { return dc.lineChart(c).interpolate('basis'); }) * // do a scatter series chart * chart.chart(dc.scatterPlot) * @param {Function} [chartFunction=dc.lineChart] * @returns {Function|dc.seriesChart} */ _chart.chart = function (chartFunction) { if (!arguments.length) { return _chartFunction; } _chartFunction = chartFunction; resetChildren(); return _chart; }; /** * **mandatory** * * Get or set accessor function for the displayed series. Given a datum, this function * should return the series that datum belongs to. * @method seriesAccessor * @memberof dc.seriesChart * @instance * @example * // simple series accessor * chart.seriesAccessor(function(d) { return "Expt: " + d.key[0]; }) * @param {Function} [accessor] * @returns {Function|dc.seriesChart} */ _chart.seriesAccessor = function (accessor) { if (!arguments.length) { return _seriesAccessor; } _seriesAccessor = accessor; resetChildren(); return _chart; }; /** * Get or set a function to sort the list of series by, given series values. * @method seriesSort * @memberof dc.seriesChart * @instance * @see {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Arrays.md#d3_ascending d3.ascending} * @see {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Arrays.md#d3_descending d3.descending} * @example * chart.seriesSort(d3.descending); * @param {Function} [sortFunction=d3.ascending] * @returns {Function|dc.seriesChart} */ _chart.seriesSort = function (sortFunction) { if (!arguments.length) { return _seriesSort; } _seriesSort = sortFunction; resetChildren(); return _chart; }; /** * Get or set a function to sort each series values by. By default this is the key accessor which, * for example, will ensure a lineChart series connects its points in increasing key/x order, * rather than haphazardly. * @method valueSort * @memberof dc.seriesChart * @instance * @see {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Arrays.md#d3_ascending d3.ascending} * @see {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Arrays.md#d3_descending d3.descending} * @example * // Default value sort * _chart.valueSort(function keySort (a, b) { * return d3.ascending(_chart.keyAccessor()(a), _chart.keyAccessor()(b)); * }); * @param {Function} [sortFunction] * @returns {Function|dc.seriesChart} */ _chart.valueSort = function (sortFunction) { if (!arguments.length) { return _valueSort; } _valueSort = sortFunction; resetChildren(); return _chart; }; // make compose private _chart._compose = _chart.compose; delete _chart.compose; return _chart; }; /** * The geo choropleth chart is designed as an easy way to create a crossfilter driven choropleth map * from GeoJson data. This chart implementation was inspired by * {@link http://bl.ocks.org/4060606 the great d3 choropleth example}. * * Examples: * - {@link http://dc-js.github.com/dc.js/vc/index.html US Venture Capital Landscape 2011} * @class geoChoroplethChart * @memberof dc * @mixes dc.colorMixin * @mixes dc.baseMixin * @example * // create a choropleth chart under '#us-chart' element using the default global chart group * var chart1 = dc.geoChoroplethChart('#us-chart'); * // create a choropleth chart under '#us-chart2' element using chart group A * var chart2 = dc.compositeChart('#us-chart2', 'chartGroupA'); * @param {String|node|d3.selection} parent - Any valid * {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Selections.md#selecting-elements d3 single selector} specifying * a dom block element such as a div; or a dom element or d3 selection. * @param {String} [chartGroup] - The name of the chart group this chart instance should be placed in. * Interaction with a chart will only trigger events and redraws within the chart's group. * @returns {dc.geoChoroplethChart} */ dc.geoChoroplethChart = function (parent, chartGroup) { var _chart = dc.colorMixin(dc.baseMixin({})); _chart.colorAccessor(function (d) { return d || 0; }); var _geoPath = d3.geo.path(); var _projectionFlag; var _geoJsons = []; _chart._doRender = function () { _chart.resetSvg(); for (var layerIndex = 0; layerIndex < _geoJsons.length; ++layerIndex) { var states = _chart.svg().append('g') .attr('class', 'layer' + layerIndex); var regionG = states.selectAll('g.' + geoJson(layerIndex).name) .data(geoJson(layerIndex).data) .enter() .append('g') .attr('class', geoJson(layerIndex).name); regionG .append('path') .attr('fill', 'white') .attr('d', _geoPath); regionG.append('title'); plotData(layerIndex); } _projectionFlag = false; }; function plotData (layerIndex) { var data = generateLayeredData(); if (isDataLayer(layerIndex)) { var regionG = renderRegionG(layerIndex); renderPaths(regionG, layerIndex, data); renderTitle(regionG, layerIndex, data); } } function generateLayeredData () { var data = {}; var groupAll = _chart.data(); for (var i = 0; i < groupAll.length; ++i) { data[_chart.keyAccessor()(groupAll[i])] = _chart.valueAccessor()(groupAll[i]); } return data; } function isDataLayer (layerIndex) { return geoJson(layerIndex).keyAccessor; } function renderRegionG (layerIndex) { var regionG = _chart.svg() .selectAll(layerSelector(layerIndex)) .classed('selected', function (d) { return isSelected(layerIndex, d); }) .classed('deselected', function (d) { return isDeselected(layerIndex, d); }) .attr('class', function (d) { var layerNameClass = geoJson(layerIndex).name; var regionClass = dc.utils.nameToId(geoJson(layerIndex).keyAccessor(d)); var baseClasses = layerNameClass + ' ' + regionClass; if (isSelected(layerIndex, d)) { baseClasses += ' selected'; } if (isDeselected(layerIndex, d)) { baseClasses += ' deselected'; } return baseClasses; }); return regionG; } function layerSelector (layerIndex) { return 'g.layer' + layerIndex + ' g.' + geoJson(layerIndex).name; } function isSelected (layerIndex, d) { return _chart.hasFilter() && _chart.hasFilter(getKey(layerIndex, d)); } function isDeselected (layerIndex, d) { return _chart.hasFilter() && !_chart.hasFilter(getKey(layerIndex, d)); } function getKey (layerIndex, d) { return geoJson(layerIndex).keyAccessor(d); } function geoJson (index) { return _geoJsons[index]; } function renderPaths (regionG, layerIndex, data) { var paths = regionG .select('path') .attr('fill', function () { var currentFill = d3.select(this).attr('fill'); if (currentFill) { return currentFill; } return 'none'; }) .on('click', function (d) { return _chart.onClick(d, layerIndex); }); dc.transition(paths, _chart.transitionDuration(), _chart.transitionDelay()).attr('fill', function (d, i) { return _chart.getColor(data[geoJson(layerIndex).keyAccessor(d)], i); }); } _chart.onClick = function (d, layerIndex) { var selectedRegion = geoJson(layerIndex).keyAccessor(d); dc.events.trigger(function () { _chart.filter(selectedRegion); _chart.redrawGroup(); }); }; function renderTitle (regionG, layerIndex, data) { if (_chart.renderTitle()) { regionG.selectAll('title').text(function (d) { var key = getKey(layerIndex, d); var value = data[key]; return _chart.title()({key: key, value: value}); }); } } _chart._doRedraw = function () { for (var layerIndex = 0; layerIndex < _geoJsons.length; ++layerIndex) { plotData(layerIndex); if (_projectionFlag) { _chart.svg().selectAll('g.' + geoJson(layerIndex).name + ' path').attr('d', _geoPath); } } _projectionFlag = false; }; /** * **mandatory** * * Use this function to insert a new GeoJson map layer. This function can be invoked multiple times * if you have multiple GeoJson data layers to render on top of each other. If you overlay multiple * layers with the same name the new overlay will override the existing one. * @method overlayGeoJson * @memberof dc.geoChoroplethChart * @instance * @see {@link http://geojson.org/ GeoJSON} * @see {@link https://github.com/topojson/topojson/wiki TopoJSON} * @see {@link https://github.com/topojson/topojson-1.x-api-reference/blob/master/API-Reference.md#wiki-feature topojson.feature} * @example * // insert a layer for rendering US states * chart.overlayGeoJson(statesJson.features, 'state', function(d) { * return d.properties.name; * }); * @param {geoJson} json - a geojson feed * @param {String} name - name of the layer * @param {Function} keyAccessor - accessor function used to extract 'key' from the GeoJson data. The key extracted by * this function should match the keys returned by the crossfilter groups. * @returns {dc.geoChoroplethChart} */ _chart.overlayGeoJson = function (json, name, keyAccessor) { for (var i = 0; i < _geoJsons.length; ++i) { if (_geoJsons[i].name === name) { _geoJsons[i].data = json; _geoJsons[i].keyAccessor = keyAccessor; return _chart; } } _geoJsons.push({name: name, data: json, keyAccessor: keyAccessor}); return _chart; }; /** * Set custom geo projection function. See the available * {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Geo-Projections.md d3 geo projection functions}. * @method projection * @memberof dc.geoChoroplethChart * @instance * @see {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Geo-Projections.md d3.geo.projection} * @see {@link https://github.com/d3/d3-geo-projection Extended d3.geo.projection} * @param {d3.projection} [projection=d3.geo.albersUsa()] * @returns {dc.geoChoroplethChart} */ _chart.projection = function (projection) { _geoPath.projection(projection); _projectionFlag = true; return _chart; }; /** * Returns all GeoJson layers currently registered with this chart. The returned array is a * reference to this chart's internal data structure, so any modification to this array will also * modify this chart's internal registration. * @method geoJsons * @memberof dc.geoChoroplethChart * @instance * @returns {Array<{name:String, data: Object, accessor: Function}>} */ _chart.geoJsons = function () { return _geoJsons; }; /** * Returns the {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Geo-Paths.md#path d3.geo.path} object used to * render the projection and features. Can be useful for figuring out the bounding box of the * feature set and thus a way to calculate scale and translation for the projection. * @method geoPath * @memberof dc.geoChoroplethChart * @instance * @see {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Geo-Paths.md#path d3.geo.path} * @returns {d3.geo.path} */ _chart.geoPath = function () { return _geoPath; }; /** * Remove a GeoJson layer from this chart by name * @method removeGeoJson * @memberof dc.geoChoroplethChart * @instance * @param {String} name * @returns {dc.geoChoroplethChart} */ _chart.removeGeoJson = function (name) { var geoJsons = []; for (var i = 0; i < _geoJsons.length; ++i) { var layer = _geoJsons[i]; if (layer.name !== name) { geoJsons.push(layer); } } _geoJsons = geoJsons; return _chart; }; return _chart.anchor(parent, chartGroup); }; /** * The bubble overlay chart is quite different from the typical bubble chart. With the bubble overlay * chart you can arbitrarily place bubbles on an existing svg or bitmap image, thus changing the * typical x and y positioning while retaining the capability to visualize data using bubble radius * and coloring. * * Examples: * - {@link http://dc-js.github.com/dc.js/crime/index.html Canadian City Crime Stats} * @class bubbleOverlay * @memberof dc * @mixes dc.bubbleMixin * @mixes dc.baseMixin * @example * // create a bubble overlay chart on top of the '#chart-container1 svg' element using the default global chart group * var bubbleChart1 = dc.bubbleOverlayChart('#chart-container1').svg(d3.select('#chart-container1 svg')); * // create a bubble overlay chart on top of the '#chart-container2 svg' element using chart group A * var bubbleChart2 = dc.compositeChart('#chart-container2', 'chartGroupA').svg(d3.select('#chart-container2 svg')); * @param {String|node|d3.selection} parent - Any valid * {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Selections.md#selecting-elements d3 single selector} specifying * a dom block element such as a div; or a dom element or d3 selection. * @param {String} [chartGroup] - The name of the chart group this chart instance should be placed in. * Interaction with a chart will only trigger events and redraws within the chart's group. * @returns {dc.bubbleOverlay} */ dc.bubbleOverlay = function (parent, chartGroup) { var BUBBLE_OVERLAY_CLASS = 'bubble-overlay'; var BUBBLE_NODE_CLASS = 'node'; var BUBBLE_CLASS = 'bubble'; /** * **mandatory** * * Set the underlying svg image element. Unlike other dc charts this chart will not generate a svg * element; therefore the bubble overlay chart will not work if this function is not invoked. If the * underlying image is a bitmap, then an empty svg will need to be created on top of the image. * @method svg * @memberof dc.bubbleOverlay * @instance * @example * // set up underlying svg element * chart.svg(d3.select('#chart svg')); * @param {SVGElement|d3.selection} [imageElement] * @returns {dc.bubbleOverlay} */ var _chart = dc.bubbleMixin(dc.baseMixin({})); var _g; var _points = []; _chart.transitionDuration(750); _chart.transitionDelay(0); _chart.radiusValueAccessor(function (d) { return d.value; }); /** * **mandatory** * * Set up a data point on the overlay. The name of a data point should match a specific 'key' among * data groups generated using keyAccessor. If a match is found (point name <-> data group key) * then a bubble will be generated at the position specified by the function. x and y * value specified here are relative to the underlying svg. * @method point * @memberof dc.bubbleOverlay * @instance * @param {String} name * @param {Number} x * @param {Number} y * @returns {dc.bubbleOverlay} */ _chart.point = function (name, x, y) { _points.push({name: name, x: x, y: y}); return _chart; }; _chart._doRender = function () { _g = initOverlayG(); _chart.r().range([_chart.MIN_RADIUS, _chart.width() * _chart.maxBubbleRelativeSize()]); initializeBubbles(); _chart.fadeDeselectedArea(); return _chart; }; function initOverlayG () { _g = _chart.select('g.' + BUBBLE_OVERLAY_CLASS); if (_g.empty()) { _g = _chart.svg().append('g').attr('class', BUBBLE_OVERLAY_CLASS); } return _g; } function initializeBubbles () { var data = mapData(); _chart.calculateRadiusDomain(); _points.forEach(function (point) { var nodeG = getNodeG(point, data); var circle = nodeG.select('circle.' + BUBBLE_CLASS); if (circle.empty()) { circle = nodeG.append('circle') .attr('class', BUBBLE_CLASS) .attr('r', 0) .attr('fill', _chart.getColor) .on('click', _chart.onClick); } dc.transition(circle, _chart.transitionDuration(), _chart.transitionDelay()) .attr('r', function (d) { return _chart.bubbleR(d); }); _chart._doRenderLabel(nodeG); _chart._doRenderTitles(nodeG); }); } function mapData () { var data = {}; _chart.data().forEach(function (datum) { data[_chart.keyAccessor()(datum)] = datum; }); return data; } function getNodeG (point, data) { var bubbleNodeClass = BUBBLE_NODE_CLASS + ' ' + dc.utils.nameToId(point.name); var nodeG = _g.select('g.' + dc.utils.nameToId(point.name)); if (nodeG.empty()) { nodeG = _g.append('g') .attr('class', bubbleNodeClass) .attr('transform', 'translate(' + point.x + ',' + point.y + ')'); } nodeG.datum(data[point.name]); return nodeG; } _chart._doRedraw = function () { updateBubbles(); _chart.fadeDeselectedArea(); return _chart; }; function updateBubbles () { var data = mapData(); _chart.calculateRadiusDomain(); _points.forEach(function (point) { var nodeG = getNodeG(point, data); var circle = nodeG.select('circle.' + BUBBLE_CLASS); dc.transition(circle, _chart.transitionDuration(), _chart.transitionDelay()) .attr('r', function (d) { return _chart.bubbleR(d); }) .attr('fill', _chart.getColor); _chart.doUpdateLabels(nodeG); _chart.doUpdateTitles(nodeG); }); } _chart.debug = function (flag) { if (flag) { var debugG = _chart.select('g.' + dc.constants.DEBUG_GROUP_CLASS); if (debugG.empty()) { debugG = _chart.svg() .append('g') .attr('class', dc.constants.DEBUG_GROUP_CLASS); } var debugText = debugG.append('text') .attr('x', 10) .attr('y', 20); debugG .append('rect') .attr('width', _chart.width()) .attr('height', _chart.height()) .on('mousemove', function () { var position = d3.mouse(debugG.node()); var msg = position[0] + ', ' + position[1]; debugText.text(msg); }); } else { _chart.selectAll('.debug').remove(); } return _chart; }; _chart.anchor(parent, chartGroup); return _chart; }; /** * Concrete row chart implementation. * * Examples: * - {@link http://dc-js.github.com/dc.js/ Nasdaq 100 Index} * @class rowChart * @memberof dc * @mixes dc.capMixin * @mixes dc.marginMixin * @mixes dc.colorMixin * @mixes dc.baseMixin * @example * // create a row chart under #chart-container1 element using the default global chart group * var chart1 = dc.rowChart('#chart-container1'); * // create a row chart under #chart-container2 element using chart group A * var chart2 = dc.rowChart('#chart-container2', 'chartGroupA'); * @param {String|node|d3.selection} parent - Any valid * {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Selections.md#selecting-elements d3 single selector} specifying * a dom block element such as a div; or a dom element or d3 selection. * @param {String} [chartGroup] - The name of the chart group this chart instance should be placed in. * Interaction with a chart will only trigger events and redraws within the chart's group. * @returns {dc.rowChart} */ dc.rowChart = function (parent, chartGroup) { var _g; var _labelOffsetX = 10; var _labelOffsetY = 15; var _hasLabelOffsetY = false; var _dyOffset = '0.35em'; // this helps center labels https://github.com/d3/d3-3.x-api-reference/blob/master/SVG-Shapes.md#svg_text var _titleLabelOffsetX = 2; var _gap = 5; var _fixedBarHeight = false; var _rowCssClass = 'row'; var _titleRowCssClass = 'titlerow'; var _renderTitleLabel = false; var _chart = dc.capMixin(dc.marginMixin(dc.colorMixin(dc.baseMixin({})))); var _x; var _elasticX; var _xAxis = d3.svg.axis().orient('bottom'); var _rowData; _chart.rowsCap = _chart.cap; function calculateAxisScale () { if (!_x || _elasticX) { var extent = d3.extent(_rowData, _chart.cappedValueAccessor); if (extent[0] > 0) { extent[0] = 0; } if (extent[1] < 0) { extent[1] = 0; } _x = d3.scale.linear().domain(extent) .range([0, _chart.effectiveWidth()]); } _xAxis.scale(_x); } function drawAxis () { var axisG = _g.select('g.axis'); calculateAxisScale(); if (axisG.empty()) { axisG = _g.append('g').attr('class', 'axis'); } axisG.attr('transform', 'translate(0, ' + _chart.effectiveHeight() + ')'); dc.transition(axisG, _chart.transitionDuration(), _chart.transitionDelay()) .call(_xAxis); } _chart._doRender = function () { _chart.resetSvg(); _g = _chart.svg() .append('g') .attr('transform', 'translate(' + _chart.margins().left + ',' + _chart.margins().top + ')'); drawChart(); return _chart; }; _chart.title(function (d) { return _chart.cappedKeyAccessor(d) + ': ' + _chart.cappedValueAccessor(d); }); _chart.label(_chart.cappedKeyAccessor); /** * Gets or sets the x scale. The x scale can be any d3 * {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Quantitative-Scales.md quantitive scale}. * @method x * @memberof dc.rowChart * @instance * @see {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Quantitative-Scales.md quantitive scale} * @param {d3.scale} [scale] * @returns {d3.scale|dc.rowChart} */ _chart.x = function (scale) { if (!arguments.length) { return _x; } _x = scale; return _chart; }; function drawGridLines () { _g.selectAll('g.tick') .select('line.grid-line') .remove(); _g.selectAll('g.tick') .append('line') .attr('class', 'grid-line') .attr('x1', 0) .attr('y1', 0) .attr('x2', 0) .attr('y2', function () { return -_chart.effectiveHeight(); }); } function drawChart () { _rowData = _chart.data(); drawAxis(); drawGridLines(); var rows = _g.selectAll('g.' + _rowCssClass) .data(_rowData); createElements(rows); removeElements(rows); updateElements(rows); } function createElements (rows) { var rowEnter = rows.enter() .append('g') .attr('class', function (d, i) { return _rowCssClass + ' _' + i; }); rowEnter.append('rect').attr('width', 0); createLabels(rowEnter); } function removeElements (rows) { rows.exit().remove(); } function rootValue () { var root = _x(0); return (root === -Infinity || root !== root) ? _x(1) : root; } function updateElements (rows) { var n = _rowData.length; var height; if (!_fixedBarHeight) { height = (_chart.effectiveHeight() - (n + 1) * _gap) / n; } else { height = _fixedBarHeight; } // vertically align label in center unless they override the value via property setter if (!_hasLabelOffsetY) { _labelOffsetY = height / 2; } var rect = rows.attr('transform', function (d, i) { return 'translate(0,' + ((i + 1) * _gap + i * height) + ')'; }).select('rect') .attr('height', height) .attr('fill', _chart.getColor) .on('click', onClick) .classed('deselected', function (d) { return (_chart.hasFilter()) ? !isSelectedRow(d) : false; }) .classed('selected', function (d) { return (_chart.hasFilter()) ? isSelectedRow(d) : false; }); dc.transition(rect, _chart.transitionDuration(), _chart.transitionDelay()) .attr('width', function (d) { return Math.abs(rootValue() - _x(_chart.valueAccessor()(d))); }) .attr('transform', translateX); createTitles(rows); updateLabels(rows); } function createTitles (rows) { if (_chart.renderTitle()) { rows.select('title').remove(); rows.append('title').text(_chart.title()); } } function createLabels (rowEnter) { if (_chart.renderLabel()) { rowEnter.append('text') .on('click', onClick); } if (_chart.renderTitleLabel()) { rowEnter.append('text') .attr('class', _titleRowCssClass) .on('click', onClick); } } function updateLabels (rows) { if (_chart.renderLabel()) { var lab = rows.select('text') .attr('x', _labelOffsetX) .attr('y', _labelOffsetY) .attr('dy', _dyOffset) .on('click', onClick) .attr('class', function (d, i) { return _rowCssClass + ' _' + i; }) .text(function (d) { return _chart.label()(d); }); dc.transition(lab, _chart.transitionDuration(), _chart.transitionDelay()) .attr('transform', translateX); } if (_chart.renderTitleLabel()) { var titlelab = rows.select('.' + _titleRowCssClass) .attr('x', _chart.effectiveWidth() - _titleLabelOffsetX) .attr('y', _labelOffsetY) .attr('dy', _dyOffset) .attr('text-anchor', 'end') .on('click', onClick) .attr('class', function (d, i) { return _titleRowCssClass + ' _' + i ; }) .text(function (d) { return _chart.title()(d); }); dc.transition(titlelab, _chart.transitionDuration(), _chart.transitionDelay()) .attr('transform', translateX); } } /** * Turn on/off Title label rendering (values) using SVG style of text-anchor 'end'. * @method renderTitleLabel * @memberof dc.rowChart * @instance * @param {Boolean} [renderTitleLabel=false] * @returns {Boolean|dc.rowChart} */ _chart.renderTitleLabel = function (renderTitleLabel) { if (!arguments.length) { return _renderTitleLabel; } _renderTitleLabel = renderTitleLabel; return _chart; }; function onClick (d) { _chart.onClick(d); } function translateX (d) { var x = _x(_chart.cappedValueAccessor(d)), x0 = rootValue(), s = x > x0 ? x0 : x; return 'translate(' + s + ',0)'; } _chart._doRedraw = function () { drawChart(); return _chart; }; /** * Get the x axis for the row chart instance. Note: not settable for row charts. * See the {@link https://github.com/d3/d3-3.x-api-reference/blob/master/SVG-Axes.md#axis d3 axis object} * documention for more information. * @method xAxis * @memberof dc.rowChart * @instance * @see {@link https://github.com/d3/d3-3.x-api-reference/blob/master/SVG-Axes.md#axis d3.svg.axis} * @example * // customize x axis tick format * chart.xAxis().tickFormat(function (v) {return v + '%';}); * // customize x axis tick values * chart.xAxis().tickValues([0, 100, 200, 300]); * @returns {d3.svg.axis} */ _chart.xAxis = function () { return _xAxis; }; /** * Get or set the fixed bar height. Default is [false] which will auto-scale bars. * For example, if you want to fix the height for a specific number of bars (useful in TopN charts) * you could fix height as follows (where count = total number of bars in your TopN and gap is * your vertical gap space). * @method fixedBarHeight * @memberof dc.rowChart * @instance * @example * chart.fixedBarHeight( chartheight - (count + 1) * gap / count); * @param {Boolean|Number} [fixedBarHeight=false] * @returns {Boolean|Number|dc.rowChart} */ _chart.fixedBarHeight = function (fixedBarHeight) { if (!arguments.length) { return _fixedBarHeight; } _fixedBarHeight = fixedBarHeight; return _chart; }; /** * Get or set the vertical gap space between rows on a particular row chart instance. * @method gap * @memberof dc.rowChart * @instance * @param {Number} [gap=5] * @returns {Number|dc.rowChart} */ _chart.gap = function (gap) { if (!arguments.length) { return _gap; } _gap = gap; return _chart; }; /** * Get or set the elasticity on x axis. If this attribute is set to true, then the x axis will rescle to auto-fit the * data range when filtered. * @method elasticX * @memberof dc.rowChart * @instance * @param {Boolean} [elasticX] * @returns {Boolean|dc.rowChart} */ _chart.elasticX = function (elasticX) { if (!arguments.length) { return _elasticX; } _elasticX = elasticX; return _chart; }; /** * Get or set the x offset (horizontal space to the top left corner of a row) for labels on a particular row chart. * @method labelOffsetX * @memberof dc.rowChart * @instance * @param {Number} [labelOffsetX=10] * @returns {Number|dc.rowChart} */ _chart.labelOffsetX = function (labelOffsetX) { if (!arguments.length) { return _labelOffsetX; } _labelOffsetX = labelOffsetX; return _chart; }; /** * Get or set the y offset (vertical space to the top left corner of a row) for labels on a particular row chart. * @method labelOffsetY * @memberof dc.rowChart * @instance * @param {Number} [labelOffsety=15] * @returns {Number|dc.rowChart} */ _chart.labelOffsetY = function (labelOffsety) { if (!arguments.length) { return _labelOffsetY; } _labelOffsetY = labelOffsety; _hasLabelOffsetY = true; return _chart; }; /** * Get of set the x offset (horizontal space between right edge of row and right edge or text. * @method titleLabelOffsetX * @memberof dc.rowChart * @instance * @param {Number} [titleLabelOffsetX=2] * @returns {Number|dc.rowChart} */ _chart.titleLabelOffsetX = function (titleLabelOffsetX) { if (!arguments.length) { return _titleLabelOffsetX; } _titleLabelOffsetX = titleLabelOffsetX; return _chart; }; function isSelectedRow (d) { return _chart.hasFilter(_chart.cappedKeyAccessor(d)); } return _chart.anchor(parent, chartGroup); }; /** * Legend is a attachable widget that can be added to other dc charts to render horizontal legend * labels. * * Examples: * - {@link http://dc-js.github.com/dc.js/ Nasdaq 100 Index} * - {@link http://dc-js.github.com/dc.js/crime/index.html Canadian City Crime Stats} * @class legend * @memberof dc * @example * chart.legend(dc.legend().x(400).y(10).itemHeight(13).gap(5)) * @returns {dc.legend} */ dc.legend = function () { var LABEL_GAP = 2; var _legend = {}, _parent, _x = 0, _y = 0, _itemHeight = 12, _gap = 5, _horizontal = false, _legendWidth = 560, _itemWidth = 70, _autoItemWidth = false, _legendText = dc.pluck('name'), _maxItems; var _g; _legend.parent = function (p) { if (!arguments.length) { return _parent; } _parent = p; return _legend; }; _legend.render = function () { _parent.svg().select('g.dc-legend').remove(); _g = _parent.svg().append('g') .attr('class', 'dc-legend') .attr('transform', 'translate(' + _x + ',' + _y + ')'); var legendables = _parent.legendables(); if (_maxItems !== undefined) { legendables = legendables.slice(0, _maxItems); } var itemEnter = _g.selectAll('g.dc-legend-item') .data(legendables) .enter() .append('g') .attr('class', 'dc-legend-item') .on('mouseover', function (d) { _parent.legendHighlight(d); }) .on('mouseout', function (d) { _parent.legendReset(d); }) .on('click', function (d) { d.chart.legendToggle(d); }); _g.selectAll('g.dc-legend-item') .classed('fadeout', function (d) { return d.chart.isLegendableHidden(d); }); if (legendables.some(dc.pluck('dashstyle'))) { itemEnter .append('line') .attr('x1', 0) .attr('y1', _itemHeight / 2) .attr('x2', _itemHeight) .attr('y2', _itemHeight / 2) .attr('stroke-width', 2) .attr('stroke-dasharray', dc.pluck('dashstyle')) .attr('stroke', dc.pluck('color')); } else { itemEnter .append('rect') .attr('width', _itemHeight) .attr('height', _itemHeight) .attr('fill', function (d) {return d ? d.color : 'blue';}); } itemEnter.append('text') .text(_legendText) .attr('x', _itemHeight + LABEL_GAP) .attr('y', function () { return _itemHeight / 2 + (this.clientHeight ? this.clientHeight : 13) / 2 - 2; }); var _cumulativeLegendTextWidth = 0; var row = 0; itemEnter.attr('transform', function (d, i) { if (_horizontal) { var itemWidth = _autoItemWidth === true ? this.getBBox().width + _gap : _itemWidth; if ((_cumulativeLegendTextWidth + itemWidth) > _legendWidth && _cumulativeLegendTextWidth > 0) { ++row; _cumulativeLegendTextWidth = 0; } var translateBy = 'translate(' + _cumulativeLegendTextWidth + ',' + row * legendItemHeight() + ')'; _cumulativeLegendTextWidth += itemWidth; return translateBy; } else { return 'translate(0,' + i * legendItemHeight() + ')'; } }); }; function legendItemHeight () { return _gap + _itemHeight; } /** * Set or get x coordinate for legend widget. * @method x * @memberof dc.legend * @instance * @param {Number} [x=0] * @returns {Number|dc.legend} */ _legend.x = function (x) { if (!arguments.length) { return _x; } _x = x; return _legend; }; /** * Set or get y coordinate for legend widget. * @method y * @memberof dc.legend * @instance * @param {Number} [y=0] * @returns {Number|dc.legend} */ _legend.y = function (y) { if (!arguments.length) { return _y; } _y = y; return _legend; }; /** * Set or get gap between legend items. * @method gap * @memberof dc.legend * @instance * @param {Number} [gap=5] * @returns {Number|dc.legend} */ _legend.gap = function (gap) { if (!arguments.length) { return _gap; } _gap = gap; return _legend; }; /** * Set or get legend item height. * @method itemHeight * @memberof dc.legend * @instance * @param {Number} [itemHeight=12] * @returns {Number|dc.legend} */ _legend.itemHeight = function (itemHeight) { if (!arguments.length) { return _itemHeight; } _itemHeight = itemHeight; return _legend; }; /** * Position legend horizontally instead of vertically. * @method horizontal * @memberof dc.legend * @instance * @param {Boolean} [horizontal=false] * @returns {Boolean|dc.legend} */ _legend.horizontal = function (horizontal) { if (!arguments.length) { return _horizontal; } _horizontal = horizontal; return _legend; }; /** * Maximum width for horizontal legend. * @method legendWidth * @memberof dc.legend * @instance * @param {Number} [legendWidth=500] * @returns {Number|dc.legend} */ _legend.legendWidth = function (legendWidth) { if (!arguments.length) { return _legendWidth; } _legendWidth = legendWidth; return _legend; }; /** * Legend item width for horizontal legend. * @method itemWidth * @memberof dc.legend * @instance * @param {Number} [itemWidth=70] * @returns {Number|dc.legend} */ _legend.itemWidth = function (itemWidth) { if (!arguments.length) { return _itemWidth; } _itemWidth = itemWidth; return _legend; }; /** * Turn automatic width for legend items on or off. If true, {@link dc.legend#itemWidth itemWidth} is ignored. * This setting takes into account the {@link dc.legend#gap gap}. * @method autoItemWidth * @memberof dc.legend * @instance * @param {Boolean} [autoItemWidth=false] * @returns {Boolean|dc.legend} */ _legend.autoItemWidth = function (autoItemWidth) { if (!arguments.length) { return _autoItemWidth; } _autoItemWidth = autoItemWidth; return _legend; }; /** * Set or get the legend text function. The legend widget uses this function to render the legend * text for each item. If no function is specified the legend widget will display the names * associated with each group. * @method legendText * @memberof dc.legend * @instance * @param {Function} [legendText] * @returns {Function|dc.legend} * @example * // default legendText * legend.legendText(dc.pluck('name')) * * // create numbered legend items * chart.legend(dc.legend().legendText(function(d, i) { return i + '. ' + d.name; })) * * // create legend displaying group counts * chart.legend(dc.legend().legendText(function(d) { return d.name + ': ' d.data; })) **/ _legend.legendText = function (legendText) { if (!arguments.length) { return _legendText; } _legendText = legendText; return _legend; }; /** * Maximum number of legend items to display * @method maxItems * @memberof dc.legend * @instance * @param {Number} [maxItems] * @return {dc.legend} */ _legend.maxItems = function (maxItems) { if (!arguments.length) { return _maxItems; } _maxItems = dc.utils.isNumber(maxItems) ? maxItems : undefined; return _legend; }; return _legend; }; /** * A scatter plot chart * * Examples: * - {@link http://dc-js.github.io/dc.js/examples/scatter.html Scatter Chart} * - {@link http://dc-js.github.io/dc.js/examples/multi-scatter.html Multi-Scatter Chart} * @class scatterPlot * @memberof dc * @mixes dc.coordinateGridMixin * @example * // create a scatter plot under #chart-container1 element using the default global chart group * var chart1 = dc.scatterPlot('#chart-container1'); * // create a scatter plot under #chart-container2 element using chart group A * var chart2 = dc.scatterPlot('#chart-container2', 'chartGroupA'); * // create a sub-chart under a composite parent chart * var chart3 = dc.scatterPlot(compositeChart); * @param {String|node|d3.selection} parent - Any valid * {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Selections.md#selecting-elements d3 single selector} specifying * a dom block element such as a div; or a dom element or d3 selection. * @param {String} [chartGroup] - The name of the chart group this chart instance should be placed in. * Interaction with a chart will only trigger events and redraws within the chart's group. * @returns {dc.scatterPlot} */ dc.scatterPlot = function (parent, chartGroup) { var _chart = dc.coordinateGridMixin({}); var _symbol = d3.svg.symbol(); var _existenceAccessor = function (d) { return d.value; }; var originalKeyAccessor = _chart.keyAccessor(); _chart.keyAccessor(function (d) { return originalKeyAccessor(d)[0]; }); _chart.valueAccessor(function (d) { return originalKeyAccessor(d)[1]; }); _chart.colorAccessor(function () { return _chart._groupName; }); _chart.title(function (d) { // this basically just counteracts the setting of its own key/value accessors // see https://github.com/dc-js/dc.js/issues/702 return _chart.keyAccessor()(d) + ',' + _chart.valueAccessor()(d) + ': ' + _chart.existenceAccessor()(d); }); var _locator = function (d) { return 'translate(' + _chart.x()(_chart.keyAccessor()(d)) + ',' + _chart.y()(_chart.valueAccessor()(d)) + ')'; }; var _highlightedSize = 7; var _symbolSize = 5; var _excludedSize = 3; var _excludedColor = null; var _excludedOpacity = 1.0; var _emptySize = 0; var _emptyOpacity = 0; var _nonemptyOpacity = 1; var _emptyColor = null; var _filtered = []; function elementSize (d, i) { if (!_existenceAccessor(d)) { return Math.pow(_emptySize, 2); } else if (_filtered[i]) { return Math.pow(_symbolSize, 2); } else { return Math.pow(_excludedSize, 2); } } _symbol.size(elementSize); dc.override(_chart, '_filter', function (filter) { if (!arguments.length) { return _chart.__filter(); } return _chart.__filter(dc.filters.RangedTwoDimensionalFilter(filter)); }); _chart.plotData = function () { var symbols = _chart.chartBodyG().selectAll('path.symbol') .data(_chart.data()); symbols .enter() .append('path') .attr('class', 'symbol') .attr('opacity', 0) .attr('fill', _chart.getColor) .attr('transform', _locator); symbols.call(renderTitles, _chart.data()); symbols.each(function (d, i) { _filtered[i] = !_chart.filter() || _chart.filter().isFiltered([d.key[0], d.key[1]]); }); dc.transition(symbols, _chart.transitionDuration(), _chart.transitionDelay()) .attr('opacity', function (d, i) { if (!_existenceAccessor(d)) { return _emptyOpacity; } else if (_filtered[i]) { return _nonemptyOpacity; } else { return _chart.excludedOpacity(); } }) .attr('fill', function (d, i) { if (_emptyColor && !_existenceAccessor(d)) { return _emptyColor; } else if (_chart.excludedColor() && !_filtered[i]) { return _chart.excludedColor(); } else { return _chart.getColor(d); } }) .attr('transform', _locator) .attr('d', _symbol); dc.transition(symbols.exit(), _chart.transitionDuration(), _chart.transitionDelay()) .attr('opacity', 0).remove(); }; function renderTitles (symbol, d) { if (_chart.renderTitle()) { symbol.selectAll('title').remove(); symbol.append('title').text(function (d) { return _chart.title()(d); }); } } /** * Get or set the existence accessor. If a point exists, it is drawn with * {@link dc.scatterPlot#symbolSize symbolSize} radius and * opacity 1; if it does not exist, it is drawn with * {@link dc.scatterPlot#emptySize emptySize} radius and opacity 0. By default, * the existence accessor checks if the reduced value is truthy. * @method existenceAccessor * @memberof dc.scatterPlot * @instance * @see {@link dc.scatterPlot#symbolSize symbolSize} * @see {@link dc.scatterPlot#emptySize emptySize} * @example * // default accessor * chart.existenceAccessor(function (d) { return d.value; }); * @param {Function} [accessor] * @returns {Function|dc.scatterPlot} */ _chart.existenceAccessor = function (accessor) { if (!arguments.length) { return _existenceAccessor; } _existenceAccessor = accessor; return this; }; /** * Get or set the symbol type used for each point. By default the symbol is a circle. * Type can be a constant or an accessor. * @method symbol * @memberof dc.scatterPlot * @instance * @see {@link https://github.com/d3/d3-3.x-api-reference/blob/master/SVG-Shapes.md#symbol_type d3.svg.symbol.type} * @example * // Circle type * chart.symbol('circle'); * // Square type * chart.symbol('square'); * @param {String|Function} [type='circle'] * @returns {String|Function|dc.scatterPlot} */ _chart.symbol = function (type) { if (!arguments.length) { return _symbol.type(); } _symbol.type(type); return _chart; }; /** * Get or set the symbol generator. By default `dc.scatterPlot` will use * {@link https://github.com/d3/d3-3.x-api-reference/blob/master/SVG-Shapes.md#symbol d3.svg.symbol()} * to generate symbols. `dc.scatterPlot` will set the * {@link https://github.com/d3/d3-3.x-api-reference/blob/master/SVG-Shapes.md#symbol_size size accessor} * on the symbol generator. * @method customSymbol * @memberof dc.scatterPlot * @instance * @see {@link https://github.com/d3/d3-3.x-api-reference/blob/master/SVG-Shapes.md#symbol d3.svg.symbol} * @see {@link https://stackoverflow.com/questions/25332120/create-additional-d3-js-symbols Create additional D3.js symbols} * @param {String|Function} [customSymbol=d3.svg.symbol()] * @returns {String|Function|dc.scatterPlot} */ _chart.customSymbol = function (customSymbol) { if (!arguments.length) { return _symbol; } _symbol = customSymbol; _symbol.size(elementSize); return _chart; }; /** * Set or get radius for symbols. * @method symbolSize * @memberof dc.scatterPlot * @instance * @see {@link https://github.com/d3/d3-3.x-api-reference/blob/master/SVG-Shapes.md#symbol_size d3.svg.symbol.size} * @param {Number} [symbolSize=3] * @returns {Number|dc.scatterPlot} */ _chart.symbolSize = function (symbolSize) { if (!arguments.length) { return _symbolSize; } _symbolSize = symbolSize; return _chart; }; /** * Set or get radius for highlighted symbols. * @method highlightedSize * @memberof dc.scatterPlot * @instance * @see {@link https://github.com/d3/d3-3.x-api-reference/blob/master/SVG-Shapes.md#symbol_size d3.svg.symbol.size} * @param {Number} [highlightedSize=5] * @returns {Number|dc.scatterPlot} */ _chart.highlightedSize = function (highlightedSize) { if (!arguments.length) { return _highlightedSize; } _highlightedSize = highlightedSize; return _chart; }; /** * Set or get size for symbols excluded from this chart's filter. If null, no * special size is applied for symbols based on their filter status. * @method excludedSize * @memberof dc.scatterPlot * @instance * @see {@link https://github.com/d3/d3-3.x-api-reference/blob/master/SVG-Shapes.md#symbol_size d3.svg.symbol.size} * @param {Number} [excludedSize=null] * @returns {Number|dc.scatterPlot} */ _chart.excludedSize = function (excludedSize) { if (!arguments.length) { return _excludedSize; } _excludedSize = excludedSize; return _chart; }; /** * Set or get color for symbols excluded from this chart's filter. If null, no * special color is applied for symbols based on their filter status. * @method excludedColor * @memberof dc.scatterPlot * @instance * @param {Number} [excludedColor=null] * @returns {Number|dc.scatterPlot} */ _chart.excludedColor = function (excludedColor) { if (!arguments.length) { return _excludedColor; } _excludedColor = excludedColor; return _chart; }; /** * Set or get opacity for symbols excluded from this chart's filter. * @method excludedOpacity * @memberof dc.scatterPlot * @instance * @param {Number} [excludedOpacity=1.0] * @returns {Number|dc.scatterPlot} */ _chart.excludedOpacity = function (excludedOpacity) { if (!arguments.length) { return _excludedOpacity; } _excludedOpacity = excludedOpacity; return _chart; }; /** * Set or get radius for symbols when the group is empty. * @method emptySize * @memberof dc.scatterPlot * @instance * @see {@link https://github.com/d3/d3-3.x-api-reference/blob/master/SVG-Shapes.md#symbol_size d3.svg.symbol.size} * @param {Number} [emptySize=0] * @returns {Number|dc.scatterPlot} */ _chart.hiddenSize = _chart.emptySize = function (emptySize) { if (!arguments.length) { return _emptySize; } _emptySize = emptySize; return _chart; }; /** * Set or get color for symbols when the group is empty. If null, just use the * {@link dc.colorMixin#colors colorMixin.colors} color scale zero value. * @name emptyColor * @memberof dc.scatterPlot * @instance * @param {String} [emptyColor=null] * @return {String} * @return {dc.scatterPlot}/ */ _chart.emptyColor = function (emptyColor) { if (!arguments.length) { return _emptyColor; } _emptyColor = emptyColor; return _chart; }; /** * Set or get opacity for symbols when the group is empty. * @name emptyOpacity * @memberof dc.scatterPlot * @instance * @param {Number} [emptyOpacity=0] * @return {Number} * @return {dc.scatterPlot} */ _chart.emptyOpacity = function (emptyOpacity) { if (!arguments.length) { return _emptyOpacity; } _emptyOpacity = emptyOpacity; return _chart; }; /** * Set or get opacity for symbols when the group is not empty. * @name nonemptyOpacity * @memberof dc.scatterPlot * @instance * @param {Number} [nonemptyOpacity=1] * @return {Number} * @return {dc.scatterPlot} */ _chart.nonemptyOpacity = function (nonemptyOpacity) { if (!arguments.length) { return _emptyOpacity; } _nonemptyOpacity = nonemptyOpacity; return _chart; }; _chart.legendables = function () { return [{chart: _chart, name: _chart._groupName, color: _chart.getColor()}]; }; _chart.legendHighlight = function (d) { resizeSymbolsWhere(function (symbol) { return symbol.attr('fill') === d.color; }, _highlightedSize); _chart.chartBodyG().selectAll('.chart-body path.symbol').filter(function () { return d3.select(this).attr('fill') !== d.color; }).classed('fadeout', true); }; _chart.legendReset = function (d) { resizeSymbolsWhere(function (symbol) { return symbol.attr('fill') === d.color; }, _symbolSize); _chart.chartBodyG().selectAll('.chart-body path.symbol').filter(function () { return d3.select(this).attr('fill') !== d.color; }).classed('fadeout', false); }; function resizeSymbolsWhere (condition, size) { var symbols = _chart.chartBodyG().selectAll('.chart-body path.symbol').filter(function () { return condition(d3.select(this)); }); var oldSize = _symbol.size(); _symbol.size(Math.pow(size, 2)); dc.transition(symbols, _chart.transitionDuration(), _chart.transitionDelay()).attr('d', _symbol); _symbol.size(oldSize); } _chart.setHandlePaths = function () { // no handle paths for poly-brushes }; _chart.extendBrush = function () { var extent = _chart.brush().extent(); if (_chart.round()) { extent[0] = extent[0].map(_chart.round()); extent[1] = extent[1].map(_chart.round()); _chart.g().select('.brush') .call(_chart.brush().extent(extent)); } return extent; }; _chart.brushIsEmpty = function (extent) { return _chart.brush().empty() || !extent || extent[0][0] >= extent[1][0] || extent[0][1] >= extent[1][1]; }; _chart._brushing = function () { var extent = _chart.extendBrush(); _chart.redrawBrush(_chart.g()); if (_chart.brushIsEmpty(extent)) { dc.events.trigger(function () { _chart.filter(null); _chart.redrawGroup(); }); } else { var ranged2DFilter = dc.filters.RangedTwoDimensionalFilter(extent); dc.events.trigger(function () { _chart.filter(null); _chart.filter(ranged2DFilter); _chart.redrawGroup(); }, dc.constants.EVENT_DELAY); } }; _chart.setBrushY = function (gBrush) { gBrush.call(_chart.brush().y(_chart.y())); }; return _chart.anchor(parent, chartGroup); }; /** * A display of a single numeric value. * Unlike other charts, you do not need to set a dimension. Instead a group object must be provided and * a valueAccessor that returns a single value. * @class numberDisplay * @memberof dc * @mixes dc.baseMixin * @example * // create a number display under #chart-container1 element using the default global chart group * var display1 = dc.numberDisplay('#chart-container1'); * @param {String|node|d3.selection} parent - Any valid * {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Selections.md#selecting-elements d3 single selector} specifying * a dom block element such as a div; or a dom element or d3 selection. * @param {String} [chartGroup] - The name of the chart group this chart instance should be placed in. * Interaction with a chart will only trigger events and redraws within the chart's group. * @returns {dc.numberDisplay} */ dc.numberDisplay = function (parent, chartGroup) { var SPAN_CLASS = 'number-display'; var _formatNumber = d3.format('.2s'); var _chart = dc.baseMixin({}); var _html = {one: '', some: '', none: ''}; var _lastValue; // dimension not required _chart._mandatoryAttributes(['group']); // default to ordering by value, to emulate old group.top(1) behavior when multiple groups _chart.ordering(function (kv) { return kv.value; }); /** * Gets or sets an optional object specifying HTML templates to use depending on the number * displayed. The text `%number` will be replaced with the current value. * - one: HTML template to use if the number is 1 * - zero: HTML template to use if the number is 0 * - some: HTML template to use otherwise * @method html * @memberof dc.numberDisplay * @instance * @example * numberWidget.html({ * one:'%number record', * some:'%number records', * none:'no records'}) * @param {{one:String, some:String, none:String}} [html={one: '', some: '', none: ''}] * @returns {{one:String, some:String, none:String}|dc.numberDisplay} */ _chart.html = function (html) { if (!arguments.length) { return _html; } if (html.none) { _html.none = html.none;//if none available } else if (html.one) { _html.none = html.one;//if none not available use one } else if (html.some) { _html.none = html.some;//if none and one not available use some } if (html.one) { _html.one = html.one;//if one available } else if (html.some) { _html.one = html.some;//if one not available use some } if (html.some) { _html.some = html.some;//if some available } else if (html.one) { _html.some = html.one;//if some not available use one } return _chart; }; /** * Calculate and return the underlying value of the display. * @method value * @memberof dc.numberDisplay * @instance * @returns {Number} */ _chart.value = function () { return _chart.data(); }; function maxBin (all) { if (!all.length) { return null; } var sorted = _chart._computeOrderedGroups(all); return sorted[sorted.length - 1]; } _chart.data(function (group) { var valObj = group.value ? group.value() : maxBin(group.all()); return _chart.valueAccessor()(valObj); }); _chart.transitionDuration(250); // good default _chart.transitionDelay(0); _chart._doRender = function () { var newValue = _chart.value(), span = _chart.selectAll('.' + SPAN_CLASS); if (span.empty()) { span = span.data([0]) .enter() .append('span') .attr('class', SPAN_CLASS); } span.transition() .duration(_chart.transitionDuration()) .delay(_chart.transitionDelay()) .ease('quad-out-in') .tween('text', function () { // [XA] don't try and interpolate from Infinity, else this breaks. var interpStart = isFinite(_lastValue) ? _lastValue : 0; var interp = d3.interpolateNumber(interpStart || 0, newValue); _lastValue = newValue; return function (t) { var html = null, num = _chart.formatNumber()(interp(t)); if (newValue === 0 && (_html.none !== '')) { html = _html.none; } else if (newValue === 1 && (_html.one !== '')) { html = _html.one; } else if (_html.some !== '') { html = _html.some; } this.innerHTML = html ? html.replace('%number', num) : num; }; }); }; _chart._doRedraw = function () { return _chart._doRender(); }; /** * Get or set a function to format the value for the display. * @method formatNumber * @memberof dc.numberDisplay * @instance * @see {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Formatting.md d3.format} * @param {Function} [formatter=d3.format('.2s')] * @returns {Function|dc.numberDisplay} */ _chart.formatNumber = function (formatter) { if (!arguments.length) { return _formatNumber; } _formatNumber = formatter; return _chart; }; return _chart.anchor(parent, chartGroup); }; /** * A heat map is matrix that represents the values of two dimensions of data using colors. * @class heatMap * @memberof dc * @mixes dc.colorMixin * @mixes dc.marginMixin * @mixes dc.baseMixin * @example * // create a heat map under #chart-container1 element using the default global chart group * var heatMap1 = dc.heatMap('#chart-container1'); * // create a heat map under #chart-container2 element using chart group A * var heatMap2 = dc.heatMap('#chart-container2', 'chartGroupA'); * @param {String|node|d3.selection} parent - Any valid * {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Selections.md#selecting-elements d3 single selector} specifying * a dom block element such as a div; or a dom element or d3 selection. * @param {String} [chartGroup] - The name of the chart group this chart instance should be placed in. * Interaction with a chart will only trigger events and redraws within the chart's group. * @returns {dc.heatMap} */ dc.heatMap = function (parent, chartGroup) { var DEFAULT_BORDER_RADIUS = 6.75; var _chartBody; var _cols; var _rows; var _colOrdering = d3.ascending; var _rowOrdering = d3.ascending; var _colScale = d3.scale.ordinal(); var _rowScale = d3.scale.ordinal(); var _xBorderRadius = DEFAULT_BORDER_RADIUS; var _yBorderRadius = DEFAULT_BORDER_RADIUS; var _chart = dc.colorMixin(dc.marginMixin(dc.baseMixin({}))); _chart._mandatoryAttributes(['group']); _chart.title(_chart.colorAccessor()); var _colsLabel = function (d) { return d; }; var _rowsLabel = function (d) { return d; }; /** * Set or get the column label function. The chart class uses this function to render * column labels on the X axis. It is passed the column name. * @method colsLabel * @memberof dc.heatMap * @instance * @example * // the default label function just returns the name * chart.colsLabel(function(d) { return d; }); * @param {Function} [labelFunction=function(d) { return d; }] * @returns {Function|dc.heatMap} */ _chart.colsLabel = function (labelFunction) { if (!arguments.length) { return _colsLabel; } _colsLabel = labelFunction; return _chart; }; /** * Set or get the row label function. The chart class uses this function to render * row labels on the Y axis. It is passed the row name. * @method rowsLabel * @memberof dc.heatMap * @instance * @example * // the default label function just returns the name * chart.rowsLabel(function(d) { return d; }); * @param {Function} [labelFunction=function(d) { return d; }] * @returns {Function|dc.heatMap} */ _chart.rowsLabel = function (labelFunction) { if (!arguments.length) { return _rowsLabel; } _rowsLabel = labelFunction; return _chart; }; var _xAxisOnClick = function (d) { filterAxis(0, d); }; var _yAxisOnClick = function (d) { filterAxis(1, d); }; var _boxOnClick = function (d) { var filter = d.key; dc.events.trigger(function () { _chart.filter(filter); _chart.redrawGroup(); }); }; function filterAxis (axis, value) { var cellsOnAxis = _chart.selectAll('.box-group').filter(function (d) { return d.key[axis] === value; }); var unfilteredCellsOnAxis = cellsOnAxis.filter(function (d) { return !_chart.hasFilter(d.key); }); dc.events.trigger(function () { var selection = unfilteredCellsOnAxis.empty() ? cellsOnAxis : unfilteredCellsOnAxis; var filters = selection.data().map(function (kv) { return dc.filters.TwoDimensionalFilter(kv.key); }); _chart._filter([filters]); _chart.redrawGroup(); }); } dc.override(_chart, 'filter', function (filter) { if (!arguments.length) { return _chart._filter(); } return _chart._filter(dc.filters.TwoDimensionalFilter(filter)); }); /** * Gets or sets the values used to create the rows of the heatmap, as an array. By default, all * the values will be fetched from the data using the value accessor. * @method rows * @memberof dc.heatMap * @instance * @param {Array} [rows] * @returns {Array|dc.heatMap} */ _chart.rows = function (rows) { if (!arguments.length) { return _rows; } _rows = rows; return _chart; }; /** #### .rowOrdering([orderFunction]) Get or set an accessor to order the rows. Default is d3.ascending. */ _chart.rowOrdering = function (_) { if (!arguments.length) { return _rowOrdering; } _rowOrdering = _; return _chart; }; /** * Gets or sets the keys used to create the columns of the heatmap, as an array. By default, all * the values will be fetched from the data using the key accessor. * @method cols * @memberof dc.heatMap * @instance * @param {Array} [cols] * @returns {Array|dc.heatMap} */ _chart.cols = function (cols) { if (!arguments.length) { return _cols; } _cols = cols; return _chart; }; /** #### .colOrdering([orderFunction]) Get or set an accessor to order the cols. Default is ascending. */ _chart.colOrdering = function (_) { if (!arguments.length) { return _colOrdering; } _colOrdering = _; return _chart; }; _chart._doRender = function () { _chart.resetSvg(); _chartBody = _chart.svg() .append('g') .attr('class', 'heatmap') .attr('transform', 'translate(' + _chart.margins().left + ',' + _chart.margins().top + ')'); return _chart._doRedraw(); }; _chart._doRedraw = function () { var data = _chart.data(), rows = _chart.rows() || data.map(_chart.valueAccessor()), cols = _chart.cols() || data.map(_chart.keyAccessor()); if (_rowOrdering) { rows = rows.sort(_rowOrdering); } if (_colOrdering) { cols = cols.sort(_colOrdering); } rows = _rowScale.domain(rows); cols = _colScale.domain(cols); var rowCount = rows.domain().length, colCount = cols.domain().length, boxWidth = Math.floor(_chart.effectiveWidth() / colCount), boxHeight = Math.floor(_chart.effectiveHeight() / rowCount); cols.rangeRoundBands([0, _chart.effectiveWidth()]); rows.rangeRoundBands([_chart.effectiveHeight(), 0]); var boxes = _chartBody.selectAll('g.box-group').data(_chart.data(), function (d, i) { return _chart.keyAccessor()(d, i) + '\0' + _chart.valueAccessor()(d, i); }); var gEnter = boxes.enter().append('g') .attr('class', 'box-group'); gEnter.append('rect') .attr('class', 'heat-box') .attr('fill', 'white') .on('click', _chart.boxOnClick()); if (_chart.renderTitle()) { gEnter.append('title'); boxes.select('title').text(_chart.title()); } dc.transition(boxes.select('rect'), _chart.transitionDuration(), _chart.transitionDelay()) .attr('x', function (d, i) { return cols(_chart.keyAccessor()(d, i)); }) .attr('y', function (d, i) { return rows(_chart.valueAccessor()(d, i)); }) .attr('rx', _xBorderRadius) .attr('ry', _yBorderRadius) .attr('fill', _chart.getColor) .attr('width', boxWidth) .attr('height', boxHeight); boxes.exit().remove(); var gCols = _chartBody.select('g.cols'); if (gCols.empty()) { gCols = _chartBody.append('g').attr('class', 'cols axis'); } var gColsText = gCols.selectAll('text').data(cols.domain()); gColsText.enter().append('text') .attr('x', function (d) { return cols(d) + boxWidth / 2; }) .style('text-anchor', 'middle') .attr('y', _chart.effectiveHeight()) .attr('dy', 12) .on('click', _chart.xAxisOnClick()) .text(_chart.colsLabel()); dc.transition(gColsText, _chart.transitionDuration(), _chart.transitionDelay()) .text(_chart.colsLabel()) .attr('x', function (d) { return cols(d) + boxWidth / 2; }) .attr('y', _chart.effectiveHeight()); gColsText.exit().remove(); var gRows = _chartBody.select('g.rows'); if (gRows.empty()) { gRows = _chartBody.append('g').attr('class', 'rows axis'); } var gRowsText = gRows.selectAll('text').data(rows.domain()); gRowsText.enter().append('text') .attr('dy', 6) .style('text-anchor', 'end') .attr('x', 0) .attr('dx', -2) .on('click', _chart.yAxisOnClick()) .text(_chart.rowsLabel()); dc.transition(gRowsText, _chart.transitionDuration(), _chart.transitionDelay()) .text(_chart.rowsLabel()) .attr('y', function (d) { return rows(d) + boxHeight / 2; }); gRowsText.exit().remove(); if (_chart.hasFilter()) { _chart.selectAll('g.box-group').each(function (d) { if (_chart.isSelectedNode(d)) { _chart.highlightSelected(this); } else { _chart.fadeDeselected(this); } }); } else { _chart.selectAll('g.box-group').each(function () { _chart.resetHighlight(this); }); } return _chart; }; /** * Gets or sets the handler that fires when an individual cell is clicked in the heatmap. * By default, filtering of the cell will be toggled. * @method boxOnClick * @memberof dc.heatMap * @instance * @example * // default box on click handler * chart.boxOnClick(function (d) { * var filter = d.key; * dc.events.trigger(function () { * _chart.filter(filter); * _chart.redrawGroup(); * }); * }); * @param {Function} [handler] * @returns {Function|dc.heatMap} */ _chart.boxOnClick = function (handler) { if (!arguments.length) { return _boxOnClick; } _boxOnClick = handler; return _chart; }; /** * Gets or sets the handler that fires when a column tick is clicked in the x axis. * By default, if any cells in the column are unselected, the whole column will be selected, * otherwise the whole column will be unselected. * @method xAxisOnClick * @memberof dc.heatMap * @instance * @param {Function} [handler] * @returns {Function|dc.heatMap} */ _chart.xAxisOnClick = function (handler) { if (!arguments.length) { return _xAxisOnClick; } _xAxisOnClick = handler; return _chart; }; /** * Gets or sets the handler that fires when a row tick is clicked in the y axis. * By default, if any cells in the row are unselected, the whole row will be selected, * otherwise the whole row will be unselected. * @method yAxisOnClick * @memberof dc.heatMap * @instance * @param {Function} [handler] * @returns {Function|dc.heatMap} */ _chart.yAxisOnClick = function (handler) { if (!arguments.length) { return _yAxisOnClick; } _yAxisOnClick = handler; return _chart; }; /** * Gets or sets the X border radius. Set to 0 to get full rectangles. * @method xBorderRadius * @memberof dc.heatMap * @instance * @param {Number} [xBorderRadius=6.75] * @returns {Number|dc.heatMap} */ _chart.xBorderRadius = function (xBorderRadius) { if (!arguments.length) { return _xBorderRadius; } _xBorderRadius = xBorderRadius; return _chart; }; /** * Gets or sets the Y border radius. Set to 0 to get full rectangles. * @method yBorderRadius * @memberof dc.heatMap * @instance * @param {Number} [yBorderRadius=6.75] * @returns {Number|dc.heatMap} */ _chart.yBorderRadius = function (yBorderRadius) { if (!arguments.length) { return _yBorderRadius; } _yBorderRadius = yBorderRadius; return _chart; }; _chart.isSelectedNode = function (d) { return _chart.hasFilter(d.key); }; return _chart.anchor(parent, chartGroup); }; // https://github.com/d3/d3-plugins/blob/master/box/box.js (function () { // Inspired by http://informationandvisualization.de/blog/box-plot d3.box = function () { var width = 1, height = 1, duration = 0, delay = 0, domain = null, value = Number, whiskers = boxWhiskers, quartiles = boxQuartiles, tickFormat = null; // For each small multiple… function box (g) { g.each(function (d, i) { d = d.map(value).sort(d3.ascending); var g = d3.select(this), n = d.length, min = d[0], max = d[n - 1]; // Compute quartiles. Must return exactly 3 elements. var quartileData = d.quartiles = quartiles(d); // Compute whiskers. Must return exactly 2 elements, or null. var whiskerIndices = whiskers && whiskers.call(this, d, i), whiskerData = whiskerIndices && whiskerIndices.map(function (i) { return d[i]; }); // Compute outliers. If no whiskers are specified, all data are 'outliers'. // We compute the outliers as indices, so that we can join across transitions! var outlierIndices = whiskerIndices ? d3.range(0, whiskerIndices[0]).concat(d3.range(whiskerIndices[1] + 1, n)) : d3.range(n); // Compute the new x-scale. var x1 = d3.scale.linear() .domain(domain && domain.call(this, d, i) || [min, max]) .range([height, 0]); // Retrieve the old x-scale, if this is an update. var x0 = this.__chart__ || d3.scale.linear() .domain([0, Infinity]) .range(x1.range()); // Stash the new scale. this.__chart__ = x1; // Note: the box, median, and box tick elements are fixed in number, // so we only have to handle enter and update. In contrast, the outliers // and other elements are variable, so we need to exit them! Variable // elements also fade in and out. // Update center line: the vertical line spanning the whiskers. var center = g.selectAll('line.center') .data(whiskerData ? [whiskerData] : []); center.enter().insert('line', 'rect') .attr('class', 'center') .attr('x1', width / 2) .attr('y1', function (d) { return x0(d[0]); }) .attr('x2', width / 2) .attr('y2', function (d) { return x0(d[1]); }) .style('opacity', 1e-6) .transition() .duration(duration) .delay(delay) .style('opacity', 1) .attr('y1', function (d) { return x1(d[0]); }) .attr('y2', function (d) { return x1(d[1]); }); center.transition() .duration(duration) .delay(delay) .style('opacity', 1) .attr('x1', width / 2) .attr('x2', width / 2) .attr('y1', function (d) { return x1(d[0]); }) .attr('y2', function (d) { return x1(d[1]); }); center.exit().transition() .duration(duration) .delay(delay) .style('opacity', 1e-6) .attr('y1', function (d) { return x1(d[0]); }) .attr('y2', function (d) { return x1(d[1]); }) .remove(); // Update innerquartile box. var box = g.selectAll('rect.box') .data([quartileData]); box.enter().append('rect') .attr('class', 'box') .attr('x', 0) .attr('y', function (d) { return x0(d[2]); }) .attr('width', width) .attr('height', function (d) { return x0(d[0]) - x0(d[2]); }) .transition() .duration(duration) .delay(delay) .attr('y', function (d) { return x1(d[2]); }) .attr('height', function (d) { return x1(d[0]) - x1(d[2]); }); box.transition() .duration(duration) .delay(delay) .attr('width', width) .attr('y', function (d) { return x1(d[2]); }) .attr('height', function (d) { return x1(d[0]) - x1(d[2]); }); // Update median line. var medianLine = g.selectAll('line.median') .data([quartileData[1]]); medianLine.enter().append('line') .attr('class', 'median') .attr('x1', 0) .attr('y1', x0) .attr('x2', width) .attr('y2', x0) .transition() .duration(duration) .delay(delay) .attr('y1', x1) .attr('y2', x1); medianLine.transition() .duration(duration) .delay(delay) .attr('x1', 0) .attr('x2', width) .attr('y1', x1) .attr('y2', x1); // Update whiskers. var whisker = g.selectAll('line.whisker') .data(whiskerData || []); whisker.enter().insert('line', 'circle, text') .attr('class', 'whisker') .attr('x1', 0) .attr('y1', x0) .attr('x2', width) .attr('y2', x0) .style('opacity', 1e-6) .transition() .duration(duration) .delay(delay) .attr('y1', x1) .attr('y2', x1) .style('opacity', 1); whisker.transition() .duration(duration) .delay(delay) .attr('x1', 0) .attr('x2', width) .attr('y1', x1) .attr('y2', x1) .style('opacity', 1); whisker.exit().transition() .duration(duration) .delay(delay) .attr('y1', x1) .attr('y2', x1) .style('opacity', 1e-6) .remove(); // Update outliers. var outlier = g.selectAll('circle.outlier') .data(outlierIndices, Number); outlier.enter().insert('circle', 'text') .attr('class', 'outlier') .attr('r', 5) .attr('cx', width / 2) .attr('cy', function (i) { return x0(d[i]); }) .style('opacity', 1e-6) .transition() .duration(duration) .delay(delay) .attr('cy', function (i) { return x1(d[i]); }) .style('opacity', 1); outlier.transition() .duration(duration) .delay(delay) .attr('cx', width / 2) .attr('cy', function (i) { return x1(d[i]); }) .style('opacity', 1); outlier.exit().transition() .duration(duration) .delay(delay) .attr('cy', function (i) { return x1(d[i]); }) .style('opacity', 1e-6) .remove(); // Compute the tick format. var format = tickFormat || x1.tickFormat(8); // Update box ticks. var boxTick = g.selectAll('text.box') .data(quartileData); boxTick.enter().append('text') .attr('class', 'box') .attr('dy', '.3em') .attr('dx', function (d, i) { return i & 1 ? 6 : -6; }) .attr('x', function (d, i) { return i & 1 ? width : 0; }) .attr('y', x0) .attr('text-anchor', function (d, i) { return i & 1 ? 'start' : 'end'; }) .text(format) .transition() .duration(duration) .delay(delay) .attr('y', x1); boxTick.transition() .duration(duration) .delay(delay) .text(format) .attr('x', function (d, i) { return i & 1 ? width : 0; }) .attr('y', x1); // Update whisker ticks. These are handled separately from the box // ticks because they may or may not exist, and we want don't want // to join box ticks pre-transition with whisker ticks post-. var whiskerTick = g.selectAll('text.whisker') .data(whiskerData || []); whiskerTick.enter().append('text') .attr('class', 'whisker') .attr('dy', '.3em') .attr('dx', 6) .attr('x', width) .attr('y', x0) .text(format) .style('opacity', 1e-6) .transition() .duration(duration) .delay(delay) .attr('y', x1) .style('opacity', 1); whiskerTick.transition() .duration(duration) .delay(delay) .text(format) .attr('x', width) .attr('y', x1) .style('opacity', 1); whiskerTick.exit().transition() .duration(duration) .delay(delay) .attr('y', x1) .style('opacity', 1e-6) .remove(); }); d3.timer.flush(); } box.width = function (x) { if (!arguments.length) { return width; } width = x; return box; }; box.height = function (x) { if (!arguments.length) { return height; } height = x; return box; }; box.tickFormat = function (x) { if (!arguments.length) { return tickFormat; } tickFormat = x; return box; }; box.duration = function (x) { if (!arguments.length) { return duration; } duration = x; return box; }; box.domain = function (x) { if (!arguments.length) { return domain; } domain = x === null ? x : d3.functor(x); return box; }; box.value = function (x) { if (!arguments.length) { return value; } value = x; return box; }; box.whiskers = function (x) { if (!arguments.length) { return whiskers; } whiskers = x; return box; }; box.quartiles = function (x) { if (!arguments.length) { return quartiles; } quartiles = x; return box; }; return box; }; function boxWhiskers (d) { return [0, d.length - 1]; } function boxQuartiles (d) { return [ d3.quantile(d, 0.25), d3.quantile(d, 0.5), d3.quantile(d, 0.75) ]; } })(); /** * A box plot is a chart that depicts numerical data via their quartile ranges. * * Examples: * - {@link http://dc-js.github.io/dc.js/examples/box-plot-time.html Box plot time example} * - {@link http://dc-js.github.io/dc.js/examples/box-plot.html Box plot example} * @class boxPlot * @memberof dc * @mixes dc.coordinateGridMixin * @example * // create a box plot under #chart-container1 element using the default global chart group * var boxPlot1 = dc.boxPlot('#chart-container1'); * // create a box plot under #chart-container2 element using chart group A * var boxPlot2 = dc.boxPlot('#chart-container2', 'chartGroupA'); * @param {String|node|d3.selection} parent - Any valid * {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Selections.md#selecting-elements d3 single selector} specifying * a dom block element such as a div; or a dom element or d3 selection. * @param {String} [chartGroup] - The name of the chart group this chart instance should be placed in. * Interaction with a chart will only trigger events and redraws within the chart's group. * @returns {dc.boxPlot} */ dc.boxPlot = function (parent, chartGroup) { var _chart = dc.coordinateGridMixin({}); // Returns a function to compute the interquartile range. function DEFAULT_WHISKERS_IQR (k) { return function (d) { var q1 = d.quartiles[0], q3 = d.quartiles[2], iqr = (q3 - q1) * k, i = -1, j = d.length; do { ++i; } while (d[i] < q1 - iqr); do { --j; } while (d[j] > q3 + iqr); return [i, j]; }; } var _whiskerIqrFactor = 1.5; var _whiskersIqr = DEFAULT_WHISKERS_IQR; var _whiskers = _whiskersIqr(_whiskerIqrFactor); var _box = d3.box(); var _tickFormat = null; var _boxWidth = function (innerChartWidth, xUnits) { if (_chart.isOrdinal()) { return _chart.x().rangeBand(); } else { return innerChartWidth / (1 + _chart.boxPadding()) / xUnits; } }; // default padding to handle min/max whisker text _chart.yAxisPadding(12); // default to ordinal _chart.x(d3.scale.ordinal()); _chart.xUnits(dc.units.ordinal); // valueAccessor should return an array of values that can be coerced into numbers // or if data is overloaded for a static array of arrays, it should be `Number`. // Empty arrays are not included. _chart.data(function (group) { return group.all().map(function (d) { d.map = function (accessor) { return accessor.call(d, d); }; return d; }).filter(function (d) { var values = _chart.valueAccessor()(d); return values.length !== 0; }); }); /** * Get or set the spacing between boxes as a fraction of box size. Valid values are within 0-1. * See the {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Ordinal-Scales.md#ordinal_rangeBands d3 docs} * for a visual description of how the padding is applied. * @method boxPadding * @memberof dc.boxPlot * @instance * @see {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Ordinal-Scales.md#ordinal_rangeBands d3.scale.ordinal.rangeBands} * @param {Number} [padding=0.8] * @returns {Number|dc.boxPlot} */ _chart.boxPadding = _chart._rangeBandPadding; _chart.boxPadding(0.8); /** * Get or set the outer padding on an ordinal box chart. This setting has no effect on non-ordinal charts * or on charts with a custom {@link dc.boxPlot#boxWidth .boxWidth}. Will pad the width by * `padding * barWidth` on each side of the chart. * @method outerPadding * @memberof dc.boxPlot * @instance * @param {Number} [padding=0.5] * @returns {Number|dc.boxPlot} */ _chart.outerPadding = _chart._outerRangeBandPadding; _chart.outerPadding(0.5); /** * Get or set the numerical width of the boxplot box. The width may also be a function taking as * parameters the chart width excluding the right and left margins, as well as the number of x * units. * @example * // Using numerical parameter * chart.boxWidth(10); * // Using function * chart.boxWidth((innerChartWidth, xUnits) { ... }); * @method boxWidth * @memberof dc.boxPlot * @instance * @param {Number|Function} [boxWidth=0.5] * @returns {Number|Function|dc.boxPlot} */ _chart.boxWidth = function (boxWidth) { if (!arguments.length) { return _boxWidth; } _boxWidth = d3.functor(boxWidth); return _chart; }; var boxTransform = function (d, i) { var xOffset = _chart.x()(_chart.keyAccessor()(d, i)); return 'translate(' + xOffset + ', 0)'; }; _chart._preprocessData = function () { if (_chart.elasticX()) { _chart.x().domain([]); } }; _chart.plotData = function () { var _calculatedBoxWidth = _boxWidth(_chart.effectiveWidth(), _chart.xUnitCount()); _box.whiskers(_whiskers) .width(_calculatedBoxWidth) .height(_chart.effectiveHeight()) .value(_chart.valueAccessor()) .domain(_chart.y().domain()) .duration(_chart.transitionDuration()) .tickFormat(_tickFormat); var boxesG = _chart.chartBodyG().selectAll('g.box').data(_chart.data(), _chart.keyAccessor()); renderBoxes(boxesG); updateBoxes(boxesG); removeBoxes(boxesG); _chart.fadeDeselectedArea(); }; function renderBoxes (boxesG) { var boxesGEnter = boxesG.enter().append('g'); boxesGEnter .attr('class', 'box') .attr('transform', boxTransform) .call(_box) .on('click', function (d) { _chart.filter(_chart.keyAccessor()(d)); _chart.redrawGroup(); }); } function updateBoxes (boxesG) { dc.transition(boxesG, _chart.transitionDuration(), _chart.transitionDelay()) .attr('transform', boxTransform) .call(_box) .each(function () { d3.select(this).select('rect.box').attr('fill', _chart.getColor); }); } function removeBoxes (boxesG) { boxesG.exit().remove().call(_box); } _chart.fadeDeselectedArea = function () { if (_chart.hasFilter()) { if (_chart.isOrdinal()) { _chart.g().selectAll('g.box').each(function (d) { if (_chart.isSelectedNode(d)) { _chart.highlightSelected(this); } else { _chart.fadeDeselected(this); } }); } else { var extent = _chart.brush().extent(); var start = extent[0]; var end = extent[1]; var keyAccessor = _chart.keyAccessor(); _chart.g().selectAll('g.box').each(function (d) { var key = keyAccessor(d); if (key < start || key >= end) { _chart.fadeDeselected(this); } else { _chart.highlightSelected(this); } }); } } else { _chart.g().selectAll('g.box').each(function () { _chart.resetHighlight(this); }); } }; _chart.isSelectedNode = function (d) { return _chart.hasFilter(_chart.keyAccessor()(d)); }; _chart.yAxisMin = function () { var min = d3.min(_chart.data(), function (e) { return d3.min(_chart.valueAccessor()(e)); }); return dc.utils.subtract(min, _chart.yAxisPadding()); }; _chart.yAxisMax = function () { var max = d3.max(_chart.data(), function (e) { return d3.max(_chart.valueAccessor()(e)); }); return dc.utils.add(max, _chart.yAxisPadding()); }; /** * Set the numerical format of the boxplot median, whiskers and quartile labels. Defaults to * integer formatting. * @example * // format ticks to 2 decimal places * chart.tickFormat(d3.format('.2f')); * @method tickFormat * @memberof dc.boxPlot * @instance * @param {Function} [tickFormat] * @returns {Number|Function|dc.boxPlot} */ _chart.tickFormat = function (tickFormat) { if (!arguments.length) { return _tickFormat; } _tickFormat = tickFormat; return _chart; }; return _chart.anchor(parent, chartGroup); }; /** * The select menu is a simple widget designed to filter a dimension by selecting an option from * an HTML `