src/elements/element.generic.cartesian.ts
import {Element} from './element';
import {GrammarRegistry} from '../grammar-registry';
import {d3_animationInterceptor} from '../utils/d3-decorators';
import * as utils from '../utils/utils';
import * as d3 from 'd3-selection';
import {
d3Selection,
GrammarElement,
GrammarModel,
GrammarRule,
ScaleFactoryMethod,
ScreenModel,
Unit
} from '../definitions';
export abstract class GenericCartesian extends Element {
decorators: GrammarRule[];
adjusters: GrammarRule[];
constructor(config: Unit) {
super(config);
this.config = config;
this.config.guide = utils.defaults(
(this.config.guide || {}),
{
animationSpeed: 0,
enableColorToBarPosition: false
});
this.config.guide.size = (this.config.guide.size || {});
var enableStack = this.config.stack;
var enableColorPositioning = this.config.guide.enableColorToBarPosition;
var defaultDecorators = [
config.flip && GrammarRegistry.get('flip'),
enableStack && GrammarRegistry.get('stack'),
enableColorPositioning && GrammarRegistry.get('positioningByColor')
];
this.decorators = (this.config.transformRules || defaultDecorators).concat(config.transformModel || []);
this.adjusters = (this.config.adjustRules || []).concat(config.adjustScales || []);
}
defineGrammarModel(fnCreateScale: ScaleFactoryMethod): GrammarModel {
const config = this.config;
this.regScale('x', fnCreateScale('pos', config.x, [0, config.options.width]))
.regScale('y', fnCreateScale('pos', config.y, [config.options.height, 0]))
.regScale('y', fnCreateScale(
'pos',
config.y,
(scaleConfig) => ['ordinal', 'period'].indexOf(scaleConfig.type) >= 0 ?
[0, config.options.height] :
[config.options.height, 0]
))
.regScale('size', fnCreateScale('size', config.size, {}))
.regScale('color', fnCreateScale('color', config.color, {}))
.regScale('split', fnCreateScale('split', config.split, {}))
.regScale('label', fnCreateScale('label', config.label, {}))
.regScale('identity', fnCreateScale('identity', config.identity, {}));
const scaleX = this.getScale('x');
const scaleY = this.getScale('y');
const scaleSize = this.getScale('size');
const scaleLabel = this.getScale('label');
const scaleColor = this.getScale('color');
const scaleSplit = this.getScale('split');
const scaleIdentity = this.getScale('identity');
const ys = scaleY.domain();
const min = scaleY.discrete ?
ys[0] :
Math.max(0, Math.min(...ys)); // NOTE: max also can be below 0
const y0 = scaleY.value(min) + scaleY.stepSize(min) * 0.5;
const order = scaleColor.domain();
const delimiter = '(@taucharts@)';
const model = {
data: (() => this.data()),
flip: false,
scaleX,
scaleY,
scaleSize,
scaleLabel,
scaleColor,
scaleSplit,
scaleIdentity,
color: ((d) => scaleColor.value(d[scaleColor.dim])),
label: ((d) => scaleLabel.value(d[scaleLabel.dim])),
group: ((d) => (`${d[scaleColor.dim]}${delimiter}${d[scaleSplit.dim]}`)),
order: ((group) => {
const color = group.split(delimiter)[0];
const i = order.indexOf(color);
return ((i < 0) ? Number.MAX_VALUE : i);
}),
size: ((d) => (scaleSize.value(d[scaleSize.dim]))),
id: ((row) => scaleIdentity.value(row[scaleIdentity.dim], row)),
xi: ((d) => scaleX.value(d[scaleX.dim])),
yi: ((d) => scaleY.value(d[scaleY.dim])),
y0: (() => y0)
};
// NOTE: Request rows IDs for the first time to prevent
// cases when plugins request these IDs in different
// order after updating chart config dynamically.
model.data().forEach((row) => model.id(row));
return model;
}
getGrammarRules() {
return this.decorators.filter(x => x);
}
getAdjustScalesRules() {
return (this.adjusters || []).filter(x => x);
}
createScreenModel(grammarModel: GrammarModel): ScreenModel {
const flip = grammarModel.flip;
const iff = ((statement, yes, no) => statement ? yes : no);
return {
flip,
id: grammarModel.id,
x: iff(flip, grammarModel.yi, grammarModel.xi),
y: iff(flip, grammarModel.xi, grammarModel.yi),
x0: iff(flip, grammarModel.y0, grammarModel.xi),
y0: iff(flip, grammarModel.xi, grammarModel.y0),
size: grammarModel.size,
group: grammarModel.group,
order: grammarModel.order,
label: grammarModel.label,
color: (d) => grammarModel.scaleColor.toColor(grammarModel.color(d)),
class: (d) => grammarModel.scaleColor.toClass(grammarModel.color(d)),
model: grammarModel,
toFibers: () => {
const data = grammarModel.data();
const groups = utils.groupBy(data, grammarModel.group);
return (Object
.keys(groups)
.sort((a, b) => grammarModel.order(a) - grammarModel.order(b))
.reduce((memo, k) => memo.concat([groups[k]]), []));
}
};
}
drawFrames() {
var self = this;
var options = this.config.options;
var round = ((x, decimals) => {
var kRound = Math.pow(10, decimals);
return (Math.round(kRound * x) / kRound);
});
var size = ((d) => round(self.screenModel.size(d) / 2, 4));
var createUpdateFunc = d3_animationInterceptor;
var drawPart = function (that: d3Selection, id: string, props) {
var speed = self.config.guide.animationSpeed;
var part = that
.selectAll(`.${id}`)
.data((row) => [row], self.screenModel.id);
part.exit()
.call(createUpdateFunc(speed, null, {width: 0}, (node) => d3.select(node).remove()));
part.call(createUpdateFunc(speed, null, props));
part.enter()
.append('rect')
.style('stroke-width', 0)
.call(createUpdateFunc(speed, {width: 0}, props));
};
var flip = this.config.flip;
var x = flip ? 'y' : 'x';
var y = flip ? 'x' : 'y';
var y0 = flip ? 'x0' : 'y0';
var w = flip ? 'height' : 'width';
var h = flip ? 'width' : 'height';
var drawElement = function (selection) {
drawPart(selection, 'lvl-top', {
[w]: ((d) => size(d)),
[h]: 1,
[x]: ((d) => self.screenModel[x](d) - size(d) / 2),
[y]: ((d) => self.screenModel[y](d)),
fill: ((d) => self.screenModel.color(d)),
class: ((d) => `lvl-top ${self.screenModel.class(d)}`)
});
drawPart(selection, 'lvl-btm', {
[w]: ((d) => size(d)),
[h]: 1,
[x]: ((d) => self.screenModel[x](d) - size(d) / 2),
[y]: ((d) => self.screenModel[y0](d)),
fill: ((d) => self.screenModel.color(d)),
class: ((d) => `lvl-btm ${self.screenModel.class(d)}`)
});
drawPart(selection, 'lvl-link', {
[w]: 0.5,
[h]: ((d) => Math.abs(self.screenModel[y](d) - self.screenModel[y0](d))),
[x]: ((d) => self.screenModel[x](d) - 0.25),
[y]: ((d) => Math.min(self.screenModel[y](d), self.screenModel[y0](d))),
fill: ((d) => self.screenModel.color(d)),
class: ((d) => `lvl-link ${self.screenModel.class(d)}`)
});
};
var updateGroups = function (selection) {
selection.attr('class', `frame-id-${self.config.uid}`)
.call(function (selection) {
var generic = selection
.selectAll('.generic')
.data((fiber) => fiber, self.screenModel.id);
generic
.exit()
.remove();
generic
.call(drawElement);
generic
.enter()
.append('g')
.attr('class', 'generic')
.call(drawElement);
});
};
var groups = utils.groupBy(this.data(), self.screenModel.group);
var fibers = Object
.keys(groups)
.sort((a, b) => self.screenModel.order(a) - self.screenModel.order(b))
.reduce((memo, k) => memo.concat([groups[k]]), []);
var frameGroups = options
.container
.selectAll(`.frame-id-${self.config.uid}`)
.data(fibers);
frameGroups
.exit()
.remove();
frameGroups
.call(updateGroups);
frameGroups
.enter()
.append('g')
.call(updateGroups);
}
}