@devexperts/dxcharts-lite
Version:
310 lines (309 loc) • 15 kB
JavaScript
/*
* Copyright (C) 2019 - 2025 Devexperts Solutions IE Limited
* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
* If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { DataSeriesYAxisLabelsProvider } from '../components/y_axis/price_labels/data-series-y-axis-labels.provider';
import { LabelsGroups } from '../components/y_axis/price_labels/y-axis-labels.model';
import { binarySearch, create2DArray, lastOf, slice2DArray } from '../utils/array.utils';
import { floorToDPR } from '../utils/device/device-pixel-ratio.utils';
import { MathUtils } from '../utils/math.utils';
import { merge } from '../utils/merge.utils';
import { cloneUnsafe } from '../utils/object.utils';
import { ChartBaseElement } from './chart-base-element';
import { DataSeriesView } from './data-series-view';
import { DEFAULT_DATA_SERIES_CONFIG, } from './data-series.config';
/**
* Properties are named in order to match VisualCandle interface
*/
export class VisualSeriesPoint {
constructor(centerUnit, close) {
this.centerUnit = centerUnit;
this.close = close;
}
/**
* returns y coordinate in pixels
*/
y(viewable) {
return floorToDPR(viewable.toY(this.close));
}
/**
* returns x coordinate in pixels
*/
x(viewable) {
return viewable.toX(this.centerUnit);
}
clone() {
return new VisualSeriesPoint(this.centerUnit, this.close);
}
}
/**
* DataSeriesModel represents single time series chart.
* Usually data source is presented as a one-dimension array, but here it can be presented as two-dimension array
* If the data is presented as two-dim array when every data array will be drawn as a separate time-series
* For example, linear chart type will be drawn with gaps on the chart
*/
export class DataSeriesModel extends ChartBaseElement {
get dataPoints2D() {
return this._dataPoints;
}
get dataPoints() {
return this._dataPointsFlat;
}
set dataPoints(points) {
// createTwoDimArray here and below is needed to support two-dimension arrays
this._dataPoints = create2DArray(points);
this._dataPointsFlat = this._dataPoints.flat();
this.visualPoints = this._toVisualPoints(this._dataPoints);
}
get visualPoints() {
return this._visualPointsFlat;
}
get visualPoints2D() {
return this._visualPoints;
}
set visualPoints(points) {
this._visualPoints = create2DArray(points);
this._visualPointsFlat = this._visualPoints.flat();
}
constructor(extentComponent, id, htId, parentId, _config) {
var _a;
super();
this.extentComponent = extentComponent;
this.id = id;
this.htId = htId;
this.parentId = parentId;
this.name = '';
this.highlighted = false;
this._dataPoints = [];
this.pricePrecisions = [2];
/**
* Should be used for paint tools like rectangular drawing or diff cloud
*/
this.linkedDataSeriesModels = [];
this._dataPointsFlat = [];
this._visualPoints = [];
this._visualPointsFlat = [];
// start/end data index in viewport
this.dataIdxStart = 0;
this.dataIdxEnd = 0;
/**
* Returns the paint configuration object for a specific series part, or the default paint configuration object
* if the specified series part is not defined in the current DataSeriesView configuration.
*
* @param {number} seriesPart - The index of the series part to get the paint configuration for.
* @returns {DataSeriesPaintConfig} The paint configuration object for the specified series part, or the default paint configuration object.
*/
this.getPaintConfig = (seriesPart) => {
var _a;
return (_a = this.config.paintConfig[seriesPart]) !== null && _a !== void 0 ? _a : this.config.paintConfig[0];
};
/**
* Returns the close value of the visual point at the given index, or 1 if the visual point is not defined.
* The index defaults to the data index start of the DataSeriesView.
*
* @param {number} [idx=this.dataIdxStart] - The index of the visual point to retrieve the close value for.
* @returns {Unit} The close value of the visual point at the given index, or 1 if the visual point is not defined.
*/
this.getBaseline = (idx = this.dataIdxStart) => { var _a, _b, _c; return ((_a = this.visualPoints[idx]) === null || _a === void 0 ? void 0 : _a.close) && ((_b = this.visualPoints[idx]) === null || _b === void 0 ? void 0 : _b.close) >= 0 ? (_c = this.visualPoints[idx]) === null || _c === void 0 ? void 0 : _c.close : 1; };
/**
* Returns the string representation of the close value of the given visual point.
*
* @param {VisualSeriesPoint} point - The visual point to get the string representation of the close value for.
* @returns {string} The string representation of the close value of the given visual point.
*/
this.getTextForPoint = (point) => `${point.close}`;
/**
* Returns last visible on the screen data series value
* @param seriesIndex
*/
this.getLastVisualSeriesPoint = () => {
const points = this.visualPoints;
const endIdx = binarySearch(points, this.scale.xEnd, i => i.centerUnit).index;
return points[endIdx];
};
/**
* Return last overall data series value (even if not visible)
* @param seriesIndex
*/
this.getLastDataSeriesPoint = () => {
const points = this.visualPoints;
return lastOf(points);
};
const config = _config !== null && _config !== void 0 ? _config : cloneUnsafe(DEFAULT_DATA_SERIES_CONFIG);
this.config = merge(config, DEFAULT_DATA_SERIES_CONFIG);
this.scale = extentComponent.scale;
this.view = new DataSeriesView(this, this.scale, () => this.extentComponent.yAxis.getAxisType(), this.getBaseline);
this.yAxisLabelProvider = new DataSeriesYAxisLabelsProvider(this, this.config, extentComponent.getYAxisBounds, (_a = extentComponent.yAxis) === null || _a === void 0 ? void 0 : _a.state);
this.highLowProvider = createDataSeriesModelHighLowProvider(this);
extentComponent.addDataSeries(this);
this.activate();
}
doActivate() {
this.addRxSubscription(this.scale.xChanged.subscribe(() => this.recalculateDataViewportIndexes()));
this.addRxSubscription(this.scale.scaleInversedSubject.subscribe(() => {
this.recalculateVisualPoints();
this.extentComponent.dynamicObjectsCanvasModel.fireDraw();
}));
}
/**
* Sets the data points and recalculates internal state
* @param {DataSeriesPoint[][] | DataSeriesPoint[]} points - The data points to set for the model. Can be an array of arrays or a single array.
* @returns {void}
*/
setDataPoints(points) {
this.dataPoints = points;
this.extentComponent.paneComponent.updateView();
}
_toVisualPoints(data) {
return data.map(d => this.toVisualPoints(d));
}
/**
* Moves the DataSeriesModel to the given extent.
* @param extent
*/
moveToExtent(extent) {
var _a;
const prevExtent = Object.assign({}, this.extentComponent);
this.extentComponent.removeDataSeries(this);
this.extentComponent = extent;
this.scale = extent.scale;
this.view = new DataSeriesView(this, this.scale, () => this.extentComponent.yAxis.getAxisType(), this.getBaseline);
this.yAxisLabelProvider.yAxisBoundsProvider = extent.getYAxisBounds;
this.yAxisLabelProvider.axisState = (_a = extent.yAxis) === null || _a === void 0 ? void 0 : _a.state;
// move data series labels
const prevExtentMainLabels = prevExtent.yAxis.model.fancyLabelsModel.labelsProviders[LabelsGroups.MAIN];
const dataSeriesLabelsId = prevExtentMainLabels && Object.keys(prevExtentMainLabels).find(p => this.parentId && p === this.parentId);
const currentMainLabels = this.extentComponent.yAxis.model.fancyLabelsModel.labelsProviders[LabelsGroups.MAIN];
if (dataSeriesLabelsId) {
const labelsProvider = prevExtentMainLabels[dataSeriesLabelsId];
labelsProvider.yAxisBoundsProvider = extent.getYAxisBounds;
// main group is not created yet (new extent without labels) or main group exists but no data series labels so far
if (!currentMainLabels || (currentMainLabels && !currentMainLabels[dataSeriesLabelsId])) {
// create new data series labels group on the new extent
this.extentComponent.yAxis.model.fancyLabelsModel.registerYAxisLabelsProvider(LabelsGroups.MAIN, labelsProvider, dataSeriesLabelsId);
// remove labels from previous extent
prevExtent.yAxis.model.fancyLabelsModel.unregisterYAxisLabelsProvider(LabelsGroups.MAIN, dataSeriesLabelsId);
}
}
// shut down old subscriptions
this.deactivate();
// and apply new ones (with updated scaleModel)
this.activate();
extent.addDataSeries(this);
}
/**
* Transforms the given array of data points of type D into an array of visual points of type V.
* Each visual point object contains a centerUnit property with the index of the point in the input array,
* and a close property with the close value of the point.
*
* @param {D[]} data - The array of data points to transform into visual points.
* @returns {V[]} An array of visual points, each with a centerUnit and close property.
*/
toVisualPoints(data) {
// @ts-ignore
return data.map((point, idx) => ({ centerUnit: idx, close: point.close }));
}
setType(type) {
this.config.type = type;
this.extentComponent.dynamicObjectsCanvasModel.fireDraw();
}
/**
* Recalculates the visual points of the DataSeriesView based on the current data points.
* The visual points are stored in the visualPoints property of the DataSeriesView.
* Should be called 1 time when data are set / updated to chart.
*/
recalculateVisualPoints() {
this.visualPoints = this._toVisualPoints(this.dataPoints2D);
}
/**
* Recalculates the indexes of the start and end points of the data viewport,
* based on the current xStart and xEnd values of the scale model, or on the given xStart and xEnd parameters.
*
* @param {number} [xStart=this.scale.xStart] - The start value of the viewport on the x-axis. Defaults to the current xStart value of the scale model.
* @param {number} [xEnd=this.scale.xEnd] - The end value of the viewport on the x-axis. Defaults to the current xEnd value of the scale model.
*/
recalculateDataViewportIndexes(xStart = this.scale.xStart, xEnd = this.scale.xEnd) {
const { dataIdxStart, dataIdxEnd } = this.calculateDataViewportIndexes(xStart, xEnd);
this.dataIdxStart = dataIdxStart;
this.dataIdxEnd = dataIdxEnd;
}
/**
* Calculates and returns the indexes of the start and end points of the data viewport,
* based on the given start and end units on the x-axis.
*
* @param {Unit} xStart - The start value of the viewport on the x-axis.
* @param {Unit} xEnd - The end value of the viewport on the x-axis.
* @returns {DataSeriesViewportIndexes} An object containing the calculated start and end indexes of the data viewport.
*/
calculateDataViewportIndexes(xStart, xEnd) {
const dataIdxStart = binarySearch(this.visualPoints, xStart, (i) => i.centerUnit).index;
const dataIdxEnd = binarySearch(this.visualPoints, xEnd, (i) => i.centerUnit).index;
return {
dataIdxStart,
dataIdxEnd,
};
}
/**
* Formats the given numerical value using the default value formatter.
*
* @param {number} value - The numerical value to be formatted.
* @returns {string} The formatted value as a string.
*/
valueFormatter(value) {
return this.extentComponent.formatters.regular(value);
}
/**
* Returns a two-dimensional array of the visual points in the viewport of the DataSeriesView.
* The viewport range can be customized by providing start and end units on the x-axis.
* If start or end units are not provided, the current viewport range of the DataSeriesView is used.
*
* @param {number} [xStart] - The start value of the viewport range on the x-axis.
* @param {number} [xEnd] - The end value of the viewport range on the x-axis.
* @returns {V[][]} A two-dimensional array of the visual points in the viewport of the DataSeriesView.
*/
getSeriesInViewport(xStart, xEnd) {
let dataIdxStart = this.dataIdxStart;
let dataIdxEnd = this.dataIdxEnd;
if (xEnd !== undefined && xStart !== undefined) {
const res = this.calculateDataViewportIndexes(xStart, xEnd);
dataIdxStart = res.dataIdxStart;
dataIdxEnd = res.dataIdxEnd;
}
return slice2DArray(this.visualPoints2D, dataIdxStart, dataIdxEnd);
}
}
export const calculateDataSeriesHighLow = (visualCandles) => {
const result = {
high: Number.MIN_SAFE_INTEGER,
low: Number.MAX_SAFE_INTEGER,
highIdx: 0,
lowIdx: 0,
};
for (let i = 0; i < visualCandles.length; i++) {
const candle = visualCandles[i];
if (candle.close > result.high) {
result.high = candle.close;
result.highIdx = i;
}
if (candle.close < result.low) {
result.low = candle.close;
result.lowIdx = i;
}
}
return result;
};
const createDataSeriesModelHighLowProvider = (dataSeries) => ({
isHighLowActive: () => dataSeries.config.highLowActive,
calculateHighLow: state => {
const highLow = calculateDataSeriesHighLow(dataSeries.getSeriesInViewport(state === null || state === void 0 ? void 0 : state.xStart, state === null || state === void 0 ? void 0 : state.xEnd).flat());
return Object.assign(Object.assign({}, highLow), { high: dataSeries.view.toAxisUnits(highLow.high), low: dataSeries.view.toAxisUnits(highLow.low) });
},
});
export const defaultValueFormatter = (value) => {
const DEFAULT_PRECISION = 5;
const calcFraction = (val) => Math.ceil(Math.log(Math.abs(val)) * Math.LOG10E);
const calcPrecision = (val, maxPrecision = DEFAULT_PRECISION) => Math.max(0, maxPrecision - Math.max(0, calcFraction(val)));
const resPrecision = calcPrecision(value);
return MathUtils.makeDecimal(value, resPrecision);
};