UNPKG

@devexperts/dxcharts-lite

Version:
310 lines (309 loc) 15 kB
/* * 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); };