/** * 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-selection/blob/master/README.md#select 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.scaleBand(); var _rowScale = d3.scaleBand(); 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(dc.filters.TwoDimensionalFilter(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(); }); } var nonstandardFilter = dc.logger.deprecate(function (filter) { return _chart._filter(dc.filters.TwoDimensionalFilter(filter)); }, 'heatmap.filter taking a coordinate is deprecated - please pass dc.filters.TwoDimensionalFilter instead'); dc.override(_chart, 'filter', function (filter) { if (!arguments.length) { return _chart._filter(); } if (filter !== null && filter.filterType !== 'TwoDimensionalFilter' && !(Array.isArray(filter) && Array.isArray(filter[0]) && filter[0][0].filterType === 'TwoDimensionalFilter')) { return nonstandardFilter(filter); } return _chart._filter(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<String|Number>} [rows] * @returns {Array<String|Number>|dc.heatMap} */ _chart.rows = function (rows) { if (!arguments.length) { return _rows; } _rows = rows; return _chart; }; /** * Get or set a comparator to order the rows. * Default is {@link https://github.com/d3/d3-array#ascending d3.ascending}. * @method rowOrdering * @memberof dc.heatMap * @instance * @param {Function} [rowOrdering] * @returns {Function|dc.heatMap} */ _chart.rowOrdering = function (rowOrdering) { if (!arguments.length) { return _rowOrdering; } _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<String|Number>} [cols] * @returns {Array<String|Number>|dc.heatMap} */ _chart.cols = function (cols) { if (!arguments.length) { return _cols; } _cols = cols; return _chart; }; /** * Get or set a comparator to order the columns. * Default is {@link https://github.com/d3/d3-array#ascending d3.ascending}. * @method colOrdering * @memberof dc.heatMap * @instance * @param {Function} [colOrdering] * @returns {Function|dc.heatMap} */ _chart.colOrdering = function (colOrdering) { if (!arguments.length) { return _colOrdering; } _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.rangeRound([0, _chart.effectiveWidth()]); rows.rangeRound([_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); }); boxes.exit().remove(); var gEnter = boxes.enter().append('g') .attr('class', 'box-group'); gEnter.append('rect') .attr('class', 'heat-box') .attr('fill', 'white') .attr('x', function (d, i) { return cols(_chart.keyAccessor()(d, i)); }) .attr('y', function (d, i) { return rows(_chart.valueAccessor()(d, i)); }) .on('click', _chart.boxOnClick()); boxes = gEnter.merge(boxes); 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); 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.exit().remove(); gColsText = 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()) .merge(gColsText); dc.transition(gColsText, _chart.transitionDuration(), _chart.transitionDelay()) .text(_chart.colsLabel()) .attr('x', function (d) { return cols(d) + boxWidth / 2; }) .attr('y', _chart.effectiveHeight()); 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.exit().remove(); gRowsText = gRowsText .enter() .append('text') .style('text-anchor', 'end') .attr('x', 0) .attr('dx', -2) .attr('y', function (d) { return rows(d) + boxHeight / 2; }) .attr('dy', 6) .on('click', _chart.yAxisOnClick()) .text(_chart.rowsLabel()) .merge(gRowsText); dc.transition(gRowsText, _chart.transitionDuration(), _chart.transitionDelay()) .text(_chart.rowsLabel()) .attr('y', function (d) { return rows(d) + boxHeight / 2; }); 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); };