monte
Version:
A visualization framework for D3.js and SVG. Ships with prebuilt charts and components.
229 lines (186 loc) • 6.55 kB
JavaScript
import { EXIT, SYMBOL_SIZE, UPDATE } from '../../const/d3';
import { AxesChart } from './AxesChart';
import { MonteError } from '../../support/MonteError';
import { commonEventNames } from '../../tools/commonEventNames';
import { noop } from '../../tools/noop';
import { resetScaleDomain } from '../../tools/resetScaleDomain';
export const ICON_MODE = {
D3_SYMBOL: 'd3Symbol',
SVG_USE_DEF: 'svgUseDef',
SVG_USE_EXTERNAL: 'svgUseExternal',
};
const ICON = 'icon';
// TODO: export icon arrangements on Monte object.
export const iconArrangeBottomTop = {
rowIndex: function(d, i) {
return Math.floor(i / this.opts.columns);
},
columnIndex: function(d, i) {
return i % this.opts.columns;
},
};
export const iconArrangeTopBottom = {
rowIndex: function(d, i) {
return this.opts.rows - Math.floor(i / this.opts.columns) - 1;
},
columnIndex: function(d, i) {
return i % this.opts.columns;
},
};
const ICON_ARRAY_DEFAULTS = {
chartCss: 'monte-icon-array',
boundingWidth: 100,
boundingHeight: 100,
margin: {
top: 0,
right: 0,
bottom: 0,
left: 0,
},
suppressAxes: true,
xScale: function() {
return d3.scalePoint().padding(0.5);
},
yScale: function() {
return d3.scalePoint().padding(0.5);
},
iconProp: '',
iconFillScale: noop,
iconFillScaleAccessor: AxesChart.generateScaleAccessor('iconFillScale', 'icon'),
iconStrokeScale: noop,
iconStrokeScaleAccessor: AxesChart.generateScaleAccessor('iconStrokeScale', 'icon'),
iconCssScale: noop,
iconCssScaleAccessor: AxesChart.generateScaleAccessor('iconCssScale', 'icon'),
iconCss: 'icon',
iconSize: SYMBOL_SIZE,
iconSymbol: (symbol) => symbol.type(d3.symbolCircle),
iconMode: ICON_MODE.D3_SYMBOL,
iconDefId: 'svgIcon',
iconSvgWidth: 24,
iconSvgHeight: 24,
iconHref: '',
iconSvgSymbol: (symbol) => {
symbol.attr('viewbox', '0 0 24 24')
.append('path')
.attr('d', 'm 0,0 24,24 m -24,0 24,-24');
},
rows: 10,
columns: 10,
arrangement: iconArrangeTopBottom,
svgVersion: 1, // SVG version 1 requires `xlink:href` for <use> references, but SVG version 2 adds a regular `href` to use instead.
};
export class IconArray extends AxesChart {
_initOptions(...options) {
super._initOptions(...options, ICON_ARRAY_DEFAULTS);
}
_initPublicEvents(...events) {
super._initPublicEvents(...events,
...commonEventNames(ICON) // Icon events
);
}
_resetStyleDomains() {
super._resetStyleDomains();
resetScaleDomain(this.opts.iconCssScale);
resetScaleDomain(this.opts.iconFillScale);
resetScaleDomain(this.opts.iconStrokeScale);
}
_domainExtent(data, scaleName) {
let extent = null;
if (scaleName === 'y') {
extent = new Array(this.opts.rows);
}
else if (scaleName === 'x') {
extent = new Array(this.opts.columns);
}
return extent.fill().map((_, idx) => idx);
}
_render() {
if (this.opts.iconMode === ICON_MODE.SVG_USE_DEF) {
const chart = this;
this.defs.append('symbol')
.attr('id', 'svgIcon')
.each(function() {
chart.opts.iconSvgSymbol(d3.select(this));
});
}
}
_update() {
const max = this.opts.rows * this.opts.columns;
if (max < this.displayData.length ) {
throw new MonteError(`Maximum number of items is ${max}. Data contains ${this.displayData.length}.`);
}
this._updateIcons();
}
_updateIcons() {
const modeMap = {
[ICON_MODE.D3_SYMBOL]: this._updateD3Symbol,
[ICON_MODE.SVG_USE_DEF]: this._updateSvgUse,
[ICON_MODE.SVG_USE_EXTERNAL]: this._updateSvgUse,
};
const icons = this.draw.selectAll('.monte-icon').data(this.displayData, this.opts.dataKey);
modeMap[this.opts.iconMode].call(this, icons, this.opts.iconMode);
// Fade out removed icons.
icons.exit()
.transition()
.call(this._transitionSetup(ICON, EXIT))
.style('opacity', 0)
.remove();
}
_updateD3Symbol(icons) {
const genSym = (d, i) => {
const size = this.tryInvoke(this.opts.iconSize, d, i);
const symbase = d3.symbol().size(size);
const symbol = this.opts.iconSymbol(symbase, d, i);
return symbol(d, i);
};
this._updateCommon('path', icons, iconTransform).attr('d', genSym);
}
_updateSvgUse(icons, mode) {
const href = mode === ICON_MODE.SVG_USE_DEF ?
(d, i, nodes) => '#' + this.tryInvoke(this.opts.iconDefId, d, i, nodes) :
(d, i, nodes) => this.tryInvoke(this.opts.iconHref, d, i, nodes);
const mergedUpdates = (d, i, nodes) => {
const node = d3.select(nodes[i]);
node.attr('width', this.opts.iconSvgWidth)
.attr('height', this.opts.iconSvgHeight);
};
const hrefAttr = this.tryInvoke(this.opts.svgVersion) === 2 ? 'href' : 'xlink:href';
this._updateCommon('use', icons, iconTransformShift, mergedUpdates).attr(hrefAttr, href);
}
_updateCommon(type, icons, transform, merge = noop) {
const t = icons.enter().append(type)
.call(this.__bindCommonEvents(ICON))
// TODO: Split apart merge
.merge(icons)
.each(merge)
.attr('transform', (d, i, nodes) => transform.call(this, d, i, nodes))
.attr('class', (d, i) => this._buildCss([
'monte-icon',
this.opts.iconCss,
this.opts.iconCssScaleAccessor,
d.css], d, i))
.call((sel) => this.fnInvoke(this.opts.iconUpdateSelectionCustomize, sel))
.transition()
.call(this._transitionSetup(ICON, UPDATE))
.style('fill', this.optionReaderFunc('iconFillScaleAccessor'))
.style('stroke', this.optionReaderFunc('iconStrokeScaleAccessor'))
.call((t) => this.fnInvoke(this.opts.iconUpdateTransitionCustomize, t));
return t;
}
}
function iconTransform(d, i, nodes) {
const col = this.tryInvoke(this.opts.arrangement.columnIndex, d, i, nodes);
const row = this.tryInvoke(this.opts.arrangement.rowIndex, d, i, nodes);
const x = this.getScaledProp('x', col);
const y = this.getScaledProp('y', row);
return `translate(${x}, ${y})`;
}
function iconTransformShift(d, i, nodes) {
const col = this.tryInvoke(this.opts.arrangement.columnIndex, d, i, nodes);
const row = this.tryInvoke(this.opts.arrangement.rowIndex, d, i, nodes);
const x = this.getScaledProp('x', col);
const y = this.getScaledProp('y', row);
const xShift = this.opts.iconSvgWidth / 2;
const yShift = this.opts.iconSvgHeight / 2;
return `translate(${x - xShift}, ${y - yShift})`;
}