import {extent} from 'd3-array'; import {axisBottom} from 'd3-axis'; import {scaleLinear} from 'd3-scale'; import {CapMixin} from '../base/cap-mixin'; import {MarginMixin} from '../base/margin-mixin'; import {ColorMixin} from '../base/color-mixin'; import {transition} from '../core/core'; import {d3compat} from '../core/config'; /** * Concrete row chart implementation. * * Examples: * - {@link http://dc-js.github.com/dc.js/ Nasdaq 100 Index} * @mixes CapMixin * @mixes MarginMixin * @mixes ColorMixin * @mixes BaseMixin */ export class RowChart extends CapMixin(ColorMixin(MarginMixin)) { /** * Create a Row Chart. * @example * // create a row chart under #chart-container1 element using the default global chart group * var chart1 = new RowChart('#chart-container1'); * // create a row chart under #chart-container2 element using chart group A * var chart2 = new RowChart('#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. */ constructor (parent, chartGroup) { super(); this._g = undefined; this._labelOffsetX = 10; this._labelOffsetY = 15; this._hasLabelOffsetY = false; this._dyOffset = '0.35em'; // this helps center labels https://github.com/d3/d3-3.x-api-reference/blob/master/SVG-Shapes.md#svg_text this._titleLabelOffsetX = 2; this._gap = 5; this._fixedBarHeight = false; this._rowCssClass = 'row'; this._titleRowCssClass = 'titlerow'; this._renderTitleLabel = false; this._x = undefined; this._elasticX = undefined; this._xAxis = axisBottom(); this._rowData = undefined; this.rowsCap = this.cap; this.title(d => `${this.cappedKeyAccessor(d)}: ${this.cappedValueAccessor(d)}`); this.label(d => this.cappedKeyAccessor(d)); this.anchor(parent, chartGroup); } _calculateAxisScale () { if (!this._x || this._elasticX) { const _extent = extent(this._rowData, d => this.cappedValueAccessor(d)); if (_extent[0] > 0) { _extent[0] = 0; } if (_extent[1] < 0) { _extent[1] = 0; } this._x = scaleLinear().domain(_extent) .range([0, this.effectiveWidth()]); } this._xAxis.scale(this._x); } _drawAxis () { let axisG = this._g.select('g.axis'); this._calculateAxisScale(); if (axisG.empty()) { axisG = this._g.append('g').attr('class', 'axis'); } axisG.attr('transform', `translate(0, ${this.effectiveHeight()})`); transition(axisG, this.transitionDuration(), this.transitionDelay()) .call(this._xAxis); } _doRender () { this.resetSvg(); this._g = this.svg() .append('g') .attr('transform', `translate(${this.margins().left},${this.margins().top})`); this._drawChart(); return this; } /** * Gets or sets the x scale. The x scale can be any d3 * {@link https://github.com/d3/d3-scale/blob/master/README.md d3.scale}. * @see {@link https://github.com/d3/d3-scale/blob/master/README.md d3.scale} * @param {d3.scale} [scale] * @returns {d3.scale|RowChart} */ x (scale) { if (!arguments.length) { return this._x; } this._x = scale; return this; } _drawGridLines () { this._g.selectAll('g.tick') .select('line.grid-line') .remove(); this._g.selectAll('g.tick') .append('line') .attr('class', 'grid-line') .attr('x1', 0) .attr('y1', 0) .attr('x2', 0) .attr('y2', () => -this.effectiveHeight()); } _drawChart () { this._rowData = this.data(); this._drawAxis(); this._drawGridLines(); let rows = this._g.selectAll(`g.${this._rowCssClass}`) .data(this._rowData); this._removeElements(rows); rows = this._createElements(rows) .merge(rows); this._updateElements(rows); } _createElements (rows) { const rowEnter = rows.enter() .append('g') .attr('class', (d, i) => `${this._rowCssClass} _${i}`); rowEnter.append('rect').attr('width', 0); this._createLabels(rowEnter); return rowEnter; } _removeElements (rows) { rows.exit().remove(); } _rootValue () { const root = this._x(0); return (root === -Infinity || root !== root) ? this._x(1) : root; } _updateElements (rows) { const n = this._rowData.length; let height; if (!this._fixedBarHeight) { height = (this.effectiveHeight() - (n + 1) * this._gap) / n; } else { height = this._fixedBarHeight; } // vertically align label in center unless they override the value via property setter if (!this._hasLabelOffsetY) { this._labelOffsetY = height / 2; } const rect = rows.attr('transform', (d, i) => `translate(0,${(i + 1) * this._gap + i * height})`).select('rect') .attr('height', height) .attr('fill', this.getColor) .on('click', d3compat.eventHandler(d => this._onClick(d))) .classed('dc-tabbable', this._keyboardAccessible) .classed('deselected', d => (this.hasFilter()) ? !this._isSelectedRow(d) : false) .classed('selected', d => (this.hasFilter()) ? this._isSelectedRow(d) : false); if (this._keyboardAccessible) { this._makeKeyboardAccessible(d => this._onClick(d)); } transition(rect, this.transitionDuration(), this.transitionDelay()) .attr('width', d => Math.abs(this._rootValue() - this._x(this.cappedValueAccessor(d)))) .attr('transform', d => this._translateX(d)); this._createTitles(rows); this._updateLabels(rows); } _createTitles (rows) { if (this.renderTitle()) { rows.select('title').remove(); rows.append('title').text(this.title()); } } _createLabels (rowEnter) { if (this.renderLabel()) { rowEnter.append('text') .on('click', d3compat.eventHandler(d => this._onClick(d))); } if (this.renderTitleLabel()) { rowEnter.append('text') .attr('class', this._titleRowCssClass) .on('click', d3compat.eventHandler(d => this._onClick(d))); } } _updateLabels (rows) { if (this.renderLabel()) { const lab = rows.select('text') .attr('x', this._labelOffsetX) .attr('y', this._labelOffsetY) .attr('dy', this._dyOffset) .on('click', d3compat.eventHandler(d => this._onClick(d))) .attr('class', (d, i) => `${this._rowCssClass} _${i}`) .text(d => this.label()(d)); transition(lab, this.transitionDuration(), this.transitionDelay()) .attr('transform', d => this._translateX(d)); } if (this.renderTitleLabel()) { const titlelab = rows.select(`.${this._titleRowCssClass}`) .attr('x', this.effectiveWidth() - this._titleLabelOffsetX) .attr('y', this._labelOffsetY) .attr('dy', this._dyOffset) .attr('text-anchor', 'end') .on('click', d3compat.eventHandler(d => this._onClick(d))) .attr('class', (d, i) => `${this._titleRowCssClass} _${i}`) .text(d => this.title()(d)); transition(titlelab, this.transitionDuration(), this.transitionDelay()) .attr('transform', d => this._translateX(d)); } } /** * Turn on/off Title label rendering (values) using SVG style of text-anchor 'end'. * @param {Boolean} [renderTitleLabel=false] * @returns {Boolean|RowChart} */ renderTitleLabel (renderTitleLabel) { if (!arguments.length) { return this._renderTitleLabel; } this._renderTitleLabel = renderTitleLabel; return this; } _onClick (d) { this.onClick(d); } _translateX (d) { const x = this._x(this.cappedValueAccessor(d)), x0 = this._rootValue(), s = x > x0 ? x0 : x; return `translate(${s},0)`; } _doRedraw () { this._drawChart(); return this; } /** * Get or sets the x axis for the row chart instance. * See the {@link https://github.com/d3/d3-axis/blob/master/README.md d3.axis} * documention for more information. * @param {d3.axis} [xAxis] * @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]); * // use a top-oriented axis. Note: position of the axis and grid lines will need to * // be set manually, see https://dc-js.github.io/dc.js/examples/row-top-axis.html * chart.xAxis(d3.axisTop()) * @returns {d3.axis|RowChart} */ xAxis (xAxis) { if (!arguments.length) { return this._xAxis; } this._xAxis = xAxis; return this; } /** * 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). * @example * chart.fixedBarHeight( chartheight - (count + 1) * gap / count); * @param {Boolean|Number} [fixedBarHeight=false] * @returns {Boolean|Number|RowChart} */ fixedBarHeight (fixedBarHeight) { if (!arguments.length) { return this._fixedBarHeight; } this._fixedBarHeight = fixedBarHeight; return this; } /** * Get or set the vertical gap space between rows on a particular row chart instance. * @param {Number} [gap=5] * @returns {Number|RowChart} */ gap (gap) { if (!arguments.length) { return this._gap; } this._gap = gap; return this; } /** * Get or set the elasticity on x axis. If this attribute is set to true, then the x axis will rescale to auto-fit the * data range when filtered. * @param {Boolean} [elasticX] * @returns {Boolean|RowChart} */ elasticX (elasticX) { if (!arguments.length) { return this._elasticX; } this._elasticX = elasticX; return this; } /** * Get or set the x offset (horizontal space to the top left corner of a row) for labels on a particular row chart. * @param {Number} [labelOffsetX=10] * @returns {Number|RowChart} */ labelOffsetX (labelOffsetX) { if (!arguments.length) { return this._labelOffsetX; } this._labelOffsetX = labelOffsetX; return this; } /** * Get or set the y offset (vertical space to the top left corner of a row) for labels on a particular row chart. * @param {Number} [labelOffsety=15] * @returns {Number|RowChart} */ labelOffsetY (labelOffsety) { if (!arguments.length) { return this._labelOffsetY; } this._labelOffsetY = labelOffsety; this._hasLabelOffsetY = true; return this; } /** * Get of set the x offset (horizontal space between right edge of row and right edge or text. * @param {Number} [titleLabelOffsetX=2] * @returns {Number|RowChart} */ titleLabelOffsetX (titleLabelOffsetX) { if (!arguments.length) { return this._titleLabelOffsetX; } this._titleLabelOffsetX = titleLabelOffsetX; return this; } _isSelectedRow (d) { return this.hasFilter(this.cappedKeyAccessor(d)); } } export const rowChart = (parent, chartGroup) => new RowChart(parent, chartGroup);