Commit fee16c6a authored by Vasily Belolapotkov's avatar Vasily Belolapotkov

refactor: decoupled preview plots and chart plots

parent 406b445e
import { IAttributesMap, makeSvgRootElement } from '../utils/ui';
import { ISize, ChartComponentFactory } from './types';
import { makeYAxis } from './y-axis';
import { makeXAxis } from './x-axis';
import { makePlotsInspector } from './plots-inspector';
import { IPlots, makePlots } from './plots';
export const makeChartPlots: ChartComponentFactory<ISize> = function(
chartStateController,
) {
const stateController = chartStateController;
const xAxisHeight = 30;
let width = 0;
let height = 0;
let svg: SVGElement;
let htmlContainer: HTMLElement;
let plots: IPlots;
return Object.freeze({
render,
});
function render(container: HTMLElement, size: ISize) {
initProps(container, size);
renderPlotContainer();
renderAxes();
renderPlots();
renderInspector();
attachEventHandlers();
}
function initProps(container: HTMLElement, size: ISize) {
htmlContainer = container;
width = size.width;
height = size.height;
}
function renderPlotContainer() {
svg = makeSvgRootElement({
classList: ['plots-container'],
attributes: getAttributes(),
});
htmlContainer.appendChild(svg);
}
function renderAxes() {
const plotSize = getPlotsSize();
const yAxis = makeYAxis(stateController);
yAxis.render(svg, { plotSize });
const xAxis = makeXAxis(stateController);
const axisSize = { width, height: xAxisHeight };
xAxis.render(svg, { plotSize, axisSize });
}
function renderPlots() {
const chartState = stateController.getState();
const plotSize = getPlotsSize();
plots = makePlots();
plots.render(svg, {
size: plotSize,
xTotalRange: chartState.xTotalRange,
yTotalRange: chartState.yTotalRange,
xScale: chartState.xScale,
xOffset: chartState.xOffset,
yScale: chartState.yScale,
yOffset: chartState.yOffset,
});
plots.renderDataSets(chartState.xAxis, chartState.dataSets);
}
function renderInspector() {
const plotsInspector = makePlotsInspector(stateController);
const plotSize = getPlotsSize();
plotsInspector.render(svg, { plotSize, tooltipContainer: htmlContainer });
}
function getAttributes(): IAttributesMap {
const plotSize = getPlotsSize();
return {
width: `${width}`,
height: `${height}`,
preserveAspectRatio: 'none',
viewBox: `0 ${-plotSize.height} ${width} ${height}`,
};
}
function getPlotsSize(): ISize {
return {
width,
height: height - xAxisHeight,
};
}
function attachEventHandlers() {
stateController.addListener(
'dataSetVisibilityChanged',
handleDataSetVisibilityChanged,
);
stateController.addListener('xViewRangeChanged', handleXViewChanged);
stateController.addListener('yViewRangeChanged', handleYViewRangeChanged);
}
function handleDataSetVisibilityChanged() {
const { dataSets, yScale, yOffset } = stateController.getState();
plots.updateDataSetsVisibility(dataSets);
plots.setYViewRange(yScale, yOffset);
}
function handleXViewChanged(): void {
const { xScale, xOffset } = stateController.getState();
plots.setXViewRange(xScale, xOffset);
}
function handleYViewRangeChanged() {
const { yScale, yOffset } = stateController.getState();
plots.setYViewRange(yScale, yOffset);
}
};
import { makeHtmlElement, toPx } from '../utils/ui';
import { ChartComponentFactory, ISize } from './types';
import { renderPlots } from './plot';
import { makePreviewPlots } from './preview-plots';
import { makePreviewWindow } from './preview-window';
export const makeChartPreview: ChartComponentFactory<ISize> = function(
chartStateController,
) {
const stateController = chartStateController;
let previewContainer: HTMLElement;
let previewWidth = 0;
let previewHeight = 0;
......@@ -18,17 +20,11 @@ export const makeChartPreview: ChartComponentFactory<ISize> = function(
previewWidth = props.width;
previewHeight = props.height;
const previewContainer = makePreviewAreaContainer();
previewContainer = makePreviewAreaContainer();
container.appendChild(previewContainer);
const plotSize = {
width: previewWidth,
height: previewHeight,
};
const zoom = { x: false, y: false };
renderPlots(previewContainer, stateController, { plotSize, zoom });
renderPreviewWindow(previewContainer);
renderPlots();
renderPreviewWindow();
}
function makePreviewAreaContainer(): HTMLElement {
......@@ -38,7 +34,15 @@ export const makeChartPreview: ChartComponentFactory<ISize> = function(
});
}
function renderPreviewWindow(previewContainer: HTMLElement): void {
function renderPlots() {
const previewPlots = makePreviewPlots(stateController);
previewPlots.render(previewContainer, {
width: previewWidth,
height: previewHeight,
});
}
function renderPreviewWindow(): void {
const previewWindow = makePreviewWindow(stateController);
previewWindow.render(previewContainer, {
width: previewWidth,
......
......@@ -29,12 +29,14 @@ export function makeChartState(chartData: IChartData): IChartState {
moveInspector,
inspectorIsActive: false,
inspectorValues: getInspectorValues(),
xScale: 1,
xScale: 0.3,
yScale: 1,
xOffset: 0,
xOffset: 0.7,
yOffset: 0,
};
updateYViewRange();
return state;
function getXTotalRange(): IRange {
......
import { DOMContainerElement, ChartComponentFactory, ISize } from './types';
import { renderPlots } from './plot';
import { makeChartPlots } from './chart-plots';
import { makeHtmlElement } from '../utils/ui';
interface IPlotAreaProps {
......@@ -23,12 +23,8 @@ export const makePlotArea: ChartComponentFactory<IPlotAreaProps> = function(
const { plotWidth: width, plotHeight: height } = props;
const plotAreaContainer = makePlotAreaContainer();
plotSize = { width, height };
renderPlots(plotAreaContainer, stateController, {
plotSize,
zoom: { x: true, y: true },
axes: { x: true, y: true },
enableInspector: true,
});
const chartPlots = makeChartPlots(stateController);
chartPlots.render(plotAreaContainer, plotSize);
container.appendChild(plotAreaContainer);
attachInspectorHandlers(plotAreaContainer);
......
import {
applyElementConfig,
IAttributesMap,
makeLinearAnimation,
makeSvgElement,
makeSvgRootElement,
} from '../utils/ui';
import {
IChartStateController,
DOMContainerElement,
IAxesConfig,
IChartComponent,
ICoordinates,
IDataSet,
IRange,
ISize,
IZoomConfig,
Timestamp,
} from './types';
import { makeYAxis } from './y-axis';
import { makeXAxis } from './x-axis';
import { makePlotsInspector } from './plots-inspector';
interface IPlotContainerAttributes extends IAttributesMap {
width: string;
height: string;
preserveAspectRatio: string;
viewBox: string;
}
interface IPlots extends IChartComponent<IPlotsProps> {
renderDataSets: (xAxis: Timestamp[], dataSets: IDataSet[]) => void;
}
import {
applyElementConfig,
IAttributesMap,
makeLinearAnimation,
makeSvgElement,
} from '../utils/ui';
interface IPlotsProps {
plotSize: ISize;
zoomConfig: IZoomConfig;
}
export interface IPlotContainer extends IChartComponent<ISize> {
renderPlots: () => void;
renderAxes: () => void;
renderInspector: () => void;
}
export interface IPlotsConfig {
plotSize: ISize;
zoom?: IZoomConfig;
axes?: IAxesConfig;
enableInspector?: boolean;
size: ISize;
xScale: number;
yScale: number;
xOffset: number;
yOffset: number;
xTotalRange: IRange;
yTotalRange: IRange;
}
export function renderPlots(
htmlContainer: HTMLElement,
chartStateController: IChartStateController,
config: IPlotsConfig,
): IPlotContainer {
// TODO: make plots renderer composable from different components
const plotContainer = makePlotContainer(chartStateController, config);
const { plotSize } = config;
plotContainer.render(htmlContainer, plotSize);
plotContainer.renderAxes();
plotContainer.renderPlots();
if (config.enableInspector) {
plotContainer.renderInspector();
}
return plotContainer;
}
function makePlotContainer(
chartStateController: IChartStateController,
config: IPlotsConfig,
): IPlotContainer {
const { zoom: zoomConfig, axes: axesConfig } = config;
const stateController = chartStateController;
const plotZoomConfig = zoomConfig || { x: false, y: false };
const plotAxesConfig = axesConfig || { x: false, y: false };
const xAxisHeight = 30;
let width = 0;
let height = 0;
let svg: SVGElement;
let htmlContainer: HTMLElement;
return Object.freeze({
render,
renderAxes,
renderPlots,
renderInspector,
});
function render(container: HTMLElement, plotSize: ISize): void {
htmlContainer = container;
width = plotSize.width;
height = plotSize.height;
svg = makeSvgRootElement({
classList: ['plots-container'],
attributes: getAttributes(),
});
container.appendChild(svg);
}
function renderAxes() {
if (!plotAxesConfig.x && !plotAxesConfig.y) {
return;
}
const plotSize = getPlotsSize();
if (plotAxesConfig.y) {
const yAxis = makeYAxis(stateController);
yAxis.render(svg, { plotSize });
}
if (plotAxesConfig.x) {
const xAxis = makeXAxis(stateController);
const axisSize = {
width,
height: xAxisHeight,
};
xAxis.render(svg, { plotSize, axisSize });
}
}
function renderPlots() {
const { xAxis, dataSets } = stateController.getState();
const plots = makePlots(stateController);
const plotSize = getPlotsSize();
plots.render(svg, { plotSize, zoomConfig: plotZoomConfig });
plots.renderDataSets(xAxis, dataSets);
}
function renderInspector() {
const plotsInspector = makePlotsInspector(stateController);
const plotSize = getPlotsSize();
plotsInspector.render(svg, { plotSize, tooltipContainer: htmlContainer });
}
function getAttributes(): IPlotContainerAttributes {
const plotSize = getPlotsSize();
return {
width: `${width}`,
height: `${height}`,
preserveAspectRatio: 'none',
viewBox: `0 ${-plotSize.height} ${width} ${height}`,
};
}
function getPlotsSize(): ISize {
const plotsHeight = plotAxesConfig.x ? height - xAxisHeight : height;
return {
width,
height: plotsHeight,
};
}
export interface IPlots extends IChartComponent<IPlotsProps> {
renderDataSets: (xAxis: Timestamp[], dataSets: IDataSet[]) => void;
setXViewRange: (xScale: number, xOffset: number) => void;
setYViewRange: (yScale: number, yOffset: number) => void;
updateDataSetsVisibility: (dataSets: IDataSet[]) => void;
}
function makePlots(chartStateController: IChartStateController): IPlots {
const stateController = chartStateController;
export function makePlots(): IPlots {
let svgPlots: SVGElement;
let width = 0;
let height = 0;
......@@ -179,15 +47,21 @@ function makePlots(chartStateController: IChartStateController): IPlots {
return Object.freeze({
render,
renderDataSets,
setXViewRange,
setYViewRange,
updateDataSetsVisibility,
});
function render(container: DOMContainerElement, props: IPlotsProps) {
const { plotSize, zoomConfig } = props;
const chartState = stateController.getState();
width = plotSize.width;
height = plotSize.height;
xTotalRange = chartState.xTotalRange;
yTotalRange = chartState.yTotalRange;
const { size } = props;
width = size.width;
height = size.height;
xScale = props.xScale;
yScale = props.yScale;
xOffset = props.xOffset;
yOffset = props.yOffset;
xTotalRange = props.xTotalRange;
yTotalRange = props.yTotalRange;
xCoordsScale = width / xTotalRange.distance;
yCoordsScale = height / yTotalRange.distance;
......@@ -196,18 +70,6 @@ function makePlots(chartStateController: IChartStateController): IPlots {
});
container.appendChild(svgPlots);
stateController.addListener(
'dataSetVisibilityChanged',
handleDataSetVisibilityChanged,
);
if (zoomConfig.x) {
stateController.addListener('xViewRangeChanged', handleXViewChanged);
}
if (zoomConfig.y) {
stateController.addListener('yViewRangeChanged', handleYViewRangeChanged);
}
}
function renderDataSets(xAxis: Timestamp[], dataSets: IDataSet[]) {
......@@ -219,18 +81,6 @@ function makePlots(chartStateController: IChartStateController): IPlots {
renderPlot(points, dataSet);
}
function getPlotsAttributes(): IAttributesMap {
const kX = 1 / xScale;
const kY = 1 / yScale;
const dY = -yOffset;
const dX = -xOffset * width;
return {
preserveAspectRatio: 'none',
transform: `scale(${kX}, ${kY}) translate(${dX} ${dY})`,
};
}
function getPoints(xAxis: Timestamp[], dataSet: IDataSet): ICoordinates[] {
return xAxis.map((xValue, index) => {
const yValue = dataSet.values[index];
......@@ -290,11 +140,6 @@ function makePlots(chartStateController: IChartStateController): IPlots {
container.appendChild(polyline);
}
function handleXViewChanged(): void {
const { xScale, xOffset } = stateController.getState();
setXViewRange(xScale, xOffset);
}
function setXViewRange(scale: number, offset: number): void {
xScale = scale;
xOffset = offset;
......@@ -302,13 +147,29 @@ function makePlots(chartStateController: IChartStateController): IPlots {
applyElementConfig(svgPlots, { attributes });
}
function handleDataSetVisibilityChanged() {
updateDataSetsVisibility();
handleYViewRangeChanged();
function setYViewRange(scale: number, offset: number): void {
animateYViewRangeChange(scale, offset);
}
function animateYViewRangeChange(targetScale: number, targetOffset: number) {
makeLinearAnimation(
[
{ start: yScale, end: targetScale },
{ start: yOffset, end: targetOffset },
],
200,
([nextScale, nextOffset]) => updateYViewRange(nextScale, nextOffset),
);
}
function updateDataSetsVisibility() {
const { dataSets } = stateController.getState();
function updateYViewRange(scale: number, offset: number) {
yScale = scale;
yOffset = offset;
const attributes = getPlotsAttributes();
applyElementConfig(svgPlots, { attributes });
}
function updateDataSetsVisibility(dataSets: IDataSet[]) {
dataSets.forEach(dataSet => {
const { id, isVisible } = dataSet;
const plot = svgPlots.querySelector(`.plot[data-setId="${id}"]`);
......@@ -324,26 +185,15 @@ function makePlots(chartStateController: IChartStateController): IPlots {
});
}
function handleYViewRangeChanged() {
const { yScale, yOffset } = stateController.getState();
animateYViewRangeChange(yScale, yOffset);
}
function animateYViewRangeChange(targetScale: number, targetOffset: number) {
makeLinearAnimation(
[
{ start: yScale, end: targetScale },
{ start: yOffset, end: targetOffset },
],
200,
([nextScale, nextOffset]) => setYViewRange(nextScale, nextOffset),
);
}
function getPlotsAttributes(): IAttributesMap {
const kX = 1 / xScale;
const kY = 1 / yScale;
const dY = -yOffset;
const dX = -xOffset * width;
function setYViewRange(scale: number, offset: number) {
yScale = scale;
yOffset = offset;
const attributes = getPlotsAttributes();
applyElementConfig(svgPlots, { attributes });
return {
preserveAspectRatio: 'none',
transform: `scale(${kX}, ${kY}) translate(${dX} ${dY})`,
};
}
}
import { ChartComponentFactory, ISize } from './types';
import { IAttributesMap, makeSvgRootElement } from '../utils/ui';
import { IPlots, makePlots } from './plots';
export const makePreviewPlots: ChartComponentFactory<ISize> = function(
chartStateController,
) {
const stateController = chartStateController;
let svg: SVGElement;
let plotSize: ISize;
let plots: IPlots;
return Object.freeze({
render,
});
function render(container: HTMLElement, size: ISize) {
plotSize = size;
renderPlotContainer(container);
renderPlots();
attachEventHandlers();
}
function renderPlotContainer(container: HTMLElement) {
svg = makeSvgRootElement({
classList: ['plot-container'],
attributes: getAttributes(),
});
container.appendChild(svg);
}
function renderPlots() {
const chartState = stateController.getState();
plots = makePlots();
plots.render(svg, {
xTotalRange: chartState.xTotalRange,
yTotalRange: chartState.yTotalRange,
size: plotSize,
xScale: 1,
xOffset: 0,
yScale: chartState.yScale,
yOffset: chartState.yOffset,
});
plots.renderDataSets(chartState.xAxis, chartState.dataSets);
}
function attachEventHandlers() {