UNPKG

monte

Version:

A visualization framework for D3.js and SVG. Ships with prebuilt charts and components.

285 lines (214 loc) 8.03 kB
import { ENTER, UPDATE } from '../../const/d3'; import { isArray, isDefined, isFunc } from '../../tools/is'; import { Chart } from '../Chart'; import { MonteError } from '../../support/MonteError'; import { MonteOptionError } from '../../support/MonteOptionError'; import { noop } from '../../tools/noop'; const EVENT_AXIS_BEFORE_RENDER = 'beforeAxisRender'; const EVENT_AXIS_RENDERED = 'axisRendered'; const EVENTS = [EVENT_AXIS_BEFORE_RENDER, EVENT_AXIS_RENDERED]; const AXES_CHART_DEFAULTS = { // The axes X and Y are generally assumed. In some cases it may be desirable to add an additional // axis such as 'Y2'. axes: ['x', 'y'], // The scale names to create axes for. suppressAxes: false, // Suppress the display of the axes. /************************************************************************************************* * * "X"-related Options * ************************************************************************************************/ // The property name of the value for the X coordinate passed to the scale function. xProp: 'x', // The scale function for X values. xScale: d3.scaleLinear, // Callback function to customize the X axis, such as tick count and format. xAxisCustomize: null, // Callback function to customize the X extent. xDomainCustomize: null, xRange: (w, h) => [0, w], // eslint-disable-line no-unused-vars xAxisConstructor: d3.axisBottom, xAxisTransform: (w, h) => `translate(0,${h})`, xLabel: null, xLabelCustomize: noop, xTickLabelCustomize: noop, /************************************************************************************************* * * "Y"-related Options * ************************************************************************************************/ // The property name of the value for the X coordinate passed to the scale function. yProp: 'y', // The scale function for Y values. yScale: d3.scaleLinear, // Callback function to customize the X axis, such as tick count and format. yAxisCustomize: null, // Callback function to customize the Y extent. yDomainCustomize: null, yRange: (w, h) => [h, 0], yAxisConstructor: d3.axisLeft, yAxisTransform: null, yLabel: null, yLabelCustomize: noop, yTickLabelCustomize: noop, }; export class AxesChart extends Chart { __axisOpt(scaleName, option) { return this.opts[`${scaleName}${option}`]; } _initOptions(...options) { super._initOptions(...options, AXES_CHART_DEFAULTS); if (!isDefined(this.opts.axes)) { // Set empty array to ease assumptions (i.e. avoid null checks) in later code. this.opts.axes = []; } this.axes = this.tryInvoke(this.opts.axes); } _initPublicEvents(...events) { super._initPublicEvents(...events, ...EVENTS // Axis events ); } _initCore() { super._initCore(); this.forEachAxisScale((scaleName) => { // Create scale const range = this.__axisOpt(scaleName, 'Range')(this.width, this.height); const scale = this.__axisOpt(scaleName, 'Scale')().range(range); this[scaleName] = scale; // Setup axes this[`${scaleName}Axis`] = this.__axisOpt(scaleName, 'AxisConstructor')(scale); }); } _initCustomize() { this.forEachAxisScale((scaleName) => { const customize = this.__axisOpt(scaleName, 'AxisCustomize'); if (isArray(customize)) { const axis = this[`${scaleName}Axis`]; customize.forEach((customFunc) => customFunc.call(this, axis)); } else if (isFunc(customize)) { customize.call(this, this[`${scaleName}Axis`]); } }); } _initRender() { // Attach axes this.forEachAxisScale((scaleName) => { this.support.append('g').attr('class', `${scaleName}-axis axis`); }); this.updateAxesTransforms(); super._initRender(); } _boundsUpdate() { const actions = super._boundsUpdate(true, true); this.updateAxesRanges(); this.updateAxesTransforms(); this.renderAxes(); actions.notify(); actions.update(); } _data(data) { super._data(data); this.updateAxesDomains(); this.renderAxes(); } replaceScale(scaleName, newScaleConstructor) { super.replaceScale(scaleName, newScaleConstructor); this[`${scaleName}Axis`].scale(this[scaleName]); this.renderAxes(); return this; } updateAxesTransforms() { this.forEachAxisScale((scaleName) => { const axisGrp = this.support.select(`.${scaleName}-axis`); const trans = this.__axisOpt(scaleName, 'AxisTransform'); if (trans) { axisGrp.attr('transform', this.tryInvoke(trans, this.width, this.height)); } }); return this; } updateAxesRanges() { this.forEachAxisScale((scaleName) => { const rangeFunc = this.__axisOpt(scaleName, 'Range'); const range = rangeFunc.call(this, this.width, this.height); this[scaleName].range(range); }); return this; } updateAxesDomains() { const data = this.data(); this.forEachAxisScale((scaleName) => { const customize = this.opts[scaleName + 'DomainCustomize']; let extent = data ? this._domainExtent(data, scaleName) : []; if (customize) { extent = this.tryInvoke(customize, extent); } this[scaleName].domain(extent); }); return this; } renderAxes() { // Only suppress all if a literal boolean is given. const suppressAxes = this.tryInvoke(this.opts.suppressAxes); if (suppressAxes === true) { return; } const isSuppressArray = isArray(suppressAxes); const stage = this.hasRendered ? UPDATE : ENTER; const t = this.bound.transition().call(this._transitionSetup('axis', stage)); // (Re)render axes this.forEachAxisScale((scaleName) => { if (isSuppressArray && suppressAxes.indexOf(scaleName) > -1) { return; } const axis = this[`${scaleName}Axis`]; this.emit(EVENT_AXIS_BEFORE_RENDER, scaleName, axis); this.support.select(`.${scaleName}-axis`) .transition(t) .call(axis) .call(this._setLabel.bind(this, scaleName)) .call((t) => this.emit(EVENT_AXIS_RENDERED, scaleName, axis, t)); }); return this; } _domainExtent(data, scaleName) { // eslint-disable-line no-unused-vars if (this.opts.directUse) { // Provide simple default extent that can be overridden by the corresponding // `<scaleName>DomainCustomize` option. return [0, 1]; } throw MonteError.UnimplementedMethod('Domain Extent', '_domainExtent'); } // Loops over each scale name that is bound to an axis. forEachAxisScale(f) { this.axes.forEach(f); return this; } _setLabel(scaleName, transition) { // eslint-disable-line no-unused-vars const optLabel = this.opts[`${scaleName}Label`]; const label = isDefined(optLabel) ? this.tryInvoke(this.opts[`${scaleName}Label`]) : null; const data = label === null ? [] : [label]; const lbl = this.support.select(`.${scaleName}-axis`).selectAll('.monte-axis-label').data(data); lbl.enter().append('text') .merge(lbl) .attr('class', 'monte-axis-label') .text((d) => d) .call((lbls) => { const opt =`${scaleName}LabelCustomize`; const lblCustomize = this.opts[opt]; if (lblCustomize) { if (isFunc(lblCustomize)) { this.fnInvoke(lblCustomize, lbls); } else { throw MonteOptionError.OptionMustBeFunction(opt, `(${opt} is optional)`); } } }); lbl.exit().remove(); } static createInstanceGroup(charts, ...additionalMethodsToProxy) { additionalMethodsToProxy.push(GROUP_PROXY_METHODS); return super.createInstanceGroup(charts, ...additionalMethodsToProxy); } } AxesChart.EVENTS = EVENTS; export const GROUP_PROXY_METHODS = [ 'forEachAxisScale', 'renderAxes', 'replaceScale', 'updateAxesDomains', 'updateAxesRanges', 'updateAxesTransforms', ];