monte
Version:
A visualization framework for D3.js and SVG. Ships with prebuilt charts and components.
236 lines (190 loc) • 7.84 kB
JavaScript
import { ENTER, EXIT, SYMBOL_SIZE, UPDATE } from '../../const/d3';
import { AxesChart } from './AxesChart';
import { commonEventNames } from '../../tools/commonEventNames';
import { noop } from '../../tools/noop';
import { resetScaleDomain } from '../../tools/resetScaleDomain';
import { upperFirst } from '../../tools/string';
const LINE = 'line';
const POINT = 'point';
const LINE_CHART_DEFAULTS = {
chartCss: 'monte-line-chart',
margin: {
top: 10,
right: 10,
bottom: 30,
left: 40,
},
/***********************************************************************************************
*
* Line related options
*
**********************************************************************************************/
valuesProp: 'values',
lineProp: '',
// Callback function to customize the line generator, such as set the interpolate.
lineCustomize: null,
lineStrokeScale: noop,
lineStrokeScaleAccessor: AxesChart.generateScaleAccessor('lineStrokeScale', 'lineProp'),
// Scale function for CSS class to apply per line. Input: line index, Output: String of CSS Class.
lineCssScale: noop,
lineCssScaleAccessor: AxesChart.generateScaleAccessor('lineCssScale', 'lineProp'),
// Static CSS class(es) to apply to every line.
lineCss: 'line',
/***********************************************************************************************
*
* Point related options
*
**********************************************************************************************/
includePoints: false,
pointProp: '',
pointFillScale: noop,
pointFillScaleAccessor: AxesChart.generateScaleAccessor('pointFillScale', 'pointProp'),
pointStrokeScale: noop,
pointStrokeScaleAccessor: AxesChart.generateScaleAccessor('pointStrokeScale', 'pointProp'),
// Scale function for CSS class to apply per line. Input: line index, Output: String of CSS Class.
pointCssScale: noop,
pointCssScaleAccessor: AxesChart.generateScaleAccessor('pointCssScale', 'pointProp'),
// Static CSS class(es) to apply to every line.
pointCss: 'point',
pointSize: SYMBOL_SIZE,
pointSymbol: (symbol) => symbol.type(d3.symbolCircle),
pointDataKey: AxesChart.defaultDataKey,
};
export class LineChart extends AxesChart {
_initOptions(...options) {
super._initOptions(...options, LINE_CHART_DEFAULTS);
}
_initCore() {
super._initCore();
// Initialize the line generator
this.line = d3.line()
.x((d) => this.getScaledProp('x', d))
.y((d) => this.getScaledProp('y', d));
}
_initCustomize() {
super._initCustomize();
if (this.opts.lineCustomize) { this.opts.lineCustomize(this.line); }
}
_initPublicEvents(...events) {
super._initPublicEvents(...events,
...commonEventNames(LINE), // Line events
...commonEventNames(POINT) // Point events
);
}
_domainExtent(data, scaleName) {
const valuesProp = this.opts.valuesProp;
const itemProp = this.opts[scaleName + 'Prop'];
const extents = data.map((line) => d3.extent(line[valuesProp], (d) => d[itemProp]));
const extent = [d3.min(extents, (d) => d[0]), d3.max(extents, (d) => d[1])];
return extent;
}
_resetStyleDomains() {
super._resetStyleDomains();
resetScaleDomain(this.opts.lineStrokeScale);
resetScaleDomain(this.opts.lineCssScale);
resetScaleDomain(this.opts.pointFillScale);
resetScaleDomain(this.opts.pointStrokeScale);
resetScaleDomain(this.opts.pointCssScale);
}
// Render the vis.
_update() {
const lineGrps = this._updateLines();
if (this.opts.includePoints) {
lineGrps.each((d, i, nodes) => this._updateLinePoints(nodes[i], d, i));
}
return lineGrps;
}
_updateLines() {
// Data join for the lines
const lineGrps = this.draw.selectAll('.monte-line-grp')
.data(this.displayData, this.opts.dataKey);
const enterLines = lineGrps.enter().append('g')
.attr('class', 'monte-line-grp')
.append('path')
.call(this.__bindCommonEvents(LINE));
this._updateLineSelection(enterLines, ENTER);
// Update existing lines and set values on new lines.
const updateLines = lineGrps.select('.monte-line');
this._updateLineSelection(updateLines, UPDATE);
// Fade out removed lines.
lineGrps.exit()
.transition()
.call(this._transitionSetup(LINE, EXIT))
.style('opacity', 0)
.remove();
// Here the order is important. Merging the line groups when only an update occurs results in an
// empty selection if the command was lineGrps.enter().selectAll('.grp-line').merge(lineGrps);
return lineGrps.merge(lineGrps.enter().selectAll('.monte-line-grp'));
}
_updateLineSelection(sel, stage) {
const selectionFnName = LINE + upperFirst(stage) + 'SelectionCustomize';
const transitionFnName = LINE + upperFirst(stage) + 'TransitionCustomize';
sel.attr('class', (d, i) => this._buildCss(
['monte-line',
this.opts.lineCss,
this.opts.lineCssScaleAccessor,
d.css], d, i))
.call((sel) => this.fnInvoke(this.opts[selectionFnName], sel))
.transition()
.call(this._transitionSetup(LINE, stage))
.attr('d', (d) => this.line(this.getProp('values', d)))
.style('stroke', this.optionReaderFunc('lineStrokeScaleAccessor'))
.call((t) => this.fnInvoke(this.opts[transitionFnName], t));
}
_updateLinePoints(node, lineDatum, lineIndex) {
const lineGrp = d3.select(node);
// Data join for the points
const points = lineGrp.selectAll('.monte-point')
.data((d) => this.getProp('values', d), this.opts.pointDataKey);
const genSym = (d, i) => {
const size = this.tryInvoke(this.opts.pointSize, d, i);
const symbase = d3.symbol().size(size);
const symbol = this.opts.pointSymbol(symbase, d, i);
return symbol(d, i);
};
// Create new points
points.enter().append('path')
.call(this.__bindCommonEvents(POINT))
.attr('d', genSym)
.attr('transform', (d) => this._translatePoint(d))
.attr('class', (d) => this._buildCss(
['monte-point',
lineDatum.css,
this.opts.lineCssScaleAccessor,
this.opts.pointCss,
this.opts.pointCssScaleAccessor,
d.css], lineDatum, lineIndex))
.call((sel) => this.fnInvoke(this.opts.pointEnterSelectionCustomize, sel))
.transition()
.call(this._transitionSetup(POINT, ENTER))
.attr('transform', (d) => this._translatePoint(d))
.call((sel) => this.fnInvoke(this.opts.pointEnterTransitionCustomize, sel));
// Update existing points
points.attr('class', (d) => this._buildCss(
['monte-point',
lineDatum.css,
this.opts.lineCssScaleAccessor,
this.opts.pointCss,
this.opts.pointCssScaleAccessor,
d.css], lineDatum, lineIndex))
.call((sel) => this.fnInvoke(this.opts.pointUpdateSelectionCustomize, sel))
.transition()
.call(this._transitionSetup(POINT, UPDATE))
.style('fill', this.optionReaderFunc('pointFillScaleAccessor'))
.style('stroke', this.optionReaderFunc('pointStrokeScaleAccessor'))
.attr('transform', (d) => this._translatePoint(d))
.attr('d', genSym)
.call((sel) => this.fnInvoke(this.opts.pointUpdateTransitionCustomize, sel));
// Fade out removed points.
points.exit()
.call((sel) => this.fnInvoke(this.opts.pointExitSelectionCustomize, sel))
.transition()
.call(this._transitionSetup(POINT, EXIT))
.style('opacity', 0)
.call((sel) => this.fnInvoke(this.opts.pointExitTransitionCustomize, sel))
.remove();
}
_translatePoint(d) {
return `translate(${this.getScaledProp('x', d)}, ${this.getScaledProp('y', d)})`;
}
}