/**
* `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 _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 = height ? (typeof height === 'function' ? height : dc.utils.constant(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 = width ? (typeof width === 'function' ? width : dc.utils.constant(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 = typeof callback === 'function' ? callback : dc.utils.constant(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().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://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort Array.sort}
* to sort elements; this accessor returns the value to order on.
* @method ordering
* @memberof dc.baseMixin
* @instance
* @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;
_chart.expireCache();
return _chart;
};
_chart._computeOrderedGroups = function (data) {
// clone the array before sorting, otherwise Array.sort sorts in-place
return data.slice().sort(function (a, b) { return _ordering(a) - _ordering(b) });
};
/**
* 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-selection/blob/master/README.md#select d3.select}
* @example
* // Has the same effect as d3.select('#chart-id').select(selector)
* chart.select(selector)
* @param {String} sel CSS selector string
* @returns {d3.selection}
*/
_chart.select = function (sel) {
return _root.select(sel);
};
/**
* 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-selection/blob/master/README.md#selectAll d3.selectAll}
* @example
* // Has the same effect as d3.select('#chart-id').selectAll(selector)
* chart.selectAll(selector)
* @param {String} sel CSS selector string
* @returns {d3.selection}
*/
_chart.selectAll = function (sel) {
return _root ? _root.selectAll(sel) : null;
};
/**
* Set the root SVGElement to either be an existing chart's root; or any valid [d3 single
* selector](https://github.com/d3/d3-selection/blob/master/README.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();
if (_anchor.children) { // is _anchor a div?
_anchor = '#' + parent.anchorName();
}
_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 (controlsUseVisibility) {
if (!arguments.length) {
return _controlsUseVisibility;
}
_controlsUseVisibility = controlsUseVisibility;
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.call('preRender', _chart, _chart);
if (_mandatoryAttributes) {
_mandatoryAttributes.forEach(checkForMandatoryAttributes);
}
var result = _chart._doRender();
if (_legend) {
_legend.render();
}
_chart._activateRenderlets('postRender');
return result;
};
_chart._activateRenderlets = function (event) {
_listeners.call('pretransition', _chart, _chart);
if (_chart.transitionDuration() > 0 && _svg) {
_svg.transition().duration(_chart.transitionDuration()).delay(_chart.transitionDelay())
.on('end', function () {
_listeners.call('renderlet', _chart, _chart);
if (event) {
_listeners.call(event, _chart, _chart);
}
});
} else {
_listeners.call('renderlet', _chart, _chart);
if (event) {
_listeners.call(event, _chart, _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.call('preRedraw', _chart, _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 done. The callback has the standard node.js continuation signature
* with error first and result second.
* @method commitHandler
* @param {Function} 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.call('filtered', _chart, _chart, f);
}
};
_chart._invokeZoomedListener = function () {
_listeners.call('zoomed', _chart, _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.
*
* This function can be replaced in order to change the click behavior (but first look at
* @method onClick
* @memberof dc.baseMixin
* @instance
* @example
* var oldHandler = chart.onClick;
* chart.onClick = function(datum) {
* // use datum.
* @param {*} datum
* @return {undefined}
*/
_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.<renderletKey>", 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-dispatch/blob/master/README.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;
};