Commit 6bd4bf2a authored by Vasily Belolapotkov's avatar Vasily Belolapotkov

optimize plot x rendering

parent 180f8d2e
......@@ -6,11 +6,7 @@ import {
Timestamp,
} from './types';
import {
applyElementConfig,
makeSvgElement,
makeSvgRootElement,
} from '../utils/ui';
import { applyElementConfig, makeSvgElement } from '../utils/ui';
interface IXAxisProps {
plotSize: ISize;
......@@ -23,18 +19,13 @@ export function makeXAxis(
const stateController = chartStateController;
const dayRange = 24 * 60 * 60 * 1000;
const xTextLabelOffset = 25;
const xLabelWidth = 4 * xTextLabelOffset;
const xAxisYPosition = -5;
const xLabelWidth = 3 * xTextLabelOffset;
let xSvg: SVGElement;
let xSvgLabels: SVGElement;
let plotSize: ISize;
let axisSize: ISize;
let xScale: number;
let xOffset: number;
let xCoordsScale: number;
let xLabelsStep: number;
let xLabelsCount: number;
return Object.freeze({ render });
......@@ -55,134 +46,142 @@ export function makeXAxis(
}
function renderAxis(container: DOMContainerElement): void {
xSvg = makeSvgContainer();
xSvgLabels = makeLabelsContainer();
container.appendChild(xSvgLabels);
createLabels();
updateLabels();
xSvg.appendChild(xSvgLabels);
container.appendChild(xSvg);
}
function attachHandlers() {
stateController.addListener('xViewRangeChanged', updateLabels);
}
function makeSvgContainer(): SVGElement {
const { xScale, xOffset } = stateController.getState();
return makeSvgRootElement({
attributes: {
width: `${plotSize.width}`,
height: `${axisSize.height}`,
y: `${xAxisYPosition}`,
viewBox: getXAxisViewBox(xScale, xOffset),
},
});
}
function makeLabelsContainer(): SVGElement {
return makeSvgElement('g', {
classList: ['axis-labels', 'x'],
attributes: {
transform: `translate(0 ${axisSize.height})`,
},
});
}
function createLabels(): void {
const { xAxis } = stateController.getState();
const { xScale, xOffset } = stateController.getState();
const xLabelsStep = getXLabelsStep(xScale);
const xLabels = getXLabels(xLabelsStep, xScale, xOffset);
xLabels.forEach(timestamp => {
const svgLabel = makeSvgLabel(timestamp);
xSvgLabels.appendChild(svgLabel);
});
}
for (let i = xAxis.length - 1; i >= 0; i = i - 1) {
xSvgLabels.appendChild(makeSvgLabel(xAxis[i]));
function getXLabels(
xLabelsStep: number,
xScale: number,
xOffset: number,
): Timestamp[] {
const { xTotalRange } = stateController.getState();
const xViewStart = xTotalRange.start + xOffset * xTotalRange.distance;
const xViewEnd = xViewStart + xScale * xTotalRange.distance;
const labels = [];
const labelsToSkip = Math.floor((xTotalRange.end - xViewEnd) / xLabelsStep);
const overhead = 2;
const xLabelsEnd =
xTotalRange.end - (labelsToSkip - overhead) * xLabelsStep;
const xLabelsStart = xViewStart - overhead * xLabelsStep;
let value = xLabelsEnd;
while (value >= xLabelsStart) {
labels.push(value);
value = value - xLabelsStep;
}
return labels;
}
function updateLabels(): void {
updateViewBox();
updateLabelsElements();
}
function updateViewBox() {
function updateLabelsElements() {
const {
xScale: newXScale,
xOffset: newXOffset,
} = stateController.getState();
const viewBox = getXAxisViewBox(newXScale, newXOffset);
applyElementConfig(xSvg, { attributes: { viewBox } });
xOffset = newXOffset;
}
function updateLabelsElements() {
const { xScale: newXScale } = stateController.getState();
const newXLabelsStep = getXLabelsStep(newXScale);
const xScaleHasChanged = xScale !== newXScale;
const xLabelsStepHasChanged = newXLabelsStep !== xLabelsStep;
const newLabels = getXLabels(newXLabelsStep, newXScale, newXOffset);
if (!xScaleHasChanged && !xLabelsStepHasChanged) {
return;
}
createMissingLabels(newLabels);
updateLabelsAttributes(newXLabelsStep, newLabels);
}
updateLabelsAttributes(
newXLabelsStep,
xScaleHasChanged,
xLabelsStepHasChanged,
);
xScale = newXScale;
xLabelsStep = newXLabelsStep;
function createMissingLabels(labels: Timestamp[]) {
labels.forEach(timestamp => {
const el = xSvgLabels.querySelector(`[data-value="${timestamp}"]`);
if (!el) {
const label = makeSvgLabel(timestamp);
xSvgLabels.appendChild(label);
}
});
}
function updateLabelsAttributes(
newXLabelsStep: number,
xScaleHasChanged: boolean,
xLabelsStepHasChanged: boolean,
newLabels: Timestamp[],
) {
const { xTotalRange, xScale: newXScale } = stateController.getState();
const {
xTotalRange,
xScale: newXScale,
xOffset,
} = stateController.getState();
const svgElementsCount = xSvgLabels.children.length;
let nextVisibleLabelValue = xTotalRange.end;
const xOffsetValue = xTotalRange.start + xOffset * xTotalRange.distance;
const elementsToRemove: SVGElement[] = [];
for (let i = 0; i < svgElementsCount - 1; i = i + 1) {
const svgLabel = <SVGElement>xSvgLabels.children[i];
const timestamp = Number(svgLabel.getAttribute('data-value'));
let styleUpdates;
const shouldRemoveElement = newLabels.indexOf(timestamp) < 0;
if (xLabelsStepHasChanged) {
const shouldBeVisible = timestamp === nextVisibleLabelValue;
if (shouldBeVisible) {
nextVisibleLabelValue = nextVisibleLabelValue - newXLabelsStep;
}
styleUpdates = { opacity: shouldBeVisible ? '1' : '0' };
if (shouldRemoveElement) {
elementsToRemove.push(svgLabel);
continue;
}
const styleUpdates = { opacity: '1' };
let attributesUpdates;
if (xScaleHasChanged) {
const timestampOffset = Number(
svgLabel.getAttribute('data-value-offset'),
);
let x = timestampOffset / newXScale;
if (i === 0) {
x = x - xTextLabelOffset;
}
attributesUpdates = { x: `${x}` };
let x =
((timestamp - xOffsetValue) * xCoordsScale) / newXScale -
xTextLabelOffset;
if (timestamp === xTotalRange.end) {
x = x - xTextLabelOffset;
}
attributesUpdates = { x: `${x}` };
applyElementConfig(svgLabel, {
style: styleUpdates,
attributes: attributesUpdates,
});
}
if (elementsToRemove.length > 0) {
elementsToRemove.forEach(element => xSvgLabels.removeChild(element));
}
}
function makeSvgLabel(timestamp: Timestamp): SVGElement {
const { xTotalRange } = stateController.getState();
const xSvgLabel = makeSvgElement('text', {
classList: ['value'],
attributes: {
x: '0',
y: '0',
'data-value': `${timestamp}`,
'data-value-offset': `${(timestamp - xTotalRange.start) *
xCoordsScale}`,
},
style: { opacity: '0' },
});
......@@ -190,11 +189,6 @@ export function makeXAxis(
return xSvgLabel;
}
function getXAxisViewBox(xScale: number, xOffset: number) {
const dX = xTextLabelOffset + (xOffset * plotSize.width) / xScale;
return `${dX} ${-axisSize.height} ${plotSize.width} ${axisSize.height}`;
}
function getXLabelText(xValue: Timestamp): string {
const date = new Date(xValue);
return date.toLocaleString('en-US', { month: 'short', day: '2-digit' });
......
......@@ -53,7 +53,7 @@
}
&.x .value {
transition: opacity 0.5s ease;
transition: opacity 0.3s ease;
}
}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment