src/shapes/ticks.js
import BaseShape from './base-shape';
/**
* Kind of Marker for entity oriented data. Useful to display a grid.
*/
export default class Ticks extends BaseShape {
_getClassName() {
return 'tick';
}
_getAccessorList() {
return { time: 0, focused: true, label: '' };
}
_getDefaults() {
return {
color: 'steelblue',
focusedOpacity: 0.8,
defaultOpacity: 0.3,
labelPosition: 'top',
shadeSegments: false,
unconstrained: false // indicates we should always update all
// ticks that exist, as the layer is
// handling tick generation dynamically
// (e.g. in axis layer)
};
}
render(renderingContext) {
if (this.$el) { return this.$el; }
this.$el = document.createElementNS(this.ns, 'g');
return this.$el;
}
update(renderingContext, data) {
const before = performance.now();
while (this.$el.firstChild) {
this.$el.removeChild(this.$el.firstChild);
}
const ticks = document.createElementNS(this.ns, 'path');
ticks.setAttributeNS(null, 'fill', 'none');
ticks.setAttributeNS(null, 'shape-rendering', 'crispEdges');
ticks.setAttributeNS(null, 'stroke', this.params.color);
ticks.setAttributeNS(null, 'stroke-width', 2);
ticks.style.opacity = this.params.opacity;
this.$el.appendChild(ticks);
const height = renderingContext.height;
let instructions = [];
const n = data.length;
if (n > 0) {
let nextX = renderingContext.timeToPixel(this.time(data[0]));
let oddTick = false;
let firstTick = true;
const segmentOpacity = (odd => { return odd ? 0.1 : 0.05; });
for (let i = 0; i < n; ++i) {
const datum = data[i];
const x = nextX;
const lastTick = (i + 1 >= n);
oddTick = !oddTick;
if (!lastTick) {
nextX = renderingContext.timeToPixel(this.time(data[i+1]));
}
if (!this.params.unconstrained) {
if (x < renderingContext.minX) {
continue;
}
if (!lastTick && (Math.floor(nextX) === Math.floor(x))) {
continue;
}
}
const opacity = this.focused(datum) ?
this.params.focusedOpacity : this.params.defaultOpacity;
instructions.push(`M${x},0L${x},${height}`);
if (this.params.shadeSegments) {
if (firstTick) {
if (x > renderingContext.minX) {
const segment = document.createElementNS(this.ns, 'rect');
segment.setAttributeNS(null, 'width', x - renderingContext.minX);
segment.setAttributeNS(null, 'height', height);
segment.setAttributeNS(null, 'fill', this.params.color);
segment.setAttributeNS(null, 'opacity', segmentOpacity(!oddTick));
segment.setAttributeNS(null, 'transform', `translate(${renderingContext.minX}, 0)`);
this.$el.appendChild(segment);
}
}
if (lastTick || nextX > x + 1) {
const segment = document.createElementNS(this.ns, 'rect');
segment.setAttributeNS(null, 'width', lastTick ? '100%' : (nextX - x));
segment.setAttributeNS(null, 'height', height);
segment.setAttributeNS(null, 'fill', this.params.color);
segment.setAttributeNS(null, 'opacity', segmentOpacity(oddTick));
segment.setAttributeNS(null, 'transform', `translate(${x}, 0)`);
this.$el.appendChild(segment);
}
}
const label = this.label(datum);
if (label && label !== "") {
// find the next label -- we only need enough space between
// this tick and that one, not necessarily between this and
// its (possibly unlabelled) neighbour
let nextLabelX = x - 1;
for (let j = i + 1; j < n; ++j) {
const nextLabel = this.label(data[j]);
if (nextLabel && nextLabel !== "") {
nextLabelX = renderingContext.timeToPixel(this.time(data[j]));
break;
}
}
const estWidth = label.length * 6;
const enoughSpaceForLabel = (nextLabelX < x || x + estWidth < nextLabelX);
if (enoughSpaceForLabel) {
const $label = document.createElementNS(this.ns, 'text');
$label.classList.add('label');
const $text = document.createTextNode(label);
$label.appendChild($text);
$label.setAttributeNS(null, 'transform', `matrix(1, 0, 0, -1, ${x + 2}, ${height + 2})`);
// firefox problem here
// $label.setAttributeNS(null, 'alignment-baseline', 'text-before-edge');
if (this.params.labelPosition === 'bottom') {
$label.setAttributeNS(null, 'y', height);
} else {
$label.setAttributeNS(null, 'y', '10');
}
$label.style.fontSize = '10px';
$label.style.lineHeight = '10px';
$label.style.fontFamily = 'monospace';
$label.style.color = '#676767';
$label.style.opacity = 0.9;
$label.style.mozUserSelect = 'none';
$label.style.webkitUserSelect = 'none';
$label.style.userSelect = 'none';
/*
const bg = document.createElementNS(this.ns, 'rect');
bg.setAttributeNS(null, 'width', '100%');
bg.setAttributeNS(null, 'height', '100%');
bg.setAttributeNS(null, 'fill', '#ffffff');
$label.appendChild(bg);
*/
this.$el.appendChild($label);
}
}
if (!this.params.unconstrained) {
if (nextX > renderingContext.maxX) {
break;
}
}
firstTick = false;
}
}
const d = instructions.join('');
ticks.setAttributeNS(null, 'd', d);
const after = performance.now();
console.log("ticks update time = " + Math.round(after - before) + "ms");
}
}