diff --git a/package.json b/package.json index 85d341c7..73c97995 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "@amcharts/amcharts5", - "version": "5.6.2", + "version": "5.7.0", "author": "amCharts (https://www.amcharts.com/)", "description": "amCharts 5", "homepage": "https://www.amcharts.com/", diff --git a/packages/geodata/CHANGELOG.md b/packages/geodata/CHANGELOG.md index 472a6b57..80aa9b2a 100644 --- a/packages/geodata/CHANGELOG.md +++ b/packages/geodata/CHANGELOG.md @@ -7,8 +7,12 @@ adhere to [Semantic Versioning](http://semver.org/spec/v2.0.0.html) rules. ## [UNRELEASED] - ????-??-?? + +## [5.1.2] - 2023-12-07 + ### Added - New map of Kazakhstan: `kazahkstan2023*`. +- New USA county maps to reflect changes in Alaska: `region/usa/ak2023*` and `region/usa/usaCounties2023*`. ### Changed - Updated Hormozgan province name in the maps of Iran. diff --git a/packages/geodata/package.json b/packages/geodata/package.json index db8f6116..b7b95ef6 100644 --- a/packages/geodata/package.json +++ b/packages/geodata/package.json @@ -1,6 +1,6 @@ { "name": "@amcharts/amcharts5-geodata", - "version": "5.1.1", + "version": "5.1.2", "author": "amCharts (https://www.amcharts.com/)", "description": "amCharts 5 Geo Data", "homepage": "https://www.amcharts.com/", diff --git a/packages/shared/CHANGELOG.md b/packages/shared/CHANGELOG.md index 850f6b3c..3780bb08 100644 --- a/packages/shared/CHANGELOG.md +++ b/packages/shared/CHANGELOG.md @@ -5,6 +5,33 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). Please note, that this project, while following numbering syntax, it DOES NOT adhere to [Semantic Versioning](http://semver.org/spec/v2.0.0.html) rules. +## [5.7.0] - 2023-12-17 + +### Added +- New `Serializer` setting: `fullSettings`. Will ignore depth settings for keys listed in `fullSettings`. +- New `StockAxis` toolbar control: `DataSaveControl`. Allows to automatically or manually save all drawings and indicators to browser's local storage, as well as restore them across sessions. [More info](https://www.amcharts.com/docs/v5/charts/stock/toolbar/data-save-control/). +- Added additional item in `SettingsControl`: Auto-save drawings and indicators. +- New setting in `SettingsControl`: `autoSave`. Enables user to toggle auto-saving of indicators/drawings via Settings dropdown. +- New setting in `DropdownList` and `DropdownListControl`: `exclude`. Can be set to an array of item IDs that should not appear in the list. Can be used to disable default list items. +- New property in `StockChart`: `spriteResizer`. Holdes an instance of `SpriteResizer` which is used among drawing tools. +- `zoomable` setting added to Axis. If set to false, calling axis.zoom() won't do anything, meaning that the axis won't be zoomed with scrollbars, wheel, cursor etc. + +## Changed +- `StockChart` will only sync zoom between X axes that are of the same type as the main axis. +- All drawing tools that allow selecting and resizing them (Label, Callout, Icons) will now use shared instance of `SpriteResizer`. +- `IndictorControl` is now searchable by default (`searchable: true`). + +### Fixed +- `PeriodSelector`'s buttons were not being reset when chart was zoomed manually. +- Dropdowns in `StockChart`'s control were not being positioned correctly in all cases on a page with RTL direction enabled. +- Drawings loaded from serialized data were not being deleted with eraser or clear tools. +- Elinated multiple calls to preparing data of `StockChart` indicators. +- On `StockChart`, indicator's panel X-axis was out of sync with the main panel's X-axis if the latter's scale was longer to some other series on the main panel. +- Italic toggle was being ignored in `StockChart` label drawing tools. +- `GaplessDateAxis` was not always including the first date when using `axis.zoomToDates(startDate, endDate)` method. +- When swiching to percent scale `StovkChart's main panel was showing wrong Y-axis numbers until first zoom. + + ## [5.6.2] - 2023-11-30 ### Added diff --git a/src/.internal/charts/stock/StockChart.ts b/src/.internal/charts/stock/StockChart.ts index a1740ae3..dee37041 100644 --- a/src/.internal/charts/stock/StockChart.ts +++ b/src/.internal/charts/stock/StockChart.ts @@ -9,8 +9,10 @@ import type { XYSeries, IXYSeriesDataItem, IXYSeriesSettings } from "../xy/serie import type { DataItem } from "../../core/render/Component"; import type { Indicator } from "./indicators/Indicator"; import type { DrawingSeries } from "./drawing/DrawingSeries"; +import type { StockControl } from "./toolbar/StockControl"; import { MultiDisposer } from "../../core/util/Disposer"; +import { SpriteResizer } from "../../core/render/SpriteResizer"; import { PanelControls } from "./PanelControls"; import { StockChartDefaultTheme } from "./StockChartDefaultTheme"; import { XYChartDefaultTheme } from "../xy/XYChartDefaultTheme"; @@ -25,6 +27,7 @@ import { registry } from "../../core/Registry"; import * as $array from "../../core/util/Array"; import * as $utils from "../../core/util/Utils"; import * as $object from "../../core/util/Object"; +import type { GaplessDateAxis } from "../xy/axes/GaplessDateAxis"; export interface IStockChartSettings extends IContainerSettings { @@ -203,6 +206,20 @@ export class StockChart extends Container { */ public readonly panelsContainer: Container = this.children.push(Container.new(this._root, { width: p100, height: p100, layout: this._root.verticalLayout, themeTags: ["chartscontainer"] })); + /** + * An array of all Stock Controls that are created for this chart. + * + * @since 5.7.0 + */ + public readonly controls: StockControl[] = []; + + /** + * An instance of [[SpriteResizer]] used for various drawing tools. + * + * @since 5.7.0 + */ + public spriteResizer = this.children.push(SpriteResizer.new(this._root, {})); + protected _afterNew() { this._settings.themeTags = $utils.mergeTags(this._settings.themeTags, ["stock"]); @@ -417,8 +434,9 @@ export class StockChart extends Container { if (this.isDirty("stockNegativeColor") || this.isDirty("stockPositiveColor") || this.isDirty("stockSeries")) { if (stockSeries && stockSeries.isType("BaseColumnSeries")) { - const stockNegativeColor = this.get("stockNegativeColor", this._root.interfaceColors.get("negative")); - const stockPositiveColor = this.get("stockPositiveColor", this._root.interfaceColors.get("positive")); + const ic = this._root.interfaceColors; + const stockNegativeColor = this.get("stockNegativeColor", ic.get("negative")); + const stockPositiveColor = this.get("stockPositiveColor", ic.get("positive")); let previous = stockSeries.dataItems[0]; if (stockPositiveColor && stockPositiveColor) { @@ -520,7 +538,6 @@ export class StockChart extends Container { const yAxis = stockSeries.get("yAxis") as ValueAxis; yAxis.set("logarithmic", false); - this._maybePrepAxisDefaults(); if (mainChart) { const seriesList: XYSeries[] = []; @@ -563,6 +580,10 @@ export class StockChart extends Container { } } } + + this.indicators.each((indicator)=>{ + indicator.markDataDirty(); + }) } /** @@ -698,7 +719,10 @@ export class StockChart extends Container { this.markDirtyIndicators(); + //indicator.markDataDirty(); // not good, shows zoomed out value axis indicator.prepareData(); + + this._syncExtremes(); } protected _removeIndicator(indicator: Indicator) { @@ -716,24 +740,25 @@ export class StockChart extends Container { const panelControls = panel.panelControls; const index = this.panelsContainer.children.indexOf(panel); const len = this.panels.length; + const visible = "visible" - panelControls.upButton.setPrivate("visible", false); - panelControls.downButton.setPrivate("visible", false); - panelControls.expandButton.setPrivate("visible", false); - panelControls.closeButton.setPrivate("visible", false); + panelControls.upButton.setPrivate(visible, false); + panelControls.downButton.setPrivate(visible, false); + panelControls.expandButton.setPrivate(visible, false); + panelControls.closeButton.setPrivate(visible, false); if (len > 1) { - panelControls.expandButton.setPrivate("visible", true); + panelControls.expandButton.setPrivate(visible, true); if (index != 0) { - panelControls.upButton.setPrivate("visible", true); + panelControls.upButton.setPrivate(visible, true); } if (index != len - 1) { - panelControls.downButton.setPrivate("visible", true); + panelControls.downButton.setPrivate(visible, true); } if (!stockSeries || stockSeries.chart != panel) { - panelControls.closeButton.setPrivate("visible", true); + panelControls.closeButton.setPrivate(visible, true); } } @@ -743,7 +768,6 @@ export class StockChart extends Container { }); } }) - } protected _processPanel(panel: StockPanel) { @@ -885,7 +909,7 @@ export class StockChart extends Container { this.panels.each((panel) => { panel.xAxes.each((xAxis) => { - if (xAxis != mainAxis) { + if (xAxis != mainAxis && xAxis.isType("DateAxis")) { let axisMin = xAxis.getPrivate("min" as any); let axisMax = xAxis.getPrivate("max" as any); @@ -895,6 +919,10 @@ export class StockChart extends Container { if (axisMax != max) { xAxis.set("max" as any, max); } + const type = "GaplessDateAxis"; + if (xAxis.isType>(type) && mainAxis.isType>(type)) { + xAxis._customDates = mainAxis._dates; + } } }) }) @@ -903,7 +931,7 @@ export class StockChart extends Container { protected _syncXAxes(axis: Axis) { $array.each(this._xAxes, (xAxis) => { - if (xAxis != axis) { + if (xAxis != axis && xAxis.isType("DateAxis")) { xAxis._skipSync = true; xAxis.set("start", axis.get("start")); xAxis.set("end", axis.get("end")); @@ -967,4 +995,23 @@ export class StockChart extends Container { return positiveColor; } + /** + * Returns a first [[StockControl]] of specific type. + * + * @since 5.7.0 + * @param type Control name + * @return Control + */ + public getControl(type: string): StockControl | undefined { + let found: StockControl | undefined; + $array.eachContinue(this.controls, (control: StockControl) => { + if (control.className == type) { + found = control; + return false; + } + return true; + }); + return found; + } + } diff --git a/src/.internal/charts/stock/StockChartDefaultTheme.ts b/src/.internal/charts/stock/StockChartDefaultTheme.ts index e19ae489..16d229a0 100644 --- a/src/.internal/charts/stock/StockChartDefaultTheme.ts +++ b/src/.internal/charts/stock/StockChartDefaultTheme.ts @@ -1,7 +1,7 @@ import { Theme } from "../../core/Theme"; import { p100, p50, p0, percent } from "../../core/util/Percent"; import { setColor } from "../../themes/DefaultTheme"; -import { color } from "../../core/util/Color"; +import { Color, color } from "../../core/util/Color"; import { ColorSet } from "../../core/util/ColorSet"; import { StockIcons } from "./toolbar/StockIcons"; @@ -851,6 +851,10 @@ export class StockChartDefaultTheme extends Theme { legendValueText: "[{volumeColor}; bold]{valueY.formatNumber('#.000a')}[/]", }) + r("ColumnSeries", ["volumeprofile"]).setAll({ + legendLabelText: "Volume Profile", + legendValueText: "[{downColor}; bold]{down.formatNumber('#.000a')}[/] [{upColor}; bold]{up.formatNumber('#.000a')}[/] [bold]{total.formatNumber('#.000a')}[/]" + }) r("LineSeries", ["vwap"]).setAll({ legendValueText: "[{seriesColor} bold]{valueY.formatNumber('#.00')}[/]", @@ -871,6 +875,15 @@ export class StockChartDefaultTheme extends Theme { // end of legend labels + r("RoundedRectangle", ["series", "column", "volumeprofile"]).setAll({ + fillOpacity: .3, + strokeWidth: 2, + strokeOpacity: 0 + }) + + r("RoundedRectangle", ["series", "column", "volumeprofile"]).states.create("hover", { + strokeOpacity: 1 + }) r("RoundedRectangle", ["macd", "difference"]).setAll({ fillOpacity: 0.5, @@ -1160,6 +1173,18 @@ export class StockChartDefaultTheme extends Theme { decreasingColor: ic.get("negative") }); + r("VolumeProfile").setAll({ + name: "Volume Profile", + shortName: "VP", + upColor: Color.fromHex(0xE3B30C), + downColor: Color.fromHex(0x2E78E3), + countType: "rows", + count: 24, + axisWidth: 40, + valueArea: 70, + valueAreaOpacity: 0.7 + }); + r("MACD").setAll({ name: "MACD", field: "close", @@ -1190,7 +1215,8 @@ export class StockChartDefaultTheme extends Theme { name: l.translateAny("Indicators"), scrollable: true, fixedLabel: true, - indicators: ["Accumulation Distribution", "Accumulative Swing Index", "Aroon", "Awesome Oscillator", "Bollinger Bands", "Chaikin Money Flow", "Chaikin Oscillator", "Commodity Channel Index", "Disparity Index", "MACD", "Median Price", "Momentum", "Moving Average", "Moving Average Deviation", "Moving Average Envelope", "On Balance Volume", "Relative Strength Index", "Standard Deviation", "Stochastic Momentum Index", "Stochastic Oscillator", "Trix", "Typical Price", "Volume", "VWAP", "Williams R", "ZigZag"] + searchable: true, + indicators: ["Accumulation Distribution", "Accumulative Swing Index", "Aroon", "Awesome Oscillator", "Bollinger Bands", "Chaikin Money Flow", "Chaikin Oscillator", "Commodity Channel Index", "Disparity Index", "MACD", "Median Price", "Momentum", "Moving Average", "Moving Average Deviation", "Moving Average Envelope", "On Balance Volume", "Relative Strength Index", "Standard Deviation", "Stochastic Momentum Index", "Stochastic Oscillator", "Trix", "Typical Price", "Volume", "Volume Profile", "VWAP", "Williams R", "ZigZag"] }); r("ComparisonControl").setAll({ @@ -1446,6 +1472,10 @@ export class StockChartDefaultTheme extends Theme { fixedLabel: true, //align: "right", items: [{ + id: "fills", + label: l.translateAny("X-axis fills"), + className: "am5stock-list-info am5stock-list-heading" + }, { form: "checkbox", id: "fills", label: l.translateAny("Fills") @@ -1468,6 +1498,41 @@ export class StockChartDefaultTheme extends Theme { form: "radio", value: "logarithmic", label: l.translateAny("Logarithmic") + }, { + id: "autosave", + label: l.translateAny("Drawings & Indicators"), + className: "am5stock-list-info am5stock-list-heading" + }, { + id: "autosave", + form: "checkbox", + label: l.translateAny("Auto-save") + },] + }); + + r("DataSaveControl").setAll({ + description: l.translateAny("Save drawings and indicators"), + togglable: true, + fixedLabel: true, + autoSave: false, + items: [{ + id: "autosave", + form: "checkbox", + label: l.translateAny("Auto-save drawings and indicators") + }, { + id: "autosave", + label: "" + }, { + id: "save", + label: l.translateAny("Save drawings & indicators"), + subLabel: l.translateAny("Saves drawings/indicators to browser local storage") + }, { + id: "restore", + label: l.translateAny("Restore saved data"), + subLabel: l.translateAny("Restores saved data from browser local storage") + }, { + id: "clear", + label: l.translateAny("Clear"), + subLabel: l.translateAny("Clears saved data from browser local storage") }] }); diff --git a/src/.internal/charts/stock/drawing/CalloutSeries.ts b/src/.internal/charts/stock/drawing/CalloutSeries.ts index e478e9a9..1b3f0643 100644 --- a/src/.internal/charts/stock/drawing/CalloutSeries.ts +++ b/src/.internal/charts/stock/drawing/CalloutSeries.ts @@ -42,47 +42,49 @@ export class CalloutSeries extends LabelSeries { const dataContext = dataItem.dataContext as any; const template = dataContext.settings; - const label = this.getPrivate("label"); - if (label) { - label.events.on("positionchanged", () => { - this._updatePointer(label); - }) - - label.events.on("click", () => { - const spriteResizer = this.spriteResizer; - if (spriteResizer.get("sprite") == label) { - spriteResizer.set("sprite", undefined); - } - else { - spriteResizer.set("sprite", label); - } - if (this._erasingEnabled) { - this._disposeIndex(dataContext.index); - } - }) - - label.on("scale", () => { - this._updatePointer(label); - }) - - label.on("rotation", () => { - this._updatePointer(label); - }) - - label.setAll({ draggable: true }); - - label.on("x", (x) => { - template.set("x", x); - }) - - label.on("y", (y) => { - template.set("y", y); - }) - - const defaultState = label.states.lookup("default")!; - setTimeout(() => { - label.animate({ key: "y", to: -label.height() / 2 - 10, from: 0, duration: defaultState.get("stateAnimationDuration", 500), easing: defaultState.get("stateAnimationEasing", $ease.out($ease.cubic)) }) - }, 50) + if (template) { + const label = this.getPrivate("label"); + if (label) { + label.events.on("positionchanged", () => { + this._updatePointer(label); + }) + + label.events.on("click", () => { + const spriteResizer = this.spriteResizer; + if (spriteResizer.get("sprite") == label) { + spriteResizer.set("sprite", undefined); + } + else { + spriteResizer.set("sprite", label); + } + if (this._erasingEnabled) { + this._disposeIndex(dataContext.index); + } + }) + + label.on("scale", () => { + this._updatePointer(label); + }) + + label.on("rotation", () => { + this._updatePointer(label); + }) + + label.setAll({ draggable: true }); + + label.on("x", (x) => { + template.set("x", x); + }) + + label.on("y", (y) => { + template.set("y", y); + }) + + const defaultState = label.states.lookup("default")!; + setTimeout(() => { + label.animate({ key: "y", to: -label.height() / 2 - 10, from: 0, duration: defaultState.get("stateAnimationDuration", 500), easing: defaultState.get("stateAnimationEasing", $ease.out($ease.cubic)) }) + }, 50) + } } } diff --git a/src/.internal/charts/stock/drawing/DrawingSeries.ts b/src/.internal/charts/stock/drawing/DrawingSeries.ts index ca2b079c..6ccab070 100644 --- a/src/.internal/charts/stock/drawing/DrawingSeries.ts +++ b/src/.internal/charts/stock/drawing/DrawingSeries.ts @@ -9,6 +9,7 @@ import type { Sprite } from "../../../core/render/Sprite"; import type { DataItem } from "../../../core/render/Component"; import type { XYSeries } from "../../xy/series/XYSeries"; import type { StockPanel } from "../StockPanel"; +import type { StockChart } from "../StockChart"; import { LineSeries, ILineSeriesSettings, ILineSeriesPrivate, ILineSeriesDataItem } from "../../xy/series/LineSeries"; import { Bullet } from "../../../core/render/Bullet"; @@ -894,4 +895,8 @@ export class DrawingSeries extends LineSeries { } return realValue; } + + protected _getStockChart(): StockChart { + return (this.get("series") as any).chart.getPrivate("stockChart"); + } } \ No newline at end of file diff --git a/src/.internal/charts/stock/drawing/IconSeries.ts b/src/.internal/charts/stock/drawing/IconSeries.ts index 9fbfee8d..0de89e4c 100644 --- a/src/.internal/charts/stock/drawing/IconSeries.ts +++ b/src/.internal/charts/stock/drawing/IconSeries.ts @@ -1,11 +1,11 @@ import type { Percent } from "../../../core/util/Percent"; import type { ISpritePointerEvent } from "../../../core/render/Sprite"; import type { DataItem } from "../../../core/render/Component"; +import type { SpriteResizer } from "../../../core/render/SpriteResizer"; import { PolylineSeries, IPolylineSeriesSettings, IPolylineSeriesPrivate, IPolylineSeriesDataItem } from "./PolylineSeries"; import { Bullet } from "../../../core/render/Bullet"; import { Graphics } from "../../../core/render/Graphics"; -import { SpriteResizer } from "../../../core/render/SpriteResizer"; import { Template } from "../../../core/util/Template"; import * as $array from "../../../core/util/Array"; @@ -56,13 +56,15 @@ export class IconSeries extends PolylineSeries { declare public _privateSettings: IIconSeriesPrivate; declare public _dataItemSettings: IIconSeriesDataItem; - public spriteResizer = this.children.push(SpriteResizer.new(this._root, {})); + public spriteResizer!: SpriteResizer; protected _tag = "icon"; protected _afterNew() { super._afterNew(); + this.spriteResizer = this._getStockChart().spriteResizer; + this.bullets.clear(); this.strokes.template.set("visible", false); @@ -71,45 +73,47 @@ export class IconSeries extends PolylineSeries { this.bullets.push((root, _series, dataItem) => { const dataContext = dataItem.dataContext as any; const template = dataContext.settings; - const sprite = Graphics.new(root, { - draggable: true, - themeTags: ["icon"] - }, template); - - this._addBulletInteraction(sprite); - - sprite.events.on("click", () => { - const spriteResizer = this.spriteResizer; - if (spriteResizer.get("sprite") == sprite) { - spriteResizer.set("sprite", undefined); - } - else { - spriteResizer.set("sprite", sprite); - } - }) - - sprite.events.on("pointerover", () => { - this._isHover = true; - }) - - sprite.events.on("pointerout", () => { - this._isHover = false; - }) - - this.spriteResizer.set("sprite", undefined); - - sprite.on("scale", (scale) => { - template.set("scale", scale) - }) - - sprite.on("rotation", (rotation) => { - template.set("rotation", rotation) - }) - - return Bullet.new(this._root, { - locationX: undefined, - sprite: sprite - }) + if (template) { + const sprite = Graphics.new(root, { + draggable: true, + themeTags: ["icon"] + }, template); + + this._addBulletInteraction(sprite); + + sprite.events.on("click", () => { + const spriteResizer = this.spriteResizer; + if (spriteResizer.get("sprite") == sprite) { + spriteResizer.set("sprite", undefined); + } + else { + spriteResizer.set("sprite", sprite); + } + }) + + sprite.events.on("pointerover", () => { + this._isHover = true; + }) + + sprite.events.on("pointerout", () => { + this._isHover = false; + }) + + this.spriteResizer.set("sprite", undefined); + + sprite.on("scale", (scale) => { + template.set("scale", scale) + }) + + sprite.on("rotation", (rotation) => { + template.set("rotation", rotation) + }) + + return Bullet.new(this._root, { + locationX: undefined, + sprite: sprite + }) + } }) } diff --git a/src/.internal/charts/stock/drawing/LabelSeries.ts b/src/.internal/charts/stock/drawing/LabelSeries.ts index 32649b25..e9ea03b7 100644 --- a/src/.internal/charts/stock/drawing/LabelSeries.ts +++ b/src/.internal/charts/stock/drawing/LabelSeries.ts @@ -1,11 +1,11 @@ import type { ISpritePointerEvent } from "../../../core/render/Sprite"; import type { Container } from "../../../core/render/Container"; import type { DataItem } from "../../../core/render/Component"; +import type { SpriteResizer } from "../../../core/render/SpriteResizer"; import { PolylineSeries, IPolylineSeriesSettings, IPolylineSeriesPrivate, IPolylineSeriesDataItem } from "./PolylineSeries"; import { Label } from "../../../core/render/Label"; import { RoundedRectangle } from "../../../core/render/RoundedRectangle"; -import { SpriteResizer } from "../../../core/render/SpriteResizer"; import { color, Color } from "../../../core/util/Color"; import { Template } from "../../../core/util/Template"; @@ -57,7 +57,7 @@ export class LabelSeries extends PolylineSeries { declare public _privateSettings: ILabelSeriesPrivate; declare public _dataItemSettings: ILabelSeriesDataItem; - public spriteResizer = this.children.push(SpriteResizer.new(this._root, {})); + public spriteResizer!: SpriteResizer; protected _clickEvent?: ISpritePointerEvent; @@ -66,6 +66,8 @@ export class LabelSeries extends PolylineSeries { protected _afterNew() { super._afterNew(); + this.spriteResizer = this._getStockChart().spriteResizer; + this.strokes.template.set("visible", false); this.fills.template.set("visible", false); @@ -118,40 +120,42 @@ export class LabelSeries extends PolylineSeries { const dataContext = dataItem.dataContext as any; const text = dataContext.text; const template = dataContext.settings; - const label = container.children.push(Label.new(this._root, { - themeTags: ["label"], - text: text - }, template)); - - this.setPrivate("label", label); - - container.events.on("click", () => { - const spriteResizer = this.spriteResizer; - if (spriteResizer.get("sprite") == label) { - spriteResizer.set("sprite", undefined); - } - else { - spriteResizer.set("sprite", label); - } - }) - - container.events.on("pointerover", () => { - this._isHover = true; - }) - - container.events.on("pointerout", () => { - this._isHover = false; - }) - - label.on("scale", (scale) => { - template.set("scale", scale) - }) - - label.on("rotation", (rotation) => { - template.set("rotation", rotation) - }) - - this._tweakBullet2(label, dataItem); + if (template) { + const label = container.children.push(Label.new(this._root, { + themeTags: ["label"], + text: text + }, template)); + + this.setPrivate("label", label); + + container.events.on("click", () => { + const spriteResizer = this.spriteResizer; + if (spriteResizer.get("sprite") == label) { + spriteResizer.set("sprite", undefined); + } + else { + spriteResizer.set("sprite", label); + } + }) + + container.events.on("pointerover", () => { + this._isHover = true; + }) + + container.events.on("pointerout", () => { + this._isHover = false; + }) + + label.on("scale", (scale) => { + template.set("scale", scale); + }) + + label.on("rotation", (rotation) => { + template.set("rotation", rotation) + }) + + this._tweakBullet2(label, dataItem); + } } protected _tweakBullet2(label: Label, _dataItem: DataItem) { @@ -218,6 +222,11 @@ export class LabelSeries extends PolylineSeries { template.fontWeight = labelFontWeight; } + const labelFontStyle = this.get("labelFontStyle"); + if (labelFontStyle != null) { + template.fontStyle = labelFontStyle; + } + const labelFill = this.get("labelFill"); if (labelFill != null) { template.fill = labelFill; diff --git a/src/.internal/charts/stock/indicators/AccumulationDistribution.ts b/src/.internal/charts/stock/indicators/AccumulationDistribution.ts index 758121a3..156ac4f3 100644 --- a/src/.internal/charts/stock/indicators/AccumulationDistribution.ts +++ b/src/.internal/charts/stock/indicators/AccumulationDistribution.ts @@ -57,8 +57,8 @@ export class AccumulationDistribution extends ChartIndicator { type: "checkbox" }]; - public _afterNew(){ - this._themeTags.push("accumulationdistribution"); + public _afterNew() { + this._themeTags.push("accumulationdistribution"); super._afterNew(); this.yAxis.set("numberFormat", "#.###a"); } @@ -76,9 +76,10 @@ export class AccumulationDistribution extends ChartIndicator { } public _prepareChildren() { - if (this.isDirty("useVolume") || this.isDirty("volumeSeries")) { - this._dataDirty = true; - this.setCustomData("useVolume", this.get("useVolume") ? "Y" : "N"); + const useVolume = "useVolume"; + if (this.isDirty(useVolume)) { + this.markDataDirty(); + this.setCustomData(useVolume, this.get(useVolume) ? "Y" : "N"); } super._prepareChildren(); } @@ -88,7 +89,6 @@ export class AccumulationDistribution extends ChartIndicator { */ public prepareData() { if (this.series) { - const dataItems = this.get("stockSeries").dataItems; const volumeSeries = this.get("volumeSeries"); this.setRaw("field", "close"); diff --git a/src/.internal/charts/stock/indicators/AccumulativeSwingIndex.ts b/src/.internal/charts/stock/indicators/AccumulativeSwingIndex.ts index e70052d5..288644c8 100644 --- a/src/.internal/charts/stock/indicators/AccumulativeSwingIndex.ts +++ b/src/.internal/charts/stock/indicators/AccumulativeSwingIndex.ts @@ -98,9 +98,10 @@ export class AccumulativeSwingIndex extends ChartIndicator { } public _prepareChildren() { - if (this.isDirty("limitMoveValue")) { - this.setCustomData("limitMoveValue", this.get("limitMoveValue")) - this._dataDirty = true; + const limitMoveValue = "limitMoveValue"; + if (this.isDirty(limitMoveValue)) { + this.setCustomData(limitMoveValue, this.get(limitMoveValue)); + this.markDataDirty(); } const series = this.series; diff --git a/src/.internal/charts/stock/indicators/Aroon.ts b/src/.internal/charts/stock/indicators/Aroon.ts index bddca1f3..a54369a7 100644 --- a/src/.internal/charts/stock/indicators/Aroon.ts +++ b/src/.internal/charts/stock/indicators/Aroon.ts @@ -86,22 +86,24 @@ export class Aroon extends ChartIndicator { fill: undefined })) - this.yAxis.setAll({ min: -1, max: 101, strictMinMax: true, numberFormat:"#'%'" }); + this.yAxis.setAll({ min: -1, max: 101, strictMinMax: true, numberFormat: "#'%'" }); } public _updateChildren() { super._updateChildren(); - if (this.isDirty("upColor")) { - let color = this.get("upColor", Color.fromHex(0x00ff00)); + const upColor = "upColor"; + if (this.isDirty(upColor)) { + let color = this.get(upColor, Color.fromHex(0x00ff00)); this._updateSeriesColor(this.series, color); - this.setCustomData("upColor", color); + this.setCustomData(upColor, color); } - if (this.isDirty("downColor")) { - let color = this.get("downColor", Color.fromHex(0xff0000)); + const downColor = "downColor"; + if (this.isDirty(downColor)) { + let color = this.get(downColor, Color.fromHex(0xff0000)); this._updateSeriesColor(this.downSeries, color); - this.setCustomData("downColor", color); + this.setCustomData(downColor, color); } } diff --git a/src/.internal/charts/stock/indicators/AwesomeOscillator.ts b/src/.internal/charts/stock/indicators/AwesomeOscillator.ts index 15005321..d1429876 100644 --- a/src/.internal/charts/stock/indicators/AwesomeOscillator.ts +++ b/src/.internal/charts/stock/indicators/AwesomeOscillator.ts @@ -76,13 +76,16 @@ export class AwesomeOscillator extends ChartIndicator { public _updateChildren() { super._updateChildren(); - if (this.isDirty("increasingColor") || this.isDirty("decreasingColor")) { + const increasingColor= "increasingColor"; + const decreasingColor= "decreasingColor"; + + if (this.isDirty(increasingColor) || this.isDirty(decreasingColor)) { const template = this.series.columns.template; - const increasing = this.get("increasingColor"); - const decreasing = this.get("decreasingColor"); + const increasing = this.get(increasingColor); + const decreasing = this.get(decreasingColor); template.states.create("riseFromPrevious", { fill: increasing, stroke: increasing }); template.states.create("dropFromPrevious", { fill: decreasing, stroke: decreasing }); - this._dataDirty = true; + this.markDataDirty(); } } diff --git a/src/.internal/charts/stock/indicators/BollingerBands.ts b/src/.internal/charts/stock/indicators/BollingerBands.ts index 89a0c76c..04d48d20 100644 --- a/src/.internal/charts/stock/indicators/BollingerBands.ts +++ b/src/.internal/charts/stock/indicators/BollingerBands.ts @@ -131,7 +131,7 @@ export class BollingerBands extends MovingAverage { public _prepareChildren() { if (this.isDirty("standardDeviations")) { - this._dataDirty = true; + this.markDataDirty(); } super._prepareChildren(); @@ -139,23 +139,25 @@ export class BollingerBands extends MovingAverage { public _updateChildren() { super._updateChildren(); - if (this.isDirty("upperColor")) { - const color = this.get("upperColor"); + const upperColor = "upperColor"; + if (this.isDirty(upperColor)) { + const color = this.get(upperColor); const upperBandSeries = this.upperBandSeries; upperBandSeries.set("stroke", color); upperBandSeries.set("fill", color); upperBandSeries.strokes.template.set("stroke", color); - this._updateSeriesColor(upperBandSeries, color, "upperColor"); + this._updateSeriesColor(upperBandSeries, color, upperColor); } - if (this.isDirty("lowerColor")) { - const color = this.get("lowerColor"); + const lowerColor = "lowerColor"; + if (this.isDirty(lowerColor)) { + const color = this.get(lowerColor); const lowerBandSeries = this.lowerBandSeries; lowerBandSeries.set("stroke", color); lowerBandSeries.strokes.template.set("stroke", color); - this._updateSeriesColor(lowerBandSeries, color, "lowerColor"); + this._updateSeriesColor(lowerBandSeries, color, lowerColor); } this.setCustomData("standardDeviations", this.get("standardDeviations")); diff --git a/src/.internal/charts/stock/indicators/ChaikinMoneyFlow.ts b/src/.internal/charts/stock/indicators/ChaikinMoneyFlow.ts index 5401456d..a886bd97 100644 --- a/src/.internal/charts/stock/indicators/ChaikinMoneyFlow.ts +++ b/src/.internal/charts/stock/indicators/ChaikinMoneyFlow.ts @@ -1,18 +1,16 @@ import type { IIndicatorEditableSetting } from "./Indicator"; -import type { XYSeries } from "../../xy/series/XYSeries"; import { ChartIndicator, IChartIndicatorSettings, IChartIndicatorPrivate, IChartIndicatorEvents } from "./ChartIndicator"; import { LineSeries } from "../../xy/series/LineSeries"; +import type { XYSeries } from "../../xy/series/XYSeries"; import * as $array from "../../../core/util/Array"; export interface IChaikinMoneyFlowSettings extends IChartIndicatorSettings { - /** - * Main volume series of the [[StockChart]]. + * A volume series indicator will be based on, if it reaquires one. */ - volumeSeries: XYSeries; - + volumeSeries: XYSeries; } export interface IChaikinMoneyFlowPrivate extends IChartIndicatorPrivate { @@ -67,13 +65,6 @@ export class ChaikinMoneyFlow extends ChartIndicator { })) } - public _prepareChildren() { - if (this.isDirty("volumeSeries")) { - this._dataDirty = true; - } - super._prepareChildren(); - } - /** * @ignore */ diff --git a/src/.internal/charts/stock/indicators/ChaikinOscillator.ts b/src/.internal/charts/stock/indicators/ChaikinOscillator.ts index 4ca32880..aa3793f0 100644 --- a/src/.internal/charts/stock/indicators/ChaikinOscillator.ts +++ b/src/.internal/charts/stock/indicators/ChaikinOscillator.ts @@ -78,8 +78,8 @@ export class ChaikinOscillator extends ChartIndicator { } public _prepareChildren() { - if (this.isDirty("volumeSeries") || this.isDirty("slowPeriod")) { - this._dataDirty = true; + if (this.isDirty("slowPeriod")) { + this.markDataDirty(); this.setCustomData("slowPeriod", this.get("slowPeriod")); } super._prepareChildren(); diff --git a/src/.internal/charts/stock/indicators/ChartIndicator.ts b/src/.internal/charts/stock/indicators/ChartIndicator.ts index e9e4af60..4ebda5e0 100644 --- a/src/.internal/charts/stock/indicators/ChartIndicator.ts +++ b/src/.internal/charts/stock/indicators/ChartIndicator.ts @@ -122,8 +122,6 @@ export abstract class ChartIndicator extends Indicator { } chart.set("cursor", XYCursor.new(root, { yAxis: yAxis, xAxis: xAxis })); - - } } diff --git a/src/.internal/charts/stock/indicators/DisparityIndex.ts b/src/.internal/charts/stock/indicators/DisparityIndex.ts index 9476ef04..07131a23 100644 --- a/src/.internal/charts/stock/indicators/DisparityIndex.ts +++ b/src/.internal/charts/stock/indicators/DisparityIndex.ts @@ -79,9 +79,10 @@ export class DisparityIndex extends ChartIndicator { } public _prepareChildren() { - if (this.isDirty("movingAverageType")) { - this._dataDirty = true; - this.setCustomData("movingAverageType", this.get("movingAverageType")); + const movingAverageType = "movingAverageType"; + if (this.isDirty(movingAverageType)) { + this.markDataDirty(); + this.setCustomData(movingAverageType, this.get(movingAverageType)); } super._prepareChildren(); diff --git a/src/.internal/charts/stock/indicators/Indicator.ts b/src/.internal/charts/stock/indicators/Indicator.ts index b027359b..33495f29 100644 --- a/src/.internal/charts/stock/indicators/Indicator.ts +++ b/src/.internal/charts/stock/indicators/Indicator.ts @@ -121,7 +121,7 @@ export abstract class Indicator extends Container { super._prepareChildren(); if (this.isDirty("stockSeries") || this.isDirty("volumeSeries")) { - this._dataDirty = true; + this.markDataDirty(); const stockSeries = this.get("stockSeries"); const previousS = this._prevSettings.stockSeries; @@ -131,10 +131,10 @@ export abstract class Indicator extends Container { if (stockSeries) { this._sDP = new MultiDisposer([ stockSeries.events.on("datavalidated", () => { - this.markDataDirty(); + this._markDataDirty(); }), stockSeries.events.on("datasetchanged", () => { - this.markDataDirty(); + this._markDataDirty(); }) ]) } @@ -147,10 +147,10 @@ export abstract class Indicator extends Container { if (volumeSeries) { this._vDP = new MultiDisposer([ volumeSeries.events.on("datavalidated", () => { - this.markDataDirty(); + this._markDataDirty(); }), volumeSeries.events.on("datasetchanged", () => { - this.markDataDirty(); + this._markDataDirty(); }) ]) } @@ -158,26 +158,33 @@ export abstract class Indicator extends Container { if (this.isDirty("field")) { if (this.get("field")) { - this._dataDirty = true; + this.markDataDirty(); } } if (this.isDirty("period")) { - this._dataDirty = true; + this.markDataDirty(); this.setCustomData("period", this.get("period")); } + if (this._dataDirty) { this.prepareData(); this._dataDirty = false; } } - protected markDataDirty() { + protected _markDataDirty() { this._dataDirty = true; this.markDirty(); } + public markDataDirty() { + this._root.events.once("frameended", () => { + this._markDataDirty(); + }) + } + public _updateChildren() { super._updateChildren(); @@ -189,7 +196,6 @@ export abstract class Indicator extends Container { this.setCustomData("field", this.get("field")); this.setCustomData("name", this.get("name")); this.setCustomData("shortName", this.get("shortName")); - } protected _dispose() { diff --git a/src/.internal/charts/stock/indicators/MACD.ts b/src/.internal/charts/stock/indicators/MACD.ts index 58b0644e..cb561d7b 100644 --- a/src/.internal/charts/stock/indicators/MACD.ts +++ b/src/.internal/charts/stock/indicators/MACD.ts @@ -154,7 +154,7 @@ export class MACD extends ChartIndicator { public _prepareChildren() { if (this.isDirty("fastPeriod") || this.isDirty("slowPeriod") || this.isDirty("signalPeriod")) { - this._dataDirty = true; + this.markDataDirty(); this.setCustomData("fastPeriod", this.get("fastPeriod")); this.setCustomData("slowPeriod", this.get("slowPeriod")); this.setCustomData("signalPeriod", this.get("signalPeriod")); diff --git a/src/.internal/charts/stock/indicators/MovingAverage.ts b/src/.internal/charts/stock/indicators/MovingAverage.ts index df11f78e..97173d6f 100644 --- a/src/.internal/charts/stock/indicators/MovingAverage.ts +++ b/src/.internal/charts/stock/indicators/MovingAverage.ts @@ -75,7 +75,7 @@ export class MovingAverage extends Indicator { public _prepareChildren() { if (this.isDirty("type") || this.isDirty("offset")) { - this._dataDirty = true; + this.markDataDirty(); this.setCustomData("type", this.get("type")); this.setCustomData("offset", this.get("offset", 0)); } diff --git a/src/.internal/charts/stock/indicators/MovingAverageDeviation.ts b/src/.internal/charts/stock/indicators/MovingAverageDeviation.ts index 98feb774..4c943eb4 100644 --- a/src/.internal/charts/stock/indicators/MovingAverageDeviation.ts +++ b/src/.internal/charts/stock/indicators/MovingAverageDeviation.ts @@ -121,7 +121,7 @@ export class MovingAverageDeviation extends ChartIndicator { public _prepareChildren() { if (this.isDirty("type") || this.isDirty("unit")) { - this._dataDirty = true; + this.markDataDirty(); } super._prepareChildren(); } diff --git a/src/.internal/charts/stock/indicators/MovingAverageEnvelope.ts b/src/.internal/charts/stock/indicators/MovingAverageEnvelope.ts index 941c140d..f1a59ef5 100644 --- a/src/.internal/charts/stock/indicators/MovingAverageEnvelope.ts +++ b/src/.internal/charts/stock/indicators/MovingAverageEnvelope.ts @@ -171,7 +171,7 @@ export class MovingAverageEnvelope extends MovingAverage { public _prepareChildren() { if (this.isDirty("shiftType") || this.isDirty("shift")) { - this._dataDirty = true; + this.markDataDirty(); this.setCustomData("shift", this.get("shift")); this.setCustomData("shiftType", this.get("shiftType")); } diff --git a/src/.internal/charts/stock/indicators/OnBalanceVolume.ts b/src/.internal/charts/stock/indicators/OnBalanceVolume.ts index a5cf47ce..d5cb1627 100644 --- a/src/.internal/charts/stock/indicators/OnBalanceVolume.ts +++ b/src/.internal/charts/stock/indicators/OnBalanceVolume.ts @@ -62,13 +62,6 @@ export class OnBalanceVolume extends ChartIndicator { })) } - public _prepareChildren() { - if (this.isDirty("volumeSeries")) { - this._dataDirty = true; - } - super._prepareChildren(); - } - /** * @ignore */ diff --git a/src/.internal/charts/stock/indicators/OverboughtOversold.ts b/src/.internal/charts/stock/indicators/OverboughtOversold.ts index 0e234c42..4847e508 100644 --- a/src/.internal/charts/stock/indicators/OverboughtOversold.ts +++ b/src/.internal/charts/stock/indicators/OverboughtOversold.ts @@ -112,7 +112,6 @@ export class OverboughtOversold extends ChartIndicator { const overBought = yAxis.makeDataItem({}); this.overBought = overBought; overBought.set("endValue", 500); - overBought.set("affectsMinMax", false); const overBoughtRB = Button.new(this._root, { themeTags: ["rangegrip", "vertical", side], @@ -162,8 +161,6 @@ export class OverboughtOversold extends ChartIndicator { const overSold = yAxis.makeDataItem({}); this.overSold = overSold; overSold.set("endValue", -500); - overSold.set("affectsMinMax", false); - const overSoldRB = Button.new(this._root, { themeTags: ["rangegrip", "vertical", side], @@ -275,13 +272,4 @@ export class OverboughtOversold extends ChartIndicator { this.overBoughtRange.strokes!.template.set("stroke", color); } } - - public _updateChildren() { - - if (this.isDirty("period")) { - this._dataDirty = true; - } - - super._updateChildren(); - } } \ No newline at end of file diff --git a/src/.internal/charts/stock/indicators/RelativeStrengthIndex.ts b/src/.internal/charts/stock/indicators/RelativeStrengthIndex.ts index 2dddf913..a356ec6f 100644 --- a/src/.internal/charts/stock/indicators/RelativeStrengthIndex.ts +++ b/src/.internal/charts/stock/indicators/RelativeStrengthIndex.ts @@ -77,15 +77,15 @@ export class RelativeStrengthIndex extends OverboughtOversold { this.smaSeries = smaSeries; } - public _updateChildren(){ + public _updateChildren() { super._updateChildren(); if (this.isDirty("smaColor")) { this._updateSeriesColor(this.smaSeries, this.get("smaColor"), "smaColor") } if (this.isDirty("smaPeriod")) { - this._dataDirty = true; + this.markDataDirty(); this.setCustomData("smaPeriod", this.get("smaPeriod")); - } + } } diff --git a/src/.internal/charts/stock/indicators/StochasticMomentumIndex.ts b/src/.internal/charts/stock/indicators/StochasticMomentumIndex.ts index 918b6114..f578491a 100644 --- a/src/.internal/charts/stock/indicators/StochasticMomentumIndex.ts +++ b/src/.internal/charts/stock/indicators/StochasticMomentumIndex.ts @@ -95,7 +95,7 @@ export class StochasticMomentumIndex extends OverboughtOversold { public _updateChildren() { if (this.isDirty("dPeriod") || this.isDirty("emaPeriod")) { - this._dataDirty = true; + this.markDataDirty(); this.setCustomData("dPeriod", this.get("dPeriod")); this.setCustomData("emaPeriod", this.get("emaPeriod")); } diff --git a/src/.internal/charts/stock/indicators/StochasticOscillator.ts b/src/.internal/charts/stock/indicators/StochasticOscillator.ts index 59ad937f..ef73386c 100644 --- a/src/.internal/charts/stock/indicators/StochasticOscillator.ts +++ b/src/.internal/charts/stock/indicators/StochasticOscillator.ts @@ -93,7 +93,7 @@ export class StochasticOscillator extends OverboughtOversold { public _updateChildren() { if (this.isDirty("kSmoothing") || this.isDirty("dSmoothing")) { - this._dataDirty = true; + this.markDataDirty(); this.setCustomData("dSmoothing", this.get("dSmoothing")); this.setCustomData("kSmoothing", this.get("kSmoothing")); } diff --git a/src/.internal/charts/stock/indicators/Trix.ts b/src/.internal/charts/stock/indicators/Trix.ts index 6717c48b..6e451d8c 100644 --- a/src/.internal/charts/stock/indicators/Trix.ts +++ b/src/.internal/charts/stock/indicators/Trix.ts @@ -105,7 +105,7 @@ export class Trix extends ChartIndicator { public _prepareChildren() { if (this.isDirty("signalPeriod")) { - this._dataDirty = true; + this.markDataDirty(); this.setCustomData("signalPeriod", this.get("signalPeriod")); } super._prepareChildren(); diff --git a/src/.internal/charts/stock/indicators/VWAP.ts b/src/.internal/charts/stock/indicators/VWAP.ts index 2cca51db..78015451 100644 --- a/src/.internal/charts/stock/indicators/VWAP.ts +++ b/src/.internal/charts/stock/indicators/VWAP.ts @@ -71,14 +71,6 @@ export class VWAP extends Indicator { } } - public _prepareChildren() { - if (this.isDirty("volumeSeries")) { - this._dataDirty = true; - } - super._prepareChildren(); - } - - /** * @ignore */ @@ -96,31 +88,31 @@ export class VWAP extends Indicator { let totalVolume = 0; let totalVW = 0; $array.each(data, (dataItem) => { - const volumeDI = volumeSeries.dataItems[i]; - const volume = volumeDI.get("valueY", 0); - const vw = dataItem.value_y * volume; - - dataItem.vw = vw; - dataItem.volume = volume; - - totalVW += vw; - totalVolume += volume; - - - if (i >= period) { - let volumeToRemove = data[i - period].volume; - let vwToRemove = data[i - period].vw; - if (volumeToRemove != null) { - totalVolume -= volumeToRemove; + if (volumeDI) { + const volume = volumeDI.get("valueY", 0); + const vw = dataItem.value_y * volume; + + dataItem.vw = vw; + dataItem.volume = volume; + + totalVW += vw; + totalVolume += volume; + + if (i >= period) { + let volumeToRemove = data[i - period].volume; + let vwToRemove = data[i - period].vw; + if (volumeToRemove != null) { + totalVolume -= volumeToRemove; + } + if (vwToRemove != null) { + totalVW -= vwToRemove; + } } - if (vwToRemove != null) { - totalVW -= vwToRemove; - } - } - dataItem.totalVW = totalVW; - dataItem.vwap = totalVW / totalVolume; + dataItem.totalVW = totalVW; + dataItem.vwap = totalVW / totalVolume; + } i++; }) diff --git a/src/.internal/charts/stock/indicators/Volume.ts b/src/.internal/charts/stock/indicators/Volume.ts index 8648e0ee..56ba1729 100644 --- a/src/.internal/charts/stock/indicators/Volume.ts +++ b/src/.internal/charts/stock/indicators/Volume.ts @@ -74,7 +74,7 @@ export class Volume extends ChartIndicator { public _prepareChildren() { if (this.isDirty("increasingColor") || this.isDirty("decreasingColor")) { - this._dataDirty = true; + this.markDataDirty(); } super._prepareChildren(); } diff --git a/src/.internal/charts/stock/indicators/VolumeProfile.ts b/src/.internal/charts/stock/indicators/VolumeProfile.ts new file mode 100644 index 00000000..9026e4cd --- /dev/null +++ b/src/.internal/charts/stock/indicators/VolumeProfile.ts @@ -0,0 +1,547 @@ +import type { DateAxis } from "../../xy/axes/DateAxis"; +import type { Color } from "../../../core/util/Color"; +import type { Graphics } from "../../../core/render/Graphics"; + +import { AxisRendererX } from "../../xy/axes/AxisRendererX"; +import { ValueAxis } from "../../xy/axes/ValueAxis"; +import { Indicator, IIndicatorSettings, IIndicatorPrivate, IIndicatorEvents, IIndicatorEditableSetting } from "./Indicator"; +import { ColumnSeries } from "../../xy/series/ColumnSeries"; +import { percent } from "../../../core/util/Percent"; + +import * as $type from "../../../core/util/Type"; + +export interface IVolumeProfileSettings extends IIndicatorSettings { + /** + * Volume up color. + */ + upColor?: Color; + + /** + * Volume down color. + */ + downColor?: Color; + + /** + * Type of count + * @todo: what if translated in options? + */ + countType?: "rows" | "ticks"; + + /** + * Number of rows or number of ticks, depending on the countType. + * + * @default 24 + */ + count?: number; + + /** + * Max width of columns in percent (%). + * + * @default 40 + */ + axisWidth?: number; + + /** + * Specifies what percentage of all volume for the trading session should + * be highlighted by Value Area. + * + * @default 70 + */ + valueArea?: number; + + /** + * Opacity of columns which fall withing value area. + * + * @default .7 + */ + valueAreaOpacity?: number; +} + +export interface IVolumeProfilePrivate extends IIndicatorPrivate { +} + +export interface IVolumeProfileEvents extends IIndicatorEvents { +} + +/** + * An implementation of a Volume Profile indicator for a [[StockChart]]. + * + * @see {@link https://www.amcharts.com/docs/v5/charts/stock/indicators/} for more info + * @since 5.7.0 + */ +export class VolumeProfile extends Indicator { + public static className: string = "VolumeProfile"; + public static classNames: Array = Indicator.classNames.concat([VolumeProfile.className]); + + declare public _settings: IVolumeProfileSettings; + declare public _privateSettings: IVolumeProfilePrivate; + declare public _events: IVolumeProfileEvents; + + /** + * Indicator series. + */ + declare public series: ColumnSeries; + + public _editableSettings: IIndicatorEditableSetting[] = [{ + key: "countType", + name: this.root.language.translateAny("Count"), + type: "dropdown", + options: [ + { value: "rows", text: this.root.language.translateAny("number of rows") }, + { value: "ticks", text: this.root.language.translateAny("ticks per row") } + ] + }, { + key: "count", + name: this.root.language.translateAny("Count"), + type: "number" + }, { + key: "valueArea", + name: this.root.language.translateAny("Value Area"), + type: "number" + }, { + key: "upColor", + name: this.root.language.translateAny("Up volume"), + type: "color" + }, { + key: "downColor", + name: this.root.language.translateAny("Down volume"), + type: "color" + }, { + key: "axisWidth", + name: this.root.language.translateAny("Width %"), + type: "number" + }]; + + public xAxis!: ValueAxis; + + public upSeries!: ColumnSeries; + + protected _previousColumn?: Graphics; + + protected _afterNew() { + super._afterNew(); + + const volumeSeries = this.get("volumeSeries"); + const stockSeries = this.get("stockSeries"); + if (volumeSeries) { + const chart = stockSeries.chart; + const root = this._root; + + if (chart) { + const yAxis = stockSeries.get("yAxis") as any; + const panelXAxis = stockSeries.get("xAxis") as DateAxis; + + panelXAxis.on("start", () => { + this.markDataDirty(); + }) + panelXAxis.on("end", () => { + this.markDataDirty(); + }) + + const xRenderer = AxisRendererX.new(root, {}); + xRenderer.grid.template.set("forceHidden", true); + xRenderer.labels.template.set("forceHidden", true); + + this.xAxis = chart.xAxes.push(ValueAxis.new(root, { + zoomable: false, + strictMinMax: true, + renderer: xRenderer + })); + + if (yAxis.get("renderer").get("opposite")) { + xRenderer.set("inversed", true); + this.xAxis.setAll({ + x: percent(100), + centerX: percent(100) + }) + } + + this.series = chart.series.unshift(ColumnSeries.new(root, { + xAxis: this.xAxis, + yAxis: yAxis, + valueXField: "down", + openValueXField: "xOpen", + openValueYField: "yOpen", + valueYField: "y", + calculateAggregates: true, + themeTags: ["indicator", "volumeprofile"] + })) + + + this.upSeries = chart.series.unshift(ColumnSeries.new(root, { + xAxis: this.xAxis, + yAxis: yAxis, + valueXField: "total", + openValueXField: "down", + openValueYField: "yOpen", + valueYField: "y", + calculateAggregates: true, + themeTags: ["indicator", "volumeprofile"] + })) + + this.upSeries.setPrivate("doNotUpdateLegend", true); + this.series.setPrivate("doNotUpdateLegend", true); + + this.upSeries.setPrivate("baseValueSeries", stockSeries); + this.series.setPrivate("baseValueSeries", stockSeries); + + this._handleLegend(this.series); + + this._addInteractivity(this.series); + this._addInteractivity(this.upSeries); + } + } + } + + protected _addInteractivity(series: ColumnSeries) { + series.columns.template.events.on("pointerover", (e) => { + let dataItem = e.target.dataItem; + + if (dataItem) { + if (dataItem.component == this.upSeries) { + dataItem = this.series.dataItems[this.upSeries.dataItems.indexOf(dataItem)]; + if (dataItem) { + const column = dataItem.get("graphics" as any); + if (column) { + column.hover(); + this._previousColumn = column; + } + } + } + else { + dataItem = this.upSeries.dataItems[this.series.dataItems.indexOf(dataItem)]; + if (dataItem) { + const column = dataItem.get("graphics" as any); + if (column) { + column.hover(); + this._previousColumn = column; + } + } + } + + this.series.updateLegendValue(dataItem); + } + }) + + series.columns.template.events.on("pointerout", () => { + this.series.updateLegendValue(undefined); + if (this._previousColumn) { + this._previousColumn.unhover(); + } + }) + + series.columns.template.adapters.add("fillOpacity", (fillOpacity, target) => { + const dataItem = target.dataItem; + if (dataItem) { + const dataContext = dataItem.dataContext as any; + if (dataContext) { + if (dataContext.area) { + return this.get("valueAreaOpacity", .7); + } + } + } + return fillOpacity; + }) + } + + public _updateChildren(): void { + super._updateChildren(); + + if (this.isDirty("count") || this.isDirty("countType") || this.isDirty("valueArea")) { + this.markDataDirty(); + } + + if (this.isDirty("upColor")) { + const upColor = this.get("upColor"); + this.upSeries.set("fill", upColor); + this.upSeries.set("stroke", upColor); + this.setCustomData("upColor", upColor); + } + + if (this.isDirty("downColor")) { + const downColor = this.get("downColor"); + this.series.set("fill", downColor); + this.series.set("stroke", downColor); + this.setCustomData("downColor", downColor); + } + + if (this.isDirty("axisWidth")) { + this.xAxis.set("width", percent(this.get("axisWidth", 40))); + } + } + + /** + * @ignore + */ + public prepareData() { + const volumeSeries = this.get("volumeSeries"); + const stockSeries = this.get("stockSeries"); + + if (volumeSeries) { + let startIndex = volumeSeries.startIndex(); + let endIndex = volumeSeries.endIndex(); + + const count = this.get("count", 20); + const type = this.get("countType"); + let step = 1; + + let min = Infinity; + let max = -Infinity; + + for (let i = startIndex; i < endIndex; i++) { + const dataItem = stockSeries.dataItems[i]; + if (dataItem) { + const close = dataItem.get("valueY"); + if ($type.isNumber(close)) { + if (close < min) { + min = close; + } + + if (close > max) { + max = close; + } + } + } + } + + let rows: number; + if (type == "ticks") { + step = count / 100; + min = Math.floor(min / step) * step; + max = Math.ceil(max / step) * step; + rows = (max - min) / step; + } + else { + step = (max - min) / count; + rows = count; + } + + const rowDataDown: Array = []; + const rowDataUp: Array = []; + + for (let i = 0; i < rows; i++) { + rowDataDown[i] = 0; + rowDataUp[i] = 0; + } + + let previousDataItem; + for (let i = startIndex; i < endIndex; i++) { + const dataItem = stockSeries.dataItems[i]; + const volumeDataItem = volumeSeries.dataItems[i]; + if (dataItem && volumeDataItem) { + const close = dataItem.get("valueY"); + const volume = volumeDataItem.get("valueY"); + + if ($type.isNumber(close) && $type.isNumber(volume)) { + let index = Math.floor((close - min) / step); + if (index == count) { + index = count - 1; + } + + if ($type.isNumber(index)) { + if (previousDataItem && previousDataItem.get("valueY", close) < close) { + rowDataDown[index] += volume; + } + else { + rowDataUp[index] += volume; + } + } + } + previousDataItem = dataItem; + } + } + + const dataDown = []; + let sum = 0; + for (let i = 0; i < rows; i++) { + let total = rowDataUp[i] + rowDataDown[i]; + sum += total; + + dataDown.push({ + yOpen: min + i * step, + y: min + (i + 1) * step, + up: rowDataUp[i], + down: rowDataDown[i], + total: total, + xOpen: 0, + area: false + }) + } + + let len = this.series.data.length; + if (len == dataDown.length) { + for (let i = 0; i < len; i++) { + this.series.data.setIndex(i, dataDown[i]); + } + } + else { + this.series.data.setAll(dataDown); + } + + const dataUp = []; + + let highest = 0; + let hi = 0; + + for (let i = 0; i < rows; i++) { + let total = rowDataUp[i] + rowDataDown[i]; + dataUp.push({ + yOpen: min + i * step, + y: min + (i + 1) * step, + up: rowDataUp[i], + down: rowDataDown[i], + total: total, + area: false + }) + + if (total > highest) { + highest = total; + hi = i; + } + } + + let valueArea = sum * this.get("valueArea", 70) / 100; + let area = highest; + + let cd = 1; + let cu = 1; + let dlen = dataUp.length; + + dataUp[hi].area = true; + dataDown[hi].area = true; + + /* + // with two rows + while (area < valueArea) { + let rowAbove1 = hi + cu; + let rowAbove2 = hi + cu + 1; + + let sumAbove = 0 + if (rowAbove1 < dlen) { + sumAbove += dataUp[rowAbove1].total; + } + if (rowAbove2 < dlen) { + sumAbove += dataUp[rowAbove2].total; + } + + let rowBelow1 = hi - cd; + let rowBelow2 = hi - cd - 1; + + let sumBelow = 0 + if (rowBelow1 >= 0) { + sumBelow += dataUp[rowBelow1].total; + } + if (rowBelow2 >= 0) { + sumBelow += dataUp[rowBelow2].total; + } + + if (sumBelow <= sumAbove) { + area += sumAbove; + if (rowAbove1 < dlen) { + dataDown[rowAbove1].area = true; + dataUp[rowAbove1].area = true; + cu++; + } + if (rowAbove2 < dlen) { + dataDown[rowAbove2].area = true; + dataUp[rowAbove2].area = true; + cu++; + } + } + else { + area += sumBelow; + if (rowBelow1 >= 0) { + dataDown[rowBelow1].area = true; + dataUp[rowBelow1].area = true; + cd++; + } + if (rowBelow2 >= 0) { + dataDown[rowBelow2].area = true; + dataUp[rowBelow2].area = true; + cd++; + } + } + + if (sumBelow == 0) { + cd++; + } + if (sumAbove == 0) { + cu++; + } + + if ((cd > dlen && cu > dlen)) { + break; + } + + } + */ + + // single row + while (area < valueArea) { + let rowAbove1 = hi + cu; + let sumAbove = 0 + if (rowAbove1 < dlen) { + sumAbove += dataUp[rowAbove1].total; + } + + let rowBelow1 = hi - cd; + + let sumBelow = 0 + if (rowBelow1 >= 0) { + sumBelow += dataUp[rowBelow1].total; + } + + if (sumBelow <= sumAbove) { + area += sumAbove; + if (rowAbove1 < dlen) { + dataDown[rowAbove1].area = true; + dataUp[rowAbove1].area = true; + cu++; + } + } + else { + area += sumBelow; + if (rowBelow1 >= 0) { + dataDown[rowBelow1].area = true; + dataUp[rowBelow1].area = true; + cd++; + } + } + + if (sumBelow == 0) { + cd++; + } + if (sumAbove == 0) { + cu++; + } + + if ((cd > dlen && cu > dlen)) { + break; + } + } + + area = Math.ceil(area); + + len = this.upSeries.data.length; + if (len == dataUp.length) { + for (let i = 0; i < len; i++) { + this.upSeries.data.setIndex(i, dataUp[i]); + } + } + else { + this.upSeries.data.setAll(dataUp); + } + } + } + + protected _dispose() { + super._dispose(); + if (this.upSeries) { + this.upSeries.dispose(); + } + + if (this.xAxis) { + this.xAxis.dispose(); + } + } +} \ No newline at end of file diff --git a/src/.internal/charts/stock/toolbar/DataSaveControl.ts b/src/.internal/charts/stock/toolbar/DataSaveControl.ts new file mode 100644 index 00000000..5c10cfcc --- /dev/null +++ b/src/.internal/charts/stock/toolbar/DataSaveControl.ts @@ -0,0 +1,264 @@ +import type { IDropdownListItem } from "./DropdownList"; + +import { DropdownListControl, IDropdownListControlSettings, IDropdownListControlPrivate, IDropdownListControlEvents } from "./DropdownListControl"; +import { DrawingControl } from "./DrawingControl"; +import { IndicatorControl } from "./IndicatorControl"; +import { StockIcons } from "./StockIcons"; + +import * as $array from "../../../core/util/Array"; + +export interface IDataSaveControlItem extends IDropdownListItem { +} + +export interface IDataSaveControlSettings extends IDropdownListControlSettings { + /** + * If set to `true`, all changes to chart's drawings and indicators will be + * automatically saved to browser local storage and restored on next load. + * + * @default false + */ + autoSave?: boolean; + + /** + * A unique indentifier for local storage. + * + * Will try to use chart's container ID if not set. + * + * Consider setting it if you have multipl [[StockChart]] on the same page. + */ + storageId?: string; +} + +export interface IDataSaveControlPrivate extends IDropdownListControlPrivate { + drawingControl?: DrawingControl; + indicatorControl?: IndicatorControl; + storageId?: string; +} + +export interface IDataSaveControlEvents extends IDropdownListControlEvents { + + /** + * Invoked when drawing/indicator data is serialized and saved to local + * storage. + */ + saved: { + drawings: string; + indicators: string; + }; + + /** + * Invoked when drawing/indicator data is loaded from local storage and + * restored on chart. + */ + restored: { + drawings: string; + indicators: string; + }; + + /** + * Invoked when local storage is cleared. + */ + cleared: {}; + +} + +/** + * A control that can be used to serialize indicators and drawings, save them + * to local storage, and restore as needed. + * + * @see {@link https://www.amcharts.com/docs/v5/charts/stock/toolbar/data-save-control/} for more info + * @since 5.7.0 + */ +export class DataSaveControl extends DropdownListControl { + public static className: string = "DataSaveControl"; + public static classNames: Array = DropdownListControl.classNames.concat([DataSaveControl.className]); + + declare public _settings: IDataSaveControlSettings; + declare public _privateSettings: IDataSaveControlPrivate; + declare public _events: IDataSaveControlEvents; + + protected _afterNew() { + super._afterNew(); + + this.setPrivate("storageId", window.location.href + "-" + this.root.dom.id); + + const stockChart = this.get("stockChart"); + const dropdown = this.getPrivate("dropdown")!; + + // Drawing control + let drawingControl = stockChart.getControl("DrawingControl"); + if (!drawingControl) { + drawingControl = DrawingControl.new(this.root, { + stockChart: stockChart + }); + } + this.setPrivate("drawingControl", drawingControl as DrawingControl); + + // Indicator control + let indicatorControl = stockChart.getControl("IndicatorControl"); + if (!indicatorControl) { + indicatorControl = IndicatorControl.new(this.root, { + stockChart: stockChart + }); + } + this.setPrivate("indicatorControl", indicatorControl as IndicatorControl); + + // Load local storage + if (localStorage && localStorage.getItem(this._getStorageId("autosave")) == "1") { + this.set("autoSave", true); + this.restoreData(); + } + + dropdown.events.on("changed", (ev) => { + if (ev.item.id == "autosave") { + const autoSave = !ev.item.checked; + this.set("autoSave", autoSave); + } + }); + + dropdown.events.on("invoked", (ev) => { + if (ev.item.id == "save") { + this.saveData(); + } + else if (ev.item.id == "restore") { + this.restoreData(); + } + else if (ev.item.id == "clear") { + this.clearData(); + } + + }); + + this.on("active", () => { + this._populateInputs(); + }); + + stockChart.events.on("drawingsupdated", (_ev) => { + if (this.get("autoSave")) { + this.saveData(); + } + }); + + stockChart.events.on("indicatorsupdated", (_ev) => { + if (this.get("autoSave")) { + this.saveData(); + } + }); + + } + + public _beforeChanged() { + super._beforeChanged(); + + if (this.isDirty("autoSave") && localStorage) { + const autoSave = this.get("autoSave", false); + if (autoSave) { + localStorage.setItem(this._getStorageId("autosave"), "1"); + this.saveData(); + } + else { + localStorage.removeItem(this._getStorageId("autosave")); + this.clearData(); + } + this._populateInputs(); + } + } + + public saveData(): void { + if (localStorage) { + const drawingControl = this.getPrivate("drawingControl")!; + const indicatorControl = this.getPrivate("indicatorControl")!; + const drawings = drawingControl.serializeDrawings("string", " ") as string; + const indicators = indicatorControl.serializeIndicators("string", " ") as string; + if (drawings == "[]") { + localStorage.removeItem(this._getStorageId("drawings")); + } + else { + localStorage.setItem(this._getStorageId("drawings"), drawings); + } + if (indicators == "[]") { + localStorage.removeItem(this._getStorageId("indicators")); + } + else { + localStorage.setItem(this._getStorageId("indicators"), indicators); + } + this.events.dispatch("saved", { + target: this, + type: "saved", + drawings: drawings, + indicators: indicators + }); + } + } + + public restoreData(): void { + if (localStorage) { + const stockChart = this.get("stockChart"); + stockChart.panels.each((panel) => { + panel.drawings.each((drawing) => { + drawing.data.clear(); + }); + }); + + stockChart.indicators.clear(); + + const drawingControl = this.getPrivate("drawingControl")!; + const indicatorControl = this.getPrivate("indicatorControl")!; + const drawings = localStorage.getItem(this._getStorageId("drawings")) || "[]"; + const indicators = localStorage.getItem(this._getStorageId("indicators")) || "[]"; + drawingControl.unserializeDrawings(drawings); + indicatorControl.unserializeIndicators(indicators); + this.events.dispatch("restored", { + target: this, + type: "restored", + drawings: drawings, + indicators: indicators + }); + } + } + + public clearData(): void { + if (localStorage) { + localStorage.removeItem(this._getStorageId("drawings")); + localStorage.removeItem(this._getStorageId("indicators")); + this.events.dispatch("cleared", { + target: this, + type: "cleared" + }); + } + } + + protected _getDefaultIcon(): SVGElement { + return StockIcons.getIcon("Save"); + } + + protected _populateInputs(): void { + const dropdown = this.getPrivate("dropdown")!; + const items = dropdown.get("items", []); + const autoSave = this.get("autoSave", false); + const isSavedData = localStorage && (localStorage.getItem(this._getStorageId("drawings")) !== null || localStorage.getItem(this._getStorageId("indicators")) !== null); + $array.each(items, (item) => { + if (!localStorage) { + item.disabled = true; + } + else if (item.id == "restore") { + item.disabled = autoSave || !isSavedData; + } + else if (item.id == "clear") { + item.disabled = !isSavedData; + } + else if (item.id == "save") { + item.disabled = autoSave; + } + else if (item.id == "autosave") { + item.checked = autoSave; + } + }) + dropdown.rebuildList(); + } + + protected _getStorageId(bucket: string): string { + return "am5-stock-" + this.get("storageId", this.getPrivate("storageId", "")) + "-" + bucket; + } + +} \ No newline at end of file diff --git a/src/.internal/charts/stock/toolbar/DrawingControl.ts b/src/.internal/charts/stock/toolbar/DrawingControl.ts index f8ea13e4..eb53cbde 100644 --- a/src/.internal/charts/stock/toolbar/DrawingControl.ts +++ b/src/.internal/charts/stock/toolbar/DrawingControl.ts @@ -1138,6 +1138,7 @@ export class DrawingControl extends StockControl { JsonParser.new(this._root).parse(drawing.__drawing).then((drawingData: any) => { drawingSeries.data.pushAll(drawingData); }); + } else { // Wait until panel becomes available diff --git a/src/.internal/charts/stock/toolbar/Dropdown.ts b/src/.internal/charts/stock/toolbar/Dropdown.ts index d62d3154..10f8f95c 100644 --- a/src/.internal/charts/stock/toolbar/Dropdown.ts +++ b/src/.internal/charts/stock/toolbar/Dropdown.ts @@ -128,11 +128,16 @@ export class Dropdown extends Entity { } public hide(): void { - this.getPrivate("container")!.style.display = "none"; + const arrow = this.getPrivate("arrow")!; + const container = this.getPrivate("container")!; + container.style.display = "none"; this.events.dispatch("closed", { type: "closed", target: this }); + + container.style.marginLeft = ""; + arrow.style.marginLeft = ""; } public show(): void { diff --git a/src/.internal/charts/stock/toolbar/DropdownList.ts b/src/.internal/charts/stock/toolbar/DropdownList.ts index 7d84c0eb..e1c0d838 100644 --- a/src/.internal/charts/stock/toolbar/DropdownList.ts +++ b/src/.internal/charts/stock/toolbar/DropdownList.ts @@ -18,12 +18,36 @@ export interface IDropdownListItem { } export interface IDropdownListSettings extends IDropdownSettings { + + /** + * A list of items in the dropdown. + */ items?: IDropdownListItem[]; + + /** + * Maximum search items to show. + */ maxSearchItems?: number; + + /** + * Is the list searchable? If `true` shows search field and + * calls `searchCallback` function for a list of items. + */ searchable?: boolean; + + /** + * A callback function which returns a list of items based on a search query. + */ searchCallback?: (query: string) => Promise; -} + /** + * An array of item IDs to now show in the list. + * + * @since 5.7.0 + */ + exclude?: string[]; + +} export interface IDropdownListPrivate extends IDropdownPrivate { list?: HTMLUListElement; @@ -44,6 +68,8 @@ export interface IDropdownListEvents extends IDropdownEvents { /** * A dropdown control for [[StockToolbar]]. + * + * @see {@link https://www.amcharts.com/docs/v5/charts/stock/toolbar/dropdown-list-control/} for more info */ export class DropdownList extends Dropdown { public static className: string = "DropdownList"; @@ -89,6 +115,17 @@ export class DropdownList extends Dropdown { } } + /** + * Rebuilds the list. + * + * Useful when changing item data within item list. + * + * @since 5.7.0 + */ + public rebuildList(): void { + this._initItems(); + } + protected _initItems(items?: IDropdownListItem[]): void { const list = this.getPrivate("list")!; list.innerHTML = ""; @@ -96,8 +133,12 @@ export class DropdownList extends Dropdown { if (!items) { items = this.get("items", []); } + + const exclude: any = this.get("exclude", []); $array.each(items, (item) => { - this.addItem(item); + if (exclude.indexOf(item.id) == -1) { + this.addItem(item); + } }); if (this.get("scrollable")) { @@ -201,7 +242,7 @@ export class DropdownList extends Dropdown { item.appendChild(info.icon); } - let inputId: string | undefined;; + let inputId: string | undefined; if (info.form) { const input: HTMLInputElement = document.createElement("input"); inputId = "am5stock-list-" + info.id; @@ -250,6 +291,10 @@ export class DropdownList extends Dropdown { item.appendChild(subLabel); } + if (info.id == "separator") { + item.innerHTML = "
"; + } + list.appendChild(item); // Add click event diff --git a/src/.internal/charts/stock/toolbar/DropdownListControl.ts b/src/.internal/charts/stock/toolbar/DropdownListControl.ts index d75aa121..b266b2a9 100644 --- a/src/.internal/charts/stock/toolbar/DropdownListControl.ts +++ b/src/.internal/charts/stock/toolbar/DropdownListControl.ts @@ -5,13 +5,51 @@ import * as $array from "../../../core/util/Array"; import * as $type from "../../../core/util/Type"; export interface IDropdownListControlSettings extends IStockControlSettings { + + /** + * Currently selected item. + */ currentItem?: string | IDropdownListItem; + + /** + * Label does not change when item is selected in the list. + */ fixedLabel?: boolean; + + /** + * A list of items in the dropdown. + */ items?: Array; + + /** + * If set to `true`, the dropdown will fix the height to fit within chart's + * area, with scroll if the contents do not fit. + */ scrollable?: boolean; + + /** + * Maximum search items to show. + */ maxSearchItems?: number, + + /** + * Is the list searchable? If `true` shows search field and + * calls `searchCallback` function for a list of items. + */ searchable?: boolean; + + /** + * A callback function which returns a list of items based on a search query. + */ searchCallback?: (query: string) => IDropdownListItem[]; + + /** + * An array of item IDs to now show in the list. + * + * @since 5.7.0 + */ + exclude?: string[]; + } export interface IDropdownListControlPrivate extends IStockControlPrivate { @@ -59,7 +97,8 @@ export class DropdownListControl extends StockControl { parent: this.getPrivate("button"), searchable: this.get("searchable", false), scrollable: this.get("scrollable", false), - items: [] + items: [], + exclude: this.get("exclude") } const maxSearchItems = this.get("maxSearchItems"); diff --git a/src/.internal/charts/stock/toolbar/IndicatorControl.ts b/src/.internal/charts/stock/toolbar/IndicatorControl.ts index 49c066e9..a283f4bc 100644 --- a/src/.internal/charts/stock/toolbar/IndicatorControl.ts +++ b/src/.internal/charts/stock/toolbar/IndicatorControl.ts @@ -26,6 +26,7 @@ import { StochasticOscillator } from "../indicators/StochasticOscillator"; import { WilliamsR } from "../indicators/WilliamsR"; import { Trix } from "../indicators/Trix"; import { Volume } from "../indicators/Volume"; +import { VolumeProfile } from "../indicators/VolumeProfile"; import { VWAP } from "../indicators/VWAP"; import { ZigZag } from "../indicators/ZigZag"; @@ -40,7 +41,7 @@ import { StockIcons } from "./StockIcons"; import * as $array from "../../../core/util/Array"; import * as $type from "../../../core/util/Type"; -export type Indicators = "Accumulation Distribution" | "Accumulative Swing Index" | "Aroon" | "Awesome Oscillator" | "Bollinger Bands" | "Chaikin Money Flow" | "Chaikin Oscillator" | "Commodity Channel Index" | "Disparity Index" | "MACD" | "Momentum" | "Moving Average" | "Moving Average Deviation" | "Moving Average Envelope" | "On Balance Volume" | "Relative Strength Index" | "Standard Deviation" | "Stochastic Oscillator" | "Stochastic Momentum Index" | "Trix" | "Typical Price" | "Volume" | "VWAP" | "Williams R" | "Median Price" | "ZigZag"; +export type Indicators = "Accumulation Distribution" | "Accumulative Swing Index" | "Aroon" | "Awesome Oscillator" | "Bollinger Bands" | "Chaikin Money Flow" | "Chaikin Oscillator" | "Commodity Channel Index" | "Disparity Index" | "MACD" | "Momentum" | "Moving Average" | "Moving Average Deviation" | "Moving Average Envelope" | "On Balance Volume" | "Relative Strength Index" | "Standard Deviation" | "Stochastic Oscillator" | "Stochastic Momentum Index" | "Trix" | "Typical Price" | "Volume" | "Volume Profile" | "VWAP" | "Williams R" | "Median Price" | "ZigZag"; export interface IIndicator { id: string; @@ -160,7 +161,7 @@ export class IndicatorControl extends DropdownListControl { */ public supportsIndicator(indicatorId: Indicators): boolean { const stockChart = this.get("stockChart"); - const volumeIndicators = ["Chaikin Money Flow", "Chaikin Oscillator", "On Balance Volume", "Volume", "VWAP"]; + const volumeIndicators = ["Chaikin Money Flow", "Chaikin Oscillator", "On Balance Volume", "Volume", "VolumeProfile", "VWAP"]; return (stockChart.get("volumeSeries") || volumeIndicators.indexOf(indicatorId) === -1) ? true : false; } @@ -359,6 +360,14 @@ export class IndicatorControl extends DropdownListControl { volumeSeries: volumeSeries }); break; + case "Volume Profile": + indicator = VolumeProfile.new(this.root, { + stockChart: stockChart, + stockSeries: stockSeries, + volumeSeries: volumeSeries, + legend: legend + }); + break; case "VWAP": indicator = VWAP.new(this.root, { stockChart: stockChart, diff --git a/src/.internal/charts/stock/toolbar/PeriodSelector.ts b/src/.internal/charts/stock/toolbar/PeriodSelector.ts index 92eeaef8..f3b71ea3 100644 --- a/src/.internal/charts/stock/toolbar/PeriodSelector.ts +++ b/src/.internal/charts/stock/toolbar/PeriodSelector.ts @@ -97,7 +97,6 @@ export class PeriodSelector extends StockControl { const periods = this.get("periods", []); const axis = this._getAxis(); - this.setPrivate("deferTimeout", this.setTimeout(() => this.setPrivate("deferReset", false), axis.get("interpolationDuration", 1000) + 200)); axis.onPrivate("min", () => this._setPeriodButtonStatus()); axis.onPrivate("max", () => this._setPeriodButtonStatus()); $array.each(periods, (period) => { @@ -117,6 +116,7 @@ export class PeriodSelector extends StockControl { if (timeout) { timeout.dispose(); } + this.setPrivate("deferTimeout", this.setTimeout(() => this.setPrivate("deferReset", false), axis.get("interpolationDuration", 1000) + 200)); })); }); diff --git a/src/.internal/charts/stock/toolbar/SettingsControl.ts b/src/.internal/charts/stock/toolbar/SettingsControl.ts index e635d38d..f441d6c9 100644 --- a/src/.internal/charts/stock/toolbar/SettingsControl.ts +++ b/src/.internal/charts/stock/toolbar/SettingsControl.ts @@ -3,15 +3,37 @@ import type { IDropdownListItem } from "./DropdownList"; import { ValueAxis } from "../../xy/axes/ValueAxis"; import { DropdownListControl, IDropdownListControlSettings, IDropdownListControlPrivate, IDropdownListControlEvents } from "./DropdownListControl"; import { StockIcons } from "./StockIcons"; +import { DataSaveControl } from "./DataSaveControl"; import * as $array from "../../../core/util/Array"; export interface ISettingsControlItem extends IDropdownListItem { } export interface ISettingsControlSettings extends IDropdownListControlSettings { + /** + * If set to `true`, all changes to chart's drawings and indicators will be + * automatically saved to browser local storage and restored on next load. + * + * @default false + * + * @since 5.4.3 + */ + autoSave?: boolean; + + /** + * A unique indentifier for local storage. + * + * Will try to use chart's container ID if not set. + * + * Consider setting it if you have multipl [[StockChart]] on the same page. + * + * @since 5.4.3 + */ + storageId?: string; } export interface ISettingsControlPrivate extends IDropdownListControlPrivate { + dataSaveControl?: DataSaveControl; } export interface ISettingsControlEvents extends IDropdownListControlEvents { @@ -51,29 +73,59 @@ export class SettingsControl extends DropdownListControl { } } + if (ev.item.id == "autosave") { + const autoSave = (ev).checked; + let dataSaveControl = this.getPrivate("dataSaveControl")!; + dataSaveControl.set("autoSave", autoSave); + } + }); this.on("active", () => { this._populateInputs(); }); - } + const stockChart = this.get("stockChart"); + const serializableTools = (stockChart.getControl("IndicatorControl") || stockChart.getControl("DrawingControl")) ? true : false; + let dataSaveControl = stockChart.getControl("DataSaveControl"); + + if(!serializableTools) { + const exclude: any = dropdown.get("exclude", []); + exclude.push("save"); + exclude.push("autosave"); + dropdown.set("exclude", ["save", "autosave"]); + } + else { + if(!dataSaveControl) { + dataSaveControl = DataSaveControl.new(this.root, { + stockChart: stockChart, + autoSave: this.get("autoSave", false), + storageId: this.get("storageId") + }); + } + this.setPrivate("dataSaveControl", dataSaveControl as DataSaveControl); + this.set("autoSave", (dataSaveControl as DataSaveControl).get("autoSave")); + this._populateInputs(); + } - // public _afterChanged() { - // super._afterChanged(); - // } + } protected _getDefaultIcon(): SVGElement { return StockIcons.getIcon("Settings"); } protected _populateInputs(): void { + + // Axes-related stuff const button = this.getPrivate("button")!; const inputs = button.getElementsByTagName("input"); const currentScale = this._getYScale(); for (let i = 0; i < inputs.length; i++) { const input = inputs[i]; switch (input.id) { + case "am5stock-list-autosave": + input.checked = this.get("autoSave", this.getPrivate("dataSaveControl")!.get("autoSave", false)); + break; case "am5stock-list-fills": input.checked = this._getFillEnabled() break; diff --git a/src/.internal/charts/stock/toolbar/StockControl.ts b/src/.internal/charts/stock/toolbar/StockControl.ts index 5466b825..8da3598f 100644 --- a/src/.internal/charts/stock/toolbar/StockControl.ts +++ b/src/.internal/charts/stock/toolbar/StockControl.ts @@ -91,6 +91,8 @@ export class StockControl extends Entity { protected _afterNew() { super._afterNew(); + this.get("stockChart").controls.push(this); + // Inherit default themes from chart this._defaultThemes = this.get("stockChart")._defaultThemes; super._afterNewApplyThemes(); diff --git a/src/.internal/charts/stock/toolbar/StockIcons.ts b/src/.internal/charts/stock/toolbar/StockIcons.ts index d999254e..28689582 100644 --- a/src/.internal/charts/stock/toolbar/StockIcons.ts +++ b/src/.internal/charts/stock/toolbar/StockIcons.ts @@ -20,6 +20,7 @@ export class StockIcons { "Interval": { viewbox: "0 0 50 50", path: "M 3 10 L 3 48 M 3 10 L 43 10 M 13 10 L 13 36 M 23 10 L 23 36 M 43 10 L 43 48 M 33 10 L 33 36" }, "Comparison": { viewbox: "0 0 50 50", path: "M 25 10 L 25 40 M 10 25 L 41 25" }, "Settings": { viewbox: "0 0 50 50", path: "M49,25 L47.6,33.2 L41.3,32.6 L39.7,35.3 L43.4,40.4 L37,45.8 L32.6,41.3 L29.7,42.4 L29.2,48.6 L20.8,48.6 L20.3,42.4 L17.4,41.3 L13,45.8 L6.6,40.4 L10.3,35.3 L8.7,32.6 L2.4,33.2 L1,25 L7.1,23.4 L7.6,20.3 L2.4,16.8 L6.6,9.6 L12.3,12.3 L14.7,10.3 L13,4.2 L20.8,1.4 L23.4,7.1 L26.6,7.1 L29.2,1.4 L37,4.2 L35.3,10.3 L37.7,12.3 L43.4,9.6 L47.6,16.8 L42.4,20.3 L42.9,23.4 L49,25 M 17 25 A 1 1 0 0 0 33 25 A 1 1 0 0 0 17 25" }, + "Save": { viewbox: "0 0 50 50", path: "M47 47 47 15 35 3 3 3 3 47 47 47ZM13 3 13 13 33 13 33 3M9 42 9 25 39 25 39 42 9 42" }, // Chart types "Line Series": { viewbox: "0 0 50 50", path: "M 3 28 L 14 18 L 25 32 L 36 9 L 47 19" }, diff --git a/src/.internal/charts/xy/XYCursor.ts b/src/.internal/charts/xy/XYCursor.ts index 8cb46bf1..a64bee86 100644 --- a/src/.internal/charts/xy/XYCursor.ts +++ b/src/.internal/charts/xy/XYCursor.ts @@ -83,8 +83,9 @@ export interface IXYCursorSettings extends IContainerSettings { * Defines in which direction to look when searching for the nearest data * item to snap to. * - * Possible values: `"xy"` (default), `"x"`, and `"y"`. + * Possible values: `"xy"` (default), `"x"`, `"y"`, `"x!"`, `"y!"`. * + * @see {@link https://www.amcharts.com/docs/v5/charts/xy-chart/cursor/#snapping-to-series} for more info * @since 5.0.6 * @default "xy" */ diff --git a/src/.internal/charts/xy/axes/Axis.ts b/src/.internal/charts/xy/axes/Axis.ts index 8cff974d..ba109187 100644 --- a/src/.internal/charts/xy/axes/Axis.ts +++ b/src/.internal/charts/xy/axes/Axis.ts @@ -119,6 +119,13 @@ export interface IAxisSettings extends IComponentSetting */ zoomY?: boolean; + + /** + * @todo review + * You can prevent axis to be zoomed if this is false. + */ + zoomable?:boolean; + /** * A relative distance the axis is allowed to be zoomed/panned beyond its * actual scope. @@ -423,162 +430,164 @@ export abstract class Axis extends Component { * @return Zoom animation */ public zoom(start: number, end: number, duration?: number, priority?: "start" | "end"): Animation | Animation | undefined { - this._updateFinals(start, end); - - if (this.get("start") !== start || this.get("end") != end) { - let sAnimation = this._sAnimation; - let eAnimation = this._eAnimation; - - let maxDeviation = this.get("maxDeviation", 0.5) * Math.min(1, (end - start)); - - if (start < - maxDeviation) { - start = -maxDeviation; - } + if(this.get("zoomable", true)){ + this._updateFinals(start, end); - if (end > 1 + maxDeviation) { - end = 1 + maxDeviation; - } + if (this.get("start") !== start || this.get("end") != end) { + let sAnimation = this._sAnimation; + let eAnimation = this._eAnimation; - if (start > end) { - [start, end] = [end, start]; - } + let maxDeviation = this.get("maxDeviation", 0.5) * Math.min(1, (end - start)); - if (!$type.isNumber(duration)) { - duration = this.get("interpolationDuration", 0); - } + if (start < - maxDeviation) { + start = -maxDeviation; + } - if (!priority) { - priority = "end"; - } + if (end > 1 + maxDeviation) { + end = 1 + maxDeviation; + } - let maxZoomFactor = this.getPrivate("maxZoomFactor", this.get("maxZoomFactor", 100)); - let maxZoomFactorReal = maxZoomFactor; + if (start > end) { + [start, end] = [end, start]; + } - if (end === 1 && start !== 0) { - if (start < this.get("start")) { - priority = "start"; + if (!$type.isNumber(duration)) { + duration = this.get("interpolationDuration", 0); } - else { + + if (!priority) { priority = "end"; } - } - if (start === 0 && end !== 1) { - if (end > this.get("end")) { - priority = "end"; + let maxZoomFactor = this.getPrivate("maxZoomFactor", this.get("maxZoomFactor", 100)); + let maxZoomFactorReal = maxZoomFactor; + + if (end === 1 && start !== 0) { + if (start < this.get("start")) { + priority = "start"; + } + else { + priority = "end"; + } } - else { - priority = "start"; + + if (start === 0 && end !== 1) { + if (end > this.get("end")) { + priority = "end"; + } + else { + priority = "start"; + } } - } - let minZoomCount = this.get("minZoomCount"); - let maxZoomCount = this.get("maxZoomCount"); + let minZoomCount = this.get("minZoomCount"); + let maxZoomCount = this.get("maxZoomCount"); - if ($type.isNumber(minZoomCount)) { - maxZoomFactor = maxZoomFactorReal / minZoomCount; - } + if ($type.isNumber(minZoomCount)) { + maxZoomFactor = maxZoomFactorReal / minZoomCount; + } - let minZoomFactor: number = 1; + let minZoomFactor: number = 1; - if ($type.isNumber(maxZoomCount)) { - minZoomFactor = maxZoomFactorReal / maxZoomCount; - } + if ($type.isNumber(maxZoomCount)) { + minZoomFactor = maxZoomFactorReal / maxZoomCount; + } + + // most likely we are dragging left scrollbar grip here, so we tend to modify end + if (priority === "start") { + if (maxZoomCount > 0) { + // add to the end + if (1 / (end - start) < minZoomFactor) { + end = start + 1 / minZoomFactor; + } + } - // most likely we are dragging left scrollbar grip here, so we tend to modify end - if (priority === "start") { - if (maxZoomCount > 0) { // add to the end - if (1 / (end - start) < minZoomFactor) { - end = start + 1 / minZoomFactor; + if (1 / (end - start) > maxZoomFactor) { + end = start + 1 / maxZoomFactor; + } + //unless end is > 0 + if (end > 1 && end - start < 1 / maxZoomFactor) { + //end = 1; + start = end - 1 / maxZoomFactor; } } + // most likely we are dragging right, so we modify left + else { + if (maxZoomCount > 0) { + // add to the end + if (1 / (end - start) < minZoomFactor) { + start = end - 1 / minZoomFactor; + } + } - // add to the end - if (1 / (end - start) > maxZoomFactor) { - end = start + 1 / maxZoomFactor; - } - //unless end is > 0 - if (end > 1 && end - start < 1 / maxZoomFactor) { - //end = 1; - start = end - 1 / maxZoomFactor; - } - } - // most likely we are dragging right, so we modify left - else { - if (maxZoomCount > 0) { - // add to the end - if (1 / (end - start) < minZoomFactor) { - start = end - 1 / minZoomFactor; + // remove from start + if (1 / (end - start) > maxZoomFactor) { + start = end - 1 / maxZoomFactor; + } + if (start < 0 && end - start < 1 / maxZoomFactor) { + //start = 0; + end = start + 1 / maxZoomFactor; } } - // remove from start if (1 / (end - start) > maxZoomFactor) { - start = end - 1 / maxZoomFactor; - } - if (start < 0 && end - start < 1 / maxZoomFactor) { - //start = 0; end = start + 1 / maxZoomFactor; } - } - if (1 / (end - start) > maxZoomFactor) { - end = start + 1 / maxZoomFactor; - } - - if (1 / (end - start) > maxZoomFactor) { - start = end - 1 / maxZoomFactor; - } + if (1 / (end - start) > maxZoomFactor) { + start = end - 1 / maxZoomFactor; + } - if (maxZoomCount != null && minZoomCount != null && (start == this.get("start") && end == this.get("end"))) { - const chart = this.chart; - if (chart) { - chart._handleAxisSelection(this, true); + if (maxZoomCount != null && minZoomCount != null && (start == this.get("start") && end == this.get("end"))) { + const chart = this.chart; + if (chart) { + chart._handleAxisSelection(this, true); + } } - } - if (((sAnimation && sAnimation.playing && sAnimation.to == start) || this.get("start") == start) && ((eAnimation && eAnimation.playing && eAnimation.to == end) || this.get("end") == end)) { - return; - } + if (((sAnimation && sAnimation.playing && sAnimation.to == start) || this.get("start") == start) && ((eAnimation && eAnimation.playing && eAnimation.to == end) || this.get("end") == end)) { + return; + } - if (duration > 0) { - let easing = this.get("interpolationEasing"); - let sAnimation, eAnimation; - if (this.get("start") != start) { - sAnimation = this.animate({ key: "start", to: start, duration: duration, easing: easing }); - } - if (this.get("end") != end) { - eAnimation = this.animate({ key: "end", to: end, duration: duration, easing: easing }); - } + if (duration > 0) { + let easing = this.get("interpolationEasing"); + let sAnimation, eAnimation; + if (this.get("start") != start) { + sAnimation = this.animate({ key: "start", to: start, duration: duration, easing: easing }); + } + if (this.get("end") != end) { + eAnimation = this.animate({ key: "end", to: end, duration: duration, easing: easing }); + } - this._sAnimation = sAnimation; - this._eAnimation = eAnimation; + this._sAnimation = sAnimation; + this._eAnimation = eAnimation; - if (sAnimation) { - return sAnimation; + if (sAnimation) { + return sAnimation; + } + else if (eAnimation) { + return eAnimation; + } } - else if (eAnimation) { - return eAnimation; + else { + this.set("start", start); + this.set("end", end); + // otherwise bullets and line out of sync, as series is not redrawn + this._root.events.once("frameended", () => { + this._markDirtyKey("start"); + this._root._markDirty(); + }) } } else { - this.set("start", start); - this.set("end", end); - // otherwise bullets and line out of sync, as series is not redrawn - this._root.events.once("frameended", () => { - this._markDirtyKey("start"); - this._root._markDirty(); - }) - } - } - else { - if (this._sAnimation) { - this._sAnimation.stop(); - } - if (this._eAnimation) { - this._eAnimation.stop(); + if (this._sAnimation) { + this._sAnimation.stop(); + } + if (this._eAnimation) { + this._eAnimation.stop(); + } } } } diff --git a/src/.internal/charts/xy/axes/AxisRendererX.ts b/src/.internal/charts/xy/axes/AxisRendererX.ts index c10dd393..412828cc 100644 --- a/src/.internal/charts/xy/axes/AxisRendererX.ts +++ b/src/.internal/charts/xy/axes/AxisRendererX.ts @@ -228,7 +228,9 @@ export class AxisRendererX extends AxisRenderer { public processAxis() { super.processAxis(); const axis = this.axis; - axis.set("width", p100); + if (axis.get("width") == null) { + axis.set("width", p100); + }; const verticalLayout = this._root.verticalLayout; axis.set("layout", verticalLayout); axis.labelsContainer.set("width", p100); diff --git a/src/.internal/charts/xy/axes/DateAxis.ts b/src/.internal/charts/xy/axes/DateAxis.ts index 8913629e..2a743032 100644 --- a/src/.internal/charts/xy/axes/DateAxis.ts +++ b/src/.internal/charts/xy/axes/DateAxis.ts @@ -316,10 +316,6 @@ export class DateAxis extends ValueAxis { groupOriginals = true; } - const firstDay = this._root.locale.firstDayOfWeek; - const utc = this._root.utc; - const timezone = this._root.timezone; - $array.each(intervals, (interval) => { let previousTime = -Infinity; @@ -344,15 +340,16 @@ export class DateAxis extends ValueAxis { let intervalDuration = $time.getDuration(interval.timeUnit); let firstItem = dataItems[0]; - let firstDate: Date; + let firstTime: any; if (firstItem) { - firstDate = new Date(dataItems[0].get(key as any)); + firstTime = dataItems[0].get(key as any); } let prevNewDataItem: DataItem | undefined; $array.each(dataItems, (dataItem) => { let time = dataItem.get(key as any); - let roundedTime = $time.round(new Date(time), interval.timeUnit, interval.count, firstDay, utc, firstDate, timezone).getTime(); + //let roundedTime = $time.round(new Date(time), interval.timeUnit, interval.count, firstDay, utc, firstDate, timezone).getTime(); + let roundedTime = $time.roun(time, interval.timeUnit, interval.count, this._root, firstTime); let dataContext: any; if (previousTime < roundedTime - intervalDuration / 24) { @@ -672,7 +669,7 @@ export class DateAxis extends ValueAxis { minorGridInterval = { timeUnit: timeUnit, count: count }; } if (timeUnit == "week") { - if(this.getPrivate("baseInterval")?.timeUnit != "week"){ + if (this.getPrivate("baseInterval")?.timeUnit != "week") { minorGridInterval = { timeUnit: "day", count: 1 }; } } @@ -684,6 +681,7 @@ export class DateAxis extends ValueAxis { const max = this.getPrivate("max"); if ($type.isNumber(min) && $type.isNumber(max)) { + const root = this._root; const selectionMin = Math.round(this.getPrivate("selectionMin")! as number); const selectionMax = Math.round(this.getPrivate("selectionMax")! as number); const renderer = this.get("renderer"); @@ -703,11 +701,11 @@ export class DateAxis extends ValueAxis { this._intervalDuration = intervalDuration; const nextGridUnit = $time.getNextUnit(gridInterval.timeUnit); - const firstDay = this._root.locale.firstDayOfWeek; - const utc = this._root.utc; - const timezone = this._root.timezone; + const utc = root.utc; + const timezone = root.timezone; - value = $time.round(new Date(selectionMin - intervalDuration), gridInterval.timeUnit, gridInterval.count, firstDay, utc, new Date(min), timezone).getTime(); + //value = $time.round(new Date(selectionMin - intervalDuration), gridInterval.timeUnit, gridInterval.count, firstDay, utc, new Date(min), timezone).getTime(); + value = $time.roun(selectionMin - intervalDuration, gridInterval.timeUnit, gridInterval.count, root, min); let previousValue = value - intervalDuration; let format: string | Intl.DateTimeFormatOptions; const formats = this.get("dateFormats")!; @@ -745,7 +743,8 @@ export class DateAxis extends ValueAxis { dataItem.setRaw("labelEndValue", undefined); let endValue = value + $time.getDuration(gridInterval.timeUnit, gridInterval.count * this._getM(gridInterval.timeUnit)); - endValue = $time.round(new Date(endValue), gridInterval.timeUnit, 1, firstDay, utc, undefined, timezone).getTime(); + //endValue = $time.round(new Date(endValue), gridInterval.timeUnit, 1, firstDay, utc, undefined, timezone).getTime(); + endValue = $time.roun(endValue, gridInterval.timeUnit, 1, root); dataItem.setRaw("endValue", endValue); @@ -762,7 +761,7 @@ export class DateAxis extends ValueAxis { const label = dataItem.get("label"); if (label) { - label.set("text", this._root.dateFormatter.format(date, format!)); + label.set("text", root.dateFormatter.format(date, format!)); } let count = gridInterval.count; @@ -779,7 +778,8 @@ export class DateAxis extends ValueAxis { } let labelEndValue = value + $time.getDuration(timeUnit, this._getM(timeUnit)); - labelEndValue = $time.round(new Date(labelEndValue), timeUnit, 1, firstDay, utc, undefined, timezone).getTime(); + //labelEndValue = $time.round(new Date(labelEndValue), timeUnit, 1, firstDay, utc, undefined, timezone).getTime(); + labelEndValue = $time.roun(labelEndValue, timeUnit, 1, root); dataItem.setRaw("labelEndValue", labelEndValue); } @@ -790,7 +790,13 @@ export class DateAxis extends ValueAxis { // min grid if (minorGridInterval) { - let minorValue = $time.round(new Date(previousValue + minorDuration * this._getM(minorGridInterval.timeUnit)), minorGridInterval.timeUnit, minorGridInterval.count, firstDay, utc, new Date(previousValue), timezone).getTime(); + const minorTimeUnit = minorGridInterval.timeUnit; + const minorCount = minorGridInterval.count; + const mmm = this._getM(minorTimeUnit); + + //let minorValue = $time.round(new Date(previousValue + minorDuration * this._getM(minorGridInterval.timeUnit)), minorGridInterval.timeUnit, minorGridInterval.count, firstDay, utc, new Date(previousValue), timezone).getTime(); + let minorValue = $time.roun(previousValue + minorDuration * mmm, minorTimeUnit, minorCount, root, previousValue); + let previousMinorValue: number | undefined; let minorFormats = this.get("minorDateFormats", this.get("dateFormats"))!; @@ -811,20 +817,21 @@ export class DateAxis extends ValueAxis { minorDataItem.setRaw("value", minorValue); - let minorEndValue = minorValue + $time.getDuration(minorGridInterval.timeUnit, minorGridInterval.count * this._getM(minorGridInterval.timeUnit)); - minorEndValue = $time.round(new Date(minorEndValue), minorGridInterval.timeUnit, 1, firstDay, utc, undefined, timezone).getTime(); + let minorEndValue = minorValue + $time.getDuration(minorTimeUnit, minorCount * mmm); + //minorEndValue = $time.round(new Date(minorEndValue), minorGridInterval.timeUnit, 1, firstDay, utc, undefined, timezone).getTime(); + minorEndValue = $time.roun(minorEndValue, minorTimeUnit, 1, root); minorDataItem.setRaw("endValue", minorEndValue); let date = new Date(minorValue); - format = minorFormats[minorGridInterval.timeUnit]; + format = minorFormats[minorTimeUnit]; const minorLabel = minorDataItem.get("label"); if (minorLabel) { if (minorLabelsEnabled) { - minorLabel.set("text", this._root.dateFormatter.format(date, format!)); + minorLabel.set("text", root.dateFormatter.format(date, format!)); } else { minorLabel.setPrivate("visible", false); @@ -879,25 +886,24 @@ export class DateAxis extends ValueAxis { protected _fixMin(min: number) { const baseInterval = this.getPrivate("baseInterval"); - const firstDay = this._root.locale.firstDayOfWeek; - const timezone = this._root.timezone; - const utc = this._root.utc; const timeUnit = baseInterval.timeUnit; - let startTime = $time.round(new Date(min), timeUnit, baseInterval.count, firstDay, utc, undefined, timezone).getTime(); + //let startTime = $time.round(new Date(min), timeUnit, baseInterval.count, firstDay, utc, undefined, timezone).getTime(); + let startTime = $time.roun(min, timeUnit, baseInterval.count, this._root); + let endTime = startTime + $time.getDuration(timeUnit, baseInterval.count * this._getM(timeUnit)) - endTime = $time.round(new Date(endTime), timeUnit, 1, firstDay, utc, undefined, timezone).getTime(); + //endTime = $time.round(new Date(endTime), timeUnit, 1, firstDay, utc, undefined, timezone).getTime(); + endTime = $time.roun(endTime, timeUnit, 1, this._root); return startTime + (endTime - startTime) * this.get("startLocation", 0); } protected _fixMax(max: number) { const baseInterval = this.getPrivate("baseInterval"); - const firstDay = this._root.locale.firstDayOfWeek; - const timezone = this._root.timezone; - const utc = this._root.utc; const timeUnit = baseInterval.timeUnit; - let startTime = $time.round(new Date(max), timeUnit, baseInterval.count, firstDay, utc, undefined, timezone).getTime(); + //let startTime = $time.round(new Date(max), timeUnit, baseInterval.count, firstDay, utc, undefined, timezone).getTime(); + let startTime = $time.roun(max, timeUnit, baseInterval.count, this._root); let endTime = startTime + $time.getDuration(timeUnit, baseInterval.count * this._getM(timeUnit)) - endTime = $time.round(new Date(endTime), timeUnit, 1, firstDay, utc, undefined, timezone).getTime(); + //endTime = $time.round(new Date(endTime), timeUnit, 1, firstDay, utc, undefined, timezone).getTime(); + endTime = $time.roun(endTime, timeUnit, 1, this._root); return startTime + (endTime - startTime) * this.get("endLocation", 1); } @@ -949,14 +955,13 @@ export class DateAxis extends ValueAxis { } else { - const firstDay = this._root.locale.firstDayOfWeek; - const utc = this._root.utc; - const timezone = this._root.timezone const timeUnit = baseInterval.timeUnit; const count = baseInterval.count; - startTime = $time.round(new Date(value), timeUnit, count, firstDay, utc, undefined, timezone).getTime(); + //startTime = $time.round(new Date(value), timeUnit, count, firstDay, utc, undefined, timezone).getTime(); + startTime = $time.roun(value, timeUnit, count, this._root); endTime = startTime + $time.getDuration(timeUnit, count * this._getM(timeUnit)); - endTime = $time.round(new Date(endTime), timeUnit, 1, firstDay, utc, undefined, timezone).getTime(); + //endTime = $time.round(new Date(endTime), timeUnit, 1, firstDay, utc, undefined, timezone).getTime(); + endTime = $time.roun(endTime, timeUnit, 1, this._root); dataItem.open![field] = startTime; dataItem.close![field] = endTime; @@ -1042,11 +1047,13 @@ export class DateAxis extends ValueAxis { const timezone = this._root.timezone; const count = baseInterval.count; - value = $time.round(new Date(value), timeUnit, count, firstDay, utc, new Date(this.getPrivate("min", 0)), timezone).getTime(); + //value = $time.round(new Date(value), timeUnit, count, firstDay, utc, new Date(this.getPrivate("min", 0)), timezone).getTime(); + value = $time.roun(value, timeUnit, count, this._root, this.getPrivate("min", 0)); let duration = $time.getDateIntervalDuration(baseInterval, new Date(value), firstDay, utc, timezone); if (timezone) { - value = $time.round(new Date(value + this.baseDuration() * 0.05), timeUnit, count, firstDay, utc, new Date(this.getPrivate("min", 0)), timezone).getTime(); + //value = $time.round(new Date(value + this.baseDuration() * 0.05), timeUnit, count, firstDay, utc, new Date(this.getPrivate("min", 0)), timezone).getTime(); + value = $time.roun(value + this.baseDuration() * 0.05, timeUnit, count, this._root, this.getPrivate("min", 0)); duration = $time.getDateIntervalDuration(baseInterval, new Date(value + duration * location), firstDay, utc, timezone); } diff --git a/src/.internal/charts/xy/axes/GaplessDateAxis.ts b/src/.internal/charts/xy/axes/GaplessDateAxis.ts index 0972f509..5315468e 100644 --- a/src/.internal/charts/xy/axes/GaplessDateAxis.ts +++ b/src/.internal/charts/xy/axes/GaplessDateAxis.ts @@ -55,11 +55,20 @@ export class GaplessDateAxis extends DateAxis { super._afterNew(); } - protected _dates: Array = []; + public _dates: Array = []; + public _customDates?: Array; + + + public _getDates(): Array { + if (this._customDates) { + return this._customDates; + } + return this._dates; + } protected _updateDates(date: number, series: XYSeries) { if (!series.get("ignoreMinMax")) { - const dates = this._dates; + const dates = this._getDates(); const result = $array.getSortedIndex(dates, (x) => $order.compare(x, date)); if (!result.found) { $array.insertIndex(dates, result.index, date); @@ -68,57 +77,57 @@ export class GaplessDateAxis extends DateAxis { } public _updateAllDates() { - const dates = this._dates; - dates.length = 0; + if (!this._customDates) { + const dates = this._dates; + dates.length = 0; - $array.each(this.series, (series) => { - let field = "valueX"; - if (series.get("yAxis") == this) { - field = "valueY" - } - $array.each(series.dataItems, (dataItem) => { - let value = dataItem.get(field as any); - if ($type.isNumber(value)) { - if (dataItem.open) { - this._updateDates(dataItem.open![field], series); - } + $array.each(this.series, (series) => { + let field = "valueX"; + if (series.get("yAxis") == this) { + field = "valueY" } + $array.each(series.dataItems, (dataItem) => { + let value = dataItem.get(field as any); + if ($type.isNumber(value)) { + if (dataItem.open) { + this._updateDates(dataItem.open![field], series); + } + } + }) }) - }) - const extraMax = this.get("extraMax", 0); - const extraMin = this.get("extraMin", 0); + const extraMax = this.get("extraMax", 0); + const extraMin = this.get("extraMin", 0); - const len = dates.length; + let len = dates.length; - const baseInterval = this.getPrivate("baseInterval"); - const timeUnit = baseInterval.timeUnit; - - const firstDay = this._root.locale.firstDayOfWeek; - const utc = this._root.utc; - const timezone = this._root.timezone; - - if (extraMax > 0) { - const extra = len * extraMax; - let time = dates[len - 1]; - if ($type.isNumber(time)) { - for (let i = len - 1; i < len + extra; i++) { - time += $time.getDuration(timeUnit, baseInterval.count * this._getM(timeUnit)); - time = $time.round(new Date(time), timeUnit, baseInterval.count, firstDay, utc, undefined, timezone).getTime(); - dates.push(time); + const baseInterval = this.getPrivate("baseInterval"); + const baseCount = baseInterval.count; + const timeUnit = baseInterval.timeUnit; + + if (extraMax > 0) { + const extra = len * extraMax; + let time = dates[len - 1]; + if ($type.isNumber(time)) { + for (let i = len - 1; i < len + extra; i++) { + time += $time.getDuration(timeUnit, baseCount * this._getM(timeUnit)); + //time = $time.round(new Date(time), timeUnit, baseInterval.count, firstDay, utc, undefined, timezone).getTime(); + time = $time.roun(time, timeUnit, baseCount, this._root); + dates.push(time); + } } } - } - - if (extraMin > 0) { - const extra = len * extraMin; - let time = dates[0]; - if ($type.isNumber(time)) { - for (let i = 0; i < extra; i++) { - time -= $time.getDuration(timeUnit, baseInterval.count); - time = $time.round(new Date(time), timeUnit, baseInterval.count, firstDay, utc, undefined, timezone).getTime(); - dates.unshift(time); + if (extraMin > 0) { + const extra = len * extraMin; + let time = dates[0]; + if ($type.isNumber(time)) { + for (let i = 0; i < extra; i++) { + time -= $time.getDuration(timeUnit, baseCount); + //time = $time.round(new Date(time), timeUnit, baseCount, firstDay, utc, undefined, timezone).getTime(); + time = $time.roun(time, timeUnit, baseCount, this._root); + dates.unshift(time); + } } } } @@ -131,7 +140,7 @@ export class GaplessDateAxis extends DateAxis { * @return Relative position */ public valueToPosition(value: number): number { - const dates = this._dates; + const dates = this._getDates(); const startLocation = this.get("startLocation", 0); const endLocation = this.get("endLocation", 1); const len = dates.length - startLocation - (1 - endLocation); @@ -167,7 +176,7 @@ export class GaplessDateAxis extends DateAxis { * @return Index */ public valueToIndex(value: number): number { - const dates = this._dates; + const dates = this._getDates(); const result = $array.getSortedIndex(dates, (x) => $order.compare(x, value)); let index = result.index; @@ -193,7 +202,8 @@ export class GaplessDateAxis extends DateAxis { public positionToValue(position: number): number { const startLocation = this.get("startLocation", 0); const endLocation = this.get("endLocation", 1); - let len = Math.round(this._dates.length - startLocation - (1 - endLocation)); + const dates = this._getDates(); + let len = Math.round(dates.length - startLocation - (1 - endLocation)); let index = position * len; let findex = Math.floor(index); if (findex < 0) { @@ -204,11 +214,38 @@ export class GaplessDateAxis extends DateAxis { findex = len - 1 } - return this._dates[findex] + (index - findex + startLocation) * this.baseDuration(); + return dates[findex] + (index - findex + startLocation) * this.baseDuration(); } protected _fixZoomFactor() { - this.setPrivateRaw("maxZoomFactor", this._dates.length - this.get("startLocation", 0) - (1 - this.get("endLocation", 1))); + this.setPrivateRaw("maxZoomFactor", this._getDates().length - this.get("startLocation", 0) - (1 - this.get("endLocation", 1))); + } + + /** + * Zooms the axis to specific `start` and `end` dates. + * + * Optional `duration` specifies duration of zoom animation in milliseconds. + * + * @param start Start Date + * @param end End Date + * @param duration Duration in milliseconds + */ + + public zoomToDates(start: Date, end: Date, duration?: number) { + const dates = this._getDates(); + const len = dates.length; + let result = $array.getSortedIndex(dates, (x) => $order.compare(x, start.getTime())); + + let startValue = dates[Math.min(result.index, len - 1)]; + + result = $array.getSortedIndex(dates, (x) => $order.compare(x, end.getTime())); + let endValue = dates[result.index]; + + if(result.index >= len){ + endValue = dates[len - 1] + this.baseDuration(); + } + + this.zoomToValues(startValue, endValue, duration); } /** @@ -240,10 +277,10 @@ export class GaplessDateAxis extends DateAxis { this._updateAllDates(); } - const firstDay = this._root.locale.firstDayOfWeek; - const utc = this._root.utc; - const timezone = this._root.timezone; - const dates = this._dates; + const root = this._root; + const utc = root.utc; + const timezone = root.timezone; + const dates = this._getDates(); const renderer = this.get("renderer"); const len = dates.length; const baseDuration = this.baseDuration(); @@ -291,12 +328,14 @@ export class GaplessDateAxis extends DateAxis { const timeUnit = gridInterval.timeUnit; const formats = this.get("dateFormats")!; - let firstDate = new Date(); - if (this._dates[0]) { - firstDate = new Date(this._dates[0]); + let firstTime = Date.now(); + + if (dates[0]) { + firstTime = dates[0]; } - let value = $time.round(new Date(this.getPrivate("selectionMin", 0)), timeUnit, gridInterval.count, firstDay, utc, firstDate, timezone).getTime(); + //let value = $time.round(new Date(this.getPrivate("selectionMin", 0)), timeUnit, gridInterval.count, firstDay, utc, firstDate, timezone).getTime(); + let value = $time.roun(this.getPrivate("selectionMin", 0), timeUnit, gridInterval.count, root, firstTime); const minorLabelsEnabled = renderer.get("minorLabelsEnabled"); const minorGridEnabled = renderer.get("minorGridEnabled", minorLabelsEnabled); @@ -365,7 +404,7 @@ export class GaplessDateAxis extends DateAxis { const label = dataItem.get("label"); if (label) { - label.set("text", this._root.dateFormatter.format(date, format!)); + label.set("text", root.dateFormatter.format(date, format!)); } this._toggleDataItem(dataItem, true); @@ -384,11 +423,12 @@ export class GaplessDateAxis extends DateAxis { } if (count > 1 || gridInterval.timeUnit == "week") { - let labelEndValue = $time.round(new Date(value), timeUnit2, 1, firstDay, utc, undefined, timezone).getTime() + $time.getDuration(timeUnit2, this._getM(timeUnit2)); + //let labelEndValue = $time.round(new Date(value), timeUnit2, 1, firstDay, utc, undefined, timezone).getTime() + $time.getDuration(timeUnit2, this._getM(timeUnit2)); + let labelEndValue = $time.roun(value, timeUnit2, 1, root) + $time.getDuration(timeUnit2, this._getM(timeUnit2)); let index = this.valueToIndex(labelEndValue) - labelEndValue = this._dates[index]; + labelEndValue = dates[index]; if (labelEndValue == value) { - let next = this._dates[index + 1]; + let next = dates[index + 1]; if (next) { labelEndValue = next; } @@ -466,16 +506,14 @@ export class GaplessDateAxis extends DateAxis { protected _addMinorGrid(startValue: number, endValue: number, minorDuration: number, gridInterval: ITimeInterval) { const minorFormats = this.get("minorDateFormats", this.get("dateFormats"))!; const mTimeUnit = gridInterval.timeUnit; - const firstDay = this._root.locale.firstDayOfWeek; - const utc = this._root.utc; - const timezone = this._root.timezone; - let value = startValue + $time.getDuration(mTimeUnit, this._getM(mTimeUnit)); - value = $time.round(new Date(value), mTimeUnit, 1, firstDay, utc, undefined, timezone).getTime(); + //value = $time.round(new Date(value), mTimeUnit, 1, firstDay, utc, undefined, timezone).getTime(); + value = $time.roun(value, mTimeUnit, 1, this._root); let maxValue = endValue - minorDuration * 0.5; let minorSelectedItems: Array = this._getIndexes(value, maxValue, gridInterval, value); + const dates = this._getDates(); $array.each(minorSelectedItems, (index) => { let minorDataItem: DataItem; @@ -488,7 +526,7 @@ export class GaplessDateAxis extends DateAxis { minorDataItem = this.minorDataItems[this._m]; } - value = this._dates[index]; + value = dates[index]; minorDataItem.setRaw("value", value); minorDataItem.setRaw("endValue", value + minorDuration); minorDataItem.setRaw("index", index); @@ -518,17 +556,18 @@ export class GaplessDateAxis extends DateAxis { const items: Array = []; const timeUnit = interval.timeUnit; const count = interval.count; + const mmm = this._getM(timeUnit); const baseInterval = this.getPrivate("baseInterval"); - const firstDay = this._root.locale.firstDayOfWeek; - const utc = this._root.utc; - const timezone = this._root.timezone; + const root = this._root; + const dates = this._getDates(); let c = count - 1; let previousValue = -Infinity; - let duration = $time.getDuration(timeUnit, this._getM(timeUnit)); - let fullDuration = $time.getDuration(timeUnit, count * this._getM(timeUnit)); + + let duration = $time.getDuration(timeUnit, mmm); + let fullDuration = $time.getDuration(timeUnit, count * mmm); let originalValue = value; if (timeUnit == "day") { @@ -536,10 +575,11 @@ export class GaplessDateAxis extends DateAxis { } while (value <= maxValue) { - value = $time.round(new Date(value), timeUnit, count, firstDay, utc, undefined, timezone).getTime(); + //value = $time.round(new Date(value), timeUnit, count, firstDay, utc, undefined, timezone).getTime(); + value = $time.roun(value, timeUnit, count, root); let index = this.valueToIndex(value); - let realValue = this._dates[index]; + let realValue = dates[index]; if (timeUnit == "day" && baseInterval.timeUnit == "day") { if (this._hasDate(value)) { @@ -553,12 +593,13 @@ export class GaplessDateAxis extends DateAxis { c = 0; } value += duration; - value = $time.round(new Date(value), timeUnit, 1, firstDay, utc, undefined, timezone).getTime(); + //value = $time.round(new Date(value), timeUnit, 1, firstDay, utc, undefined, timezone).getTime(); + value = $time.roun(value, timeUnit, 1, root); } else { if (realValue < value) { - for (let i = index, len = this._dates.length; i < len; i++) { - realValue = this._dates[i]; + for (let i = index, len = dates.length; i < len; i++) { + realValue = dates[i]; if (realValue >= value) { index = i; break; @@ -569,12 +610,14 @@ export class GaplessDateAxis extends DateAxis { $array.move(items, index); value += fullDuration; - value = $time.round(new Date(value), timeUnit, count, firstDay, utc, undefined, timezone).getTime(); + //value = $time.round(new Date(value), timeUnit, count, firstDay, utc, undefined, timezone).getTime(); + value = $time.roun(value, timeUnit, count, root); } if (value == previousValue) { value += fullDuration + duration; - value = $time.round(new Date(value), timeUnit, count, firstDay, utc, undefined, timezone).getTime(); + //value = $time.round(new Date(value), timeUnit, count, firstDay, utc, undefined, timezone).getTime(); + value = $time.roun(value, timeUnit, count, root); } if (value == previousValue) { break; @@ -587,7 +630,7 @@ export class GaplessDateAxis extends DateAxis { } protected _hasDate(time: number) { - const result = $array.getSortedIndex(this._dates, (date) => { + const result = $array.getSortedIndex(this._getDates(), (date) => { return $order.compareNumber(date, time); }); diff --git a/src/.internal/charts/xy/series/XYSeries.ts b/src/.internal/charts/xy/series/XYSeries.ts index fc307d4d..770d2cd6 100644 --- a/src/.internal/charts/xy/series/XYSeries.ts +++ b/src/.internal/charts/xy/series/XYSeries.ts @@ -812,6 +812,8 @@ export interface IXYSeriesPrivate extends ISeriesPrivate { highValueYCloseSelection?: number; outOfSelection?: boolean; + + doNotUpdateLegend?:boolean; } @@ -2098,8 +2100,10 @@ export abstract class XYSeries extends Series { * @param dataItem Data item */ public showDataItemTooltip(dataItem: DataItem | undefined) { - this.updateLegendMarker(dataItem); - this.updateLegendValue(dataItem); + if(!this.getPrivate("doNotUpdateLegend")){ + this.updateLegendMarker(dataItem); + this.updateLegendValue(dataItem); + } const tooltip = this.get("tooltip"); diff --git a/src/.internal/core/Classes.ts b/src/.internal/core/Classes.ts index c2638c89..1b8b694f 100644 --- a/src/.internal/core/Classes.ts +++ b/src/.internal/core/Classes.ts @@ -53,6 +53,7 @@ import type { CommodityChannelIndex } from "./../charts/stock/indicators/Commodi import type { ComparisonControl } from "./../charts/stock/toolbar/ComparisonControl.js"; import type { Component } from "./render/Component.js"; import type { Container } from "./render/Container.js"; +import type { DataSaveControl } from "./../charts/stock/toolbar/DataSaveControl.js"; import type { DateAxis } from "./../charts/xy/axes/DateAxis.js"; import type { DateRangeSelector } from "./../charts/stock/toolbar/DateRangeSelector.js"; import type { DisparityIndex } from "./../charts/stock/indicators/DisparityIndex.js"; @@ -209,6 +210,7 @@ import type { Venn } from "./../charts/venn/Venn.js"; import type { VerticalLayout } from "./render/VerticalLayout.js"; import type { VerticalLineSeries } from "./../charts/stock/drawing/VerticalLineSeries.js"; import type { Volume } from "./../charts/stock/indicators/Volume.js"; +import type { VolumeProfile } from "./../charts/stock/indicators/VolumeProfile.js"; import type { VoronoiTreemap } from "./../charts/hierarchy/VoronoiTreemap.js"; import type { WilliamsR } from "./../charts/stock/indicators/WilliamsR.js"; import type { WordCloud } from "./../charts/wordcloud/WordCloud.js"; @@ -270,6 +272,7 @@ export interface IClasses { "ComparisonControl": ComparisonControl; "Component": Component; "Container": Container; + "DataSaveControl": DataSaveControl; "DateAxis": DateAxis; "DateRangeSelector": DateRangeSelector; "DisparityIndex": DisparityIndex; @@ -426,6 +429,7 @@ export interface IClasses { "VerticalLayout": VerticalLayout; "VerticalLineSeries": VerticalLineSeries; "Volume": Volume; + "VolumeProfile": VolumeProfile; "VoronoiTreemap": VoronoiTreemap; "WilliamsR": WilliamsR; "WordCloud": WordCloud; diff --git a/src/.internal/core/Registry.ts b/src/.internal/core/Registry.ts index 88499b2d..0801c267 100644 --- a/src/.internal/core/Registry.ts +++ b/src/.internal/core/Registry.ts @@ -6,7 +6,7 @@ export class Registry { /** * Currently running version of amCharts. */ - readonly version: string = "5.6.2"; + readonly version: string = "5.7.0"; /** * List of applied licenses. diff --git a/src/.internal/core/render/Series.ts b/src/.internal/core/render/Series.ts index 779d0cf5..836802cb 100644 --- a/src/.internal/core/render/Series.ts +++ b/src/.internal/core/render/Series.ts @@ -431,6 +431,9 @@ export abstract class Series extends Component { if (!this._aggregatesCalculated) { this._calculateAggregates(0, this.dataItems.length); this._aggregatesCalculated = true; + if(startIndex != 0){ + this._psi = undefined; + } } } diff --git a/src/.internal/core/render/SpriteResizer.ts b/src/.internal/core/render/SpriteResizer.ts index ab60ae75..c97bb1ff 100644 --- a/src/.internal/core/render/SpriteResizer.ts +++ b/src/.internal/core/render/SpriteResizer.ts @@ -1,12 +1,12 @@ import type { Sprite, ISpritePointerEvent } from "./Sprite"; import type { IDisposer } from "../util/Disposer"; +import type { Template } from "../util/Template"; import { Container, IContainerPrivate, IContainerSettings, IContainerEvents } from "./Container"; import { p50, Percent } from "../util/Percent"; import { RoundedRectangle } from "./RoundedRectangle"; import { Rectangle } from "./Rectangle"; import { color } from "../util/Color"; -import type { Template } from "../util/Template"; import * as $math from "../util/Math"; diff --git a/src/.internal/core/render/backend/CanvasRenderer.ts b/src/.internal/core/render/backend/CanvasRenderer.ts index 3f5e851b..40b04414 100644 --- a/src/.internal/core/render/backend/CanvasRenderer.ts +++ b/src/.internal/core/render/backend/CanvasRenderer.ts @@ -3543,6 +3543,12 @@ export class CanvasRenderer extends ArrayDisposer implements IRenderer, IDispose return hit; } + getObjectAtPoint(point: IPoint): CanvasDisplayObject | undefined { + const bbox = this._adjustBoundingBox(this.view.getBoundingClientRect()); + + return this._getHitTarget(point, bbox, this._layerDom) || undefined; + } + _withEvents(key: Key, f: (events: IEvents) => void): void { const events = this._events[key] as IEvents | undefined; diff --git a/src/.internal/core/render/backend/Renderer.ts b/src/.internal/core/render/backend/Renderer.ts index 8117db4d..9bc12416 100644 --- a/src/.internal/core/render/backend/Renderer.ts +++ b/src/.internal/core/render/backend/Renderer.ts @@ -286,6 +286,7 @@ export interface IRenderer extends IDisposer { view: HTMLElement; getEvent(originalEvent: A, adjustPoint?: boolean): IRendererEvent; + getObjectAtPoint(point: IPoint): IDisplayObject | undefined; } export interface ICanvasOptions { diff --git a/src/.internal/core/util/Time.ts b/src/.internal/core/util/Time.ts index e3acee7b..6a99e28a 100644 --- a/src/.internal/core/util/Time.ts +++ b/src/.internal/core/util/Time.ts @@ -7,6 +7,7 @@ import * as $type from "./Type"; import * as $utils from "./Utils"; import type { Timezone } from "./Timezone"; +import type { Root } from "../Root"; export type TimeUnit = "millisecond" | "second" | "minute" | "hour" | "day" | "week" | "month" | "year"; @@ -341,6 +342,17 @@ export function add(date: Date, unit: TimeUnit, count: number, utc?: boolean, ti return date; } +/** + * @ignore + */ +export function roun(time: number, unit: TimeUnit, count: number, root: Root, firstTime?: number): number { + let firstDate; + if (firstTime != null) { + firstDate = new Date(firstTime); + } + return round(new Date(time), unit, count, root.locale.firstDayOfWeek, root.utc, firstDate, root.timezone).getTime(); +} + /** * "Rounds" the date to specific time unit. @@ -566,7 +578,7 @@ export function round(date: Date, unit: TimeUnit, count: number, firstDateOfWeek } day = 1; hour = 0; - minute = offsetDif; + minute = offsetDif; second = 0; millisecond = 0; break; diff --git a/src/.internal/core/util/Utils.ts b/src/.internal/core/util/Utils.ts index ff3fbce6..fbd3b788 100644 --- a/src/.internal/core/util/Utils.ts +++ b/src/.internal/core/util/Utils.ts @@ -1226,32 +1226,6 @@ export function alternativeColor(color: iRGB, lightAlternative: iRGB = { r: 255, return isLight(color) ? dark : light; } -/** - * @ignore - * @deprecated - */ -// export function unshiftThemeClass(settings: any, themeClass: string) { -// let themeClasses = settings.themeClasses; -// if (!themeClasses) { -// themeClasses = []; -// } -// themeClasses.unshift(themeClass); -// settings.themeClasses = themeClasses; -// } - -/** - * @ignore - * @deprecated - */ -// export function pushThemeClass(settings: any, themeClass: string) { -// let themeClasses = settings.themeClasses; -// if (!themeClasses) { -// themeClasses = []; -// } -// themeClasses.push(themeClass); -// settings.themeClasses = themeClasses; -// } - /** * @ignore */ diff --git a/src/.internal/plugins/json/Classes-script.ts b/src/.internal/plugins/json/Classes-script.ts index c4578a93..9c500905 100644 --- a/src/.internal/plugins/json/Classes-script.ts +++ b/src/.internal/plugins/json/Classes-script.ts @@ -53,6 +53,7 @@ import type { CommodityChannelIndex } from "./../../../stock"; import type { ComparisonControl } from "./../../../stock"; import type { Component } from "./../../../index"; import type { Container } from "./../../../index"; +import type { DataSaveControl } from "./../../../stock"; import type { DateAxis } from "./../../../xy"; import type { DateRangeSelector } from "./../../../stock"; import type { DisparityIndex } from "./../../../stock"; @@ -209,6 +210,7 @@ import type { Venn } from "./../../../venn"; import type { VerticalLayout } from "./../../../index"; import type { VerticalLineSeries } from "./../../../stock"; import type { Volume } from "./../../../stock"; +import type { VolumeProfile } from "./../../../stock"; import type { VoronoiTreemap } from "./../../../hierarchy"; import type { WilliamsR } from "./../../../stock"; import type { WordCloud } from "./../../../wc"; @@ -270,6 +272,7 @@ export interface IClasses { "ComparisonControl": () => Promise; "Component": () => Promise; "Container": () => Promise; + "DataSaveControl": () => Promise; "DateAxis": () => Promise; "DateRangeSelector": () => Promise; "DisparityIndex": () => Promise; @@ -426,6 +429,7 @@ export interface IClasses { "VerticalLayout": () => Promise; "VerticalLineSeries": () => Promise; "Volume": () => Promise; + "VolumeProfile": () => Promise; "VoronoiTreemap": () => Promise; "WilliamsR": () => Promise; "WordCloud": () => Promise; @@ -488,6 +492,7 @@ const classes: IClasses = { "ComparisonControl": () => import(/* webpackExports: "ComparisonControl", webpackMode: "weak" */ "./../../../stock").then((m) => m.ComparisonControl), "Component": () => import(/* webpackExports: "Component", webpackMode: "weak" */ "./../../../index").then((m) => m.Component), "Container": () => import(/* webpackExports: "Container", webpackMode: "weak" */ "./../../../index").then((m) => m.Container), + "DataSaveControl": () => import(/* webpackExports: "DataSaveControl", webpackMode: "weak" */ "./../../../stock").then((m) => m.DataSaveControl), "DateAxis": () => import(/* webpackExports: "DateAxis", webpackMode: "weak" */ "./../../../xy").then((m) => m.DateAxis), "DateRangeSelector": () => import(/* webpackExports: "DateRangeSelector", webpackMode: "weak" */ "./../../../stock").then((m) => m.DateRangeSelector), "DisparityIndex": () => import(/* webpackExports: "DisparityIndex", webpackMode: "weak" */ "./../../../stock").then((m) => m.DisparityIndex), @@ -644,6 +649,7 @@ const classes: IClasses = { "VerticalLayout": () => import(/* webpackExports: "VerticalLayout", webpackMode: "weak" */ "./../../../index").then((m) => m.VerticalLayout), "VerticalLineSeries": () => import(/* webpackExports: "VerticalLineSeries", webpackMode: "weak" */ "./../../../stock").then((m) => m.VerticalLineSeries), "Volume": () => import(/* webpackExports: "Volume", webpackMode: "weak" */ "./../../../stock").then((m) => m.Volume), + "VolumeProfile": () => import(/* webpackExports: "VolumeProfile", webpackMode: "weak" */ "./../../../stock").then((m) => m.VolumeProfile), "VoronoiTreemap": () => import(/* webpackExports: "VoronoiTreemap", webpackMode: "weak" */ "./../../../hierarchy").then((m) => m.VoronoiTreemap), "WilliamsR": () => import(/* webpackExports: "WilliamsR", webpackMode: "weak" */ "./../../../stock").then((m) => m.WilliamsR), "WordCloud": () => import(/* webpackExports: "WordCloud", webpackMode: "weak" */ "./../../../wc").then((m) => m.WordCloud), diff --git a/src/.internal/plugins/json/Classes.ts b/src/.internal/plugins/json/Classes.ts index 0503d797..e8dd37e6 100644 --- a/src/.internal/plugins/json/Classes.ts +++ b/src/.internal/plugins/json/Classes.ts @@ -53,6 +53,7 @@ import type { CommodityChannelIndex } from "./../../../stock"; import type { ComparisonControl } from "./../../../stock"; import type { Component } from "./../../../index"; import type { Container } from "./../../../index"; +import type { DataSaveControl } from "./../../../stock"; import type { DateAxis } from "./../../../xy"; import type { DateRangeSelector } from "./../../../stock"; import type { DisparityIndex } from "./../../../stock"; @@ -209,6 +210,7 @@ import type { Venn } from "./../../../venn"; import type { VerticalLayout } from "./../../../index"; import type { VerticalLineSeries } from "./../../../stock"; import type { Volume } from "./../../../stock"; +import type { VolumeProfile } from "./../../../stock"; import type { VoronoiTreemap } from "./../../../hierarchy"; import type { WilliamsR } from "./../../../stock"; import type { WordCloud } from "./../../../wc"; @@ -270,6 +272,7 @@ export interface IClasses { "ComparisonControl": () => Promise; "Component": () => Promise; "Container": () => Promise; + "DataSaveControl": () => Promise; "DateAxis": () => Promise; "DateRangeSelector": () => Promise; "DisparityIndex": () => Promise; @@ -426,6 +429,7 @@ export interface IClasses { "VerticalLayout": () => Promise; "VerticalLineSeries": () => Promise; "Volume": () => Promise; + "VolumeProfile": () => Promise; "VoronoiTreemap": () => Promise; "WilliamsR": () => Promise; "WordCloud": () => Promise; @@ -488,6 +492,7 @@ const classes: IClasses = { "ComparisonControl": () => import(/* webpackExports: "ComparisonControl", webpackChunkName: "json_stock" */ "./../../../stock").then((m) => m.ComparisonControl), "Component": () => import(/* webpackExports: "Component", webpackChunkName: "json_index" */ "./../../../index").then((m) => m.Component), "Container": () => import(/* webpackExports: "Container", webpackChunkName: "json_index" */ "./../../../index").then((m) => m.Container), + "DataSaveControl": () => import(/* webpackExports: "DataSaveControl", webpackChunkName: "json_stock" */ "./../../../stock").then((m) => m.DataSaveControl), "DateAxis": () => import(/* webpackExports: "DateAxis", webpackChunkName: "json_xy" */ "./../../../xy").then((m) => m.DateAxis), "DateRangeSelector": () => import(/* webpackExports: "DateRangeSelector", webpackChunkName: "json_stock" */ "./../../../stock").then((m) => m.DateRangeSelector), "DisparityIndex": () => import(/* webpackExports: "DisparityIndex", webpackChunkName: "json_stock" */ "./../../../stock").then((m) => m.DisparityIndex), @@ -644,6 +649,7 @@ const classes: IClasses = { "VerticalLayout": () => import(/* webpackExports: "VerticalLayout", webpackChunkName: "json_index" */ "./../../../index").then((m) => m.VerticalLayout), "VerticalLineSeries": () => import(/* webpackExports: "VerticalLineSeries", webpackChunkName: "json_stock" */ "./../../../stock").then((m) => m.VerticalLineSeries), "Volume": () => import(/* webpackExports: "Volume", webpackChunkName: "json_stock" */ "./../../../stock").then((m) => m.Volume), + "VolumeProfile": () => import(/* webpackExports: "VolumeProfile", webpackChunkName: "json_stock" */ "./../../../stock").then((m) => m.VolumeProfile), "VoronoiTreemap": () => import(/* webpackExports: "VoronoiTreemap", webpackChunkName: "json_hierarchy" */ "./../../../hierarchy").then((m) => m.VoronoiTreemap), "WilliamsR": () => import(/* webpackExports: "WilliamsR", webpackChunkName: "json_stock" */ "./../../../stock").then((m) => m.WilliamsR), "WordCloud": () => import(/* webpackExports: "WordCloud", webpackChunkName: "json_wc" */ "./../../../wc").then((m) => m.WordCloud), diff --git a/src/.internal/plugins/json/Serializer.ts b/src/.internal/plugins/json/Serializer.ts index 891406a4..b070f71c 100644 --- a/src/.internal/plugins/json/Serializer.ts +++ b/src/.internal/plugins/json/Serializer.ts @@ -22,6 +22,13 @@ export interface ISerializerSettings extends IEntitySettings { */ includeSettings?: Array; + /** + * Include full values of these settings. + * + * @since 6.4.3 + */ + fullSettings?: Array; + /** * An array of properties to not include in the serialized data. * @@ -107,6 +114,7 @@ export class Serializer extends Entity { const am5object = source instanceof Entity || source instanceof Template || source instanceof Color || source instanceof Percent ? true : false; // Process settings + const fullSettings: any = this.get("fullSettings", []); if (source instanceof Entity) { res.type = source.className; @@ -132,7 +140,7 @@ export class Serializer extends Entity { $array.each(settings, (setting) => { const settingValue = (source).get(setting); if (settingValue !== undefined) { - res.settings[setting] = this.serialize(settingValue, depth + 1, full); + res.settings[setting] = this.serialize(settingValue, depth + 1, full || fullSettings.indexOf(setting) !== -1); } }); } @@ -143,7 +151,7 @@ export class Serializer extends Entity { if (settings.length) { res.settings = {}; $array.each(settings, (setting) => { - res.settings[setting] = this.serialize((source).get(setting), depth + 1); + res.settings[setting] = this.serialize((source).get(setting), depth + 1, fullSettings.indexOf(setting) !== -1); }); } return res; diff --git a/src/stock.ts b/src/stock.ts index 127cf274..b3cd88e1 100644 --- a/src/stock.ts +++ b/src/stock.ts @@ -26,6 +26,7 @@ export { StochasticMomentumIndex, IStochasticMomentumIndexEvents, IStochasticMom export { AwesomeOscillator, IAwesomeOscillatorEvents, IAwesomeOscillatorPrivate, IAwesomeOscillatorSettings } from "./.internal/charts/stock/indicators/AwesomeOscillator"; export { WilliamsR, IWilliamsRPrivate, IWilliamsREvents, IWilliamsRSettings } from "./.internal/charts/stock/indicators/WilliamsR"; export { Volume, IVolumeEvents, IVolumePrivate, IVolumeSettings } from "./.internal/charts/stock/indicators/Volume"; +export { VolumeProfile, IVolumeProfileEvents, IVolumeProfilePrivate, IVolumeProfileSettings } from "./.internal/charts/stock/indicators/VolumeProfile"; export { CommodityChannelIndex, ICommodityChannelIndexEvents, ICommodityChannelIndexPrivate, ICommodityChannelIndexSettings } from "./.internal/charts/stock/indicators/CommodityChannelIndex"; export { DisparityIndex, IDisparityIndexEvents, IDisparityIndexPrivate, IDisparityIndexSettings } from "./.internal/charts/stock/indicators/DisparityIndex"; export { StandardDeviation, IStandardDeviationEvents, IStandardDeviationPrivate, IStandardDeviationSettings } from "./.internal/charts/stock/indicators/StandardDeviation"; @@ -73,5 +74,6 @@ export { SeriesTypeControl, ISeriesTypeControlEvents, ISeriesTypeControlPrivate, export { IntervalControl, IIntervalControlEvents, IIntervalControlItem, IIntervalControlPrivate, IIntervalControlSettings } from "./.internal/charts/stock/toolbar/IntervalControl"; export { ResetControl, IResetControlEvents, IResetControlPrivate, IResetControlSettings } from "./.internal/charts/stock/toolbar/ResetControl"; export { SettingsControl, ISettingsControlEvents, ISettingsControlItem, ISettingsControlPrivate, ISettingsControlSettings } from "./.internal/charts/stock/toolbar/SettingsControl"; +export { DataSaveControl, IDataSaveControlEvents, IDataSaveControlItem, IDataSaveControlPrivate, IDataSaveControlSettings } from "./.internal/charts/stock/toolbar/DataSaveControl"; export { ComparisonControl, IComparisonControlEvents, IComparisonControlPrivate, IComparisonControlSettings } from "./.internal/charts/stock/toolbar/ComparisonControl"; export { ColorControl, IColorControlEvents, IColorControlPrivate, IColorControlSettings } from "./.internal/charts/stock/toolbar/ColorControl";