diff --git a/examples/shared/stock-chart-comparing-stocks/index.ts b/examples/shared/stock-chart-comparing-stocks/index.ts index b4706670..c6f08331 100644 --- a/examples/shared/stock-chart-comparing-stocks/index.ts +++ b/examples/shared/stock-chart-comparing-stocks/index.ts @@ -335,7 +335,7 @@ seriesSwitcher.events.on("selected", function(ev) { function getNewSettings(series: am5xy.XYSeries) { let newSettings: any = []; - am5.array.each(["name", "valueYField", "highValueYField", "lowValueYField", "openValueYField", "calculateAggregates", "valueXField", "xAxis", "yAxis", "legendValueText", "stroke", "fill"], function(setting: any) { + am5.array.each(["name", "valueYField", "highValueYField", "lowValueYField", "openValueYField", "calculateAggregates", "valueXField", "xAxis", "yAxis", "legendValueText", "legendRangeValueText", "stroke", "fill"], function(setting: any) { newSettings[setting] = series.get(setting); }); return newSettings; diff --git a/examples/shared/stock-chart-data-granularity/index.ts b/examples/shared/stock-chart-data-granularity/index.ts index 25526262..a8c0f6eb 100644 --- a/examples/shared/stock-chart-data-granularity/index.ts +++ b/examples/shared/stock-chart-data-granularity/index.ts @@ -225,7 +225,7 @@ seriesSwitcher.events.on("selected", function(ev) { function getNewSettings(series: am5xy.XYSeries) { let newSettings: any = []; - am5.array.each(["name", "valueYField", "highValueYField", "lowValueYField", "openValueYField", "calculateAggregates", "valueXField", "xAxis", "yAxis", "legendValueText", "stroke", "fill"], function(setting: any) { + am5.array.each(["name", "valueYField", "highValueYField", "lowValueYField", "openValueYField", "calculateAggregates", "valueXField", "xAxis", "yAxis", "legendValueText", "legendRangeValueText", "stroke", "fill"], function(setting: any) { newSettings[setting] = series.get(setting); }); return newSettings; diff --git a/examples/shared/stock-chart-data-grouping/index.ts b/examples/shared/stock-chart-data-grouping/index.ts index 46a0f805..4f76e5f9 100644 --- a/examples/shared/stock-chart-data-grouping/index.ts +++ b/examples/shared/stock-chart-data-grouping/index.ts @@ -267,7 +267,7 @@ seriesSwitcher.events.on("selected", function(ev) { function getNewSettings(series: am5xy.XYSeries) { let newSettings: any = []; - am5.array.each(["name", "valueYField", "highValueYField", "lowValueYField", "openValueYField", "calculateAggregates", "valueXField", "xAxis", "yAxis", "legendValueText", "stroke", "fill"], function(setting: any) { + am5.array.each(["name", "valueYField", "highValueYField", "lowValueYField", "openValueYField", "calculateAggregates", "valueXField", "xAxis", "yAxis", "legendValueText", "legendRangeValueText", "stroke", "fill"], function(setting: any) { newSettings[setting] = series.get(setting); }); return newSettings; diff --git a/examples/shared/stock-chart-intraday/index.ts b/examples/shared/stock-chart-intraday/index.ts index 08d0a207..1e6f6d4a 100644 --- a/examples/shared/stock-chart-intraday/index.ts +++ b/examples/shared/stock-chart-intraday/index.ts @@ -208,7 +208,7 @@ seriesSwitcher.events.on("selected", function(ev) { function getNewSettings(series: am5xy.XYSeries) { let newSettings: any = []; - am5.array.each(["name", "valueYField", "highValueYField", "lowValueYField", "openValueYField", "calculateAggregates", "valueXField", "xAxis", "yAxis", "legendValueText", "stroke", "fill"], function(setting: any) { + am5.array.each(["name", "valueYField", "highValueYField", "lowValueYField", "openValueYField", "calculateAggregates", "valueXField", "xAxis", "yAxis", "legendValueText", "legendRangeValueText", "stroke", "fill"], function(setting: any) { newSettings[setting] = series.get(setting); }); return newSettings; diff --git a/examples/shared/stock-chart-live/index.ts b/examples/shared/stock-chart-live/index.ts index cb264757..68cd354d 100644 --- a/examples/shared/stock-chart-live/index.ts +++ b/examples/shared/stock-chart-live/index.ts @@ -200,6 +200,7 @@ function getNewSettings(series) { "xAxis", "yAxis", "legendValueText", + "legendRangeValueText", "stroke", "fill" ], diff --git a/examples/shared/stock-chart-volume-separate-panel/index.ts b/examples/shared/stock-chart-volume-separate-panel/index.ts index 6f9d1431..b6b0f7ed 100644 --- a/examples/shared/stock-chart-volume-separate-panel/index.ts +++ b/examples/shared/stock-chart-volume-separate-panel/index.ts @@ -252,7 +252,7 @@ seriesSwitcher.events.on("selected", function(ev) { function getNewSettings(series: am5xy.XYSeries) { let newSettings: any = []; - am5.array.each(["name", "valueYField", "highValueYField", "lowValueYField", "openValueYField", "calculateAggregates", "valueXField", "xAxis", "yAxis", "legendValueText", "stroke", "fill"], function(setting: any) { + am5.array.each(["name", "valueYField", "highValueYField", "lowValueYField", "openValueYField", "calculateAggregates", "valueXField", "xAxis", "yAxis", "legendValueText", "legendRangeValueText", "stroke", "fill"], function(setting: any) { newSettings[setting] = series.get(setting); }); return newSettings; diff --git a/examples/shared/stock-chart/index.ts b/examples/shared/stock-chart/index.ts index 57b8932d..367798e8 100644 --- a/examples/shared/stock-chart/index.ts +++ b/examples/shared/stock-chart/index.ts @@ -210,7 +210,7 @@ seriesSwitcher.events.on("selected", function(ev) { function getNewSettings(series: am5xy.XYSeries) { let newSettings: any = []; - am5.array.each(["name", "valueYField", "highValueYField", "lowValueYField", "openValueYField", "calculateAggregates", "valueXField", "xAxis", "yAxis", "legendValueText", "stroke", "fill"], function(setting: any) { + am5.array.each(["name", "valueYField", "highValueYField", "lowValueYField", "openValueYField", "calculateAggregates", "valueXField", "xAxis", "yAxis", "legendValueText", "legendRangeValueText", "stroke", "fill"], function(setting: any) { newSettings[setting] = series.get(setting); }); return newSettings; diff --git a/package.json b/package.json index f9489546..437bbca5 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "@amcharts/amcharts5", - "version": "5.7.7", + "version": "5.8.0", "author": "amCharts (https://www.amcharts.com/)", "description": "amCharts 5", "homepage": "https://www.amcharts.com/", diff --git a/packages/shared/CHANGELOG.md b/packages/shared/CHANGELOG.md index 18a0e011..592f26ab 100644 --- a/packages/shared/CHANGELOG.md +++ b/packages/shared/CHANGELOG.md @@ -5,6 +5,34 @@ 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.8.0] - 2024-02-01 + +### Added +- New container type `ZoomableContainer` added, which allows adding zoom capabilities to virtually any chart. [More info](https://www.amcharts.com/docs/v5/concepts/common-elements/containers/#Zoomable_container). +- New class `ZoomTools` added. Can be used to quickly add zoom support for elements compatible with `IZoomable` interface, e.g. `ZoomableContainer`. +- New read-only property of `DrawingControl`: `drawingSeries`. Contains an object where key is a drawing tool name and values are array with references to actual drawing series. + +### Changed +- Default value for `strokeOpacity` for the grid line of a `StockChart` changed from `0` to `0.4` so that it would differ from regular grid lines. + +### Fixed +- Resizing stock chart's panels after moving them up/down was not working properly. +- `Hierarchy.addChildData` was duplicating node's children which were added before. +- If `topDepth` was set to `1` on `Partition`, initially the chart was showing not all levels of nodes. +- When clicking on particular partitions of a `Partion` chart, zoom animation was not playing. +- `DateRangeSelector` was showing extra day in automatically-calculated end date. +- `DateRangeSelector` was zooming to the start of the selected day, instead of end. +- Tooltip background color passed to `HeatLegend.showValue(value, text, color)` was being ignored. +- `ClusteredPointSeries` was not showing charts if they were added to clustered bullets. +- Fixed issue with `DrawingControl` which was unnecessarily duplicating drawing series with each tool selection / API call. +- Using "1M" button in `PeriodSelector` was not always selecting full month. +- Restoring drawings with `DrawingControl.unserializeDrawings()` will now correctly update indexes of the restored drawings based on current drawing counter. +- In some cases `StockChart` was taking a data item which was just outside of zoom range when calculating aggregate values for its series, affecting percent change values and some other aggregates. +- Legend value was not being updated when series axis' range changed. This caused `legendRangeLabelText` to not being updated (if set) when chart was being zoomed-in/out. +- If a type of `StockChart` series was changed with data grouping enabled, some duplicate candlesticks or ohlc sticks were displayed until first zoom. +- In some rare cases chart would show incorrect last segment of a `LineSeries` when in percent mode. + + ## [5.7.7] - 2024-01-18 ### Added diff --git a/src/.internal/charts/hierarchy/Hierarchy.ts b/src/.internal/charts/hierarchy/Hierarchy.ts index 74958b78..c191c683 100644 --- a/src/.internal/charts/hierarchy/Hierarchy.ts +++ b/src/.internal/charts/hierarchy/Hierarchy.ts @@ -544,18 +544,29 @@ export abstract class Hierarchy extends Series { let depth = dataItem.get("depth"); $array.each(childData, (child) => { - const childDataItem = new DataItem(this, child, this._makeDataItem(child)); + let found = false; + $array.eachContinue(children, (dataItem) => { + if (dataItem.dataContext == child) { + found = true; + return false; + } + return true; + }) - children.push(childDataItem); + if (!found) { + const childDataItem = new DataItem(this, child, this._makeDataItem(child)); - childDataItem.setRaw("parent", dataItem); - childDataItem.setRaw("depth", depth + 1); + children.push(childDataItem); - if (childDataItem.get("fill") == null) { - childDataItem.setRaw("fill", dataItem.get("fill")); - } + childDataItem.setRaw("parent", dataItem); + childDataItem.setRaw("depth", depth + 1); + + if (childDataItem.get("fill") == null) { + childDataItem.setRaw("fill", dataItem.get("fill")); + } - this.processDataItem(childDataItem); + this.processDataItem(childDataItem); + } }) } @@ -890,9 +901,9 @@ export abstract class Hierarchy extends Series { this.enableDataItem(this.dataItems[0], downDepth, 0); } - this._root.events.once("frameended", ()=>{ + this._root.events.once("frameended", () => { this._zoom(dataItem); - }) + }) } } diff --git a/src/.internal/charts/hierarchy/Partition.ts b/src/.internal/charts/hierarchy/Partition.ts index 60e75848..7d0c73c7 100644 --- a/src/.internal/charts/hierarchy/Partition.ts +++ b/src/.internal/charts/hierarchy/Partition.ts @@ -61,6 +61,10 @@ export interface IPartitionSettings extends IHierarchySettings { */ orientation?: "horizontal" | "vertical"; + /** + * @ignore + */ + _d?:number; } export interface IPartitionPrivate extends IHierarchyPrivate { @@ -73,8 +77,7 @@ export interface IPartitionPrivate extends IHierarchyPrivate { /** * Current vertical scale. */ - scaleY?: number; - + scaleY?: number; } /** @@ -286,7 +289,7 @@ export class Partition extends Hierarchy { const maxDepth = this.getPrivate("maxDepth", 1); const levelHeight = height / (maxDepth + 1); const levelWidth = width / (maxDepth + 1); - const initialDepth = Math.min(this.get("initialDepth", 1), maxDepth - topDepth); + const initialDepth = Math.min(this.get("initialDepth", 1), maxDepth)// - topDepth); let downDepth = this._currentDownDepth; if (downDepth == null) { @@ -344,8 +347,9 @@ export class Partition extends Hierarchy { } this.animatePrivate({ key: "scaleX", to: scaleX, duration: duration, easing: easing }); - this.animatePrivate({ key: "scaleY", to: scaleY, duration: duration, easing: easing }); + this.animatePrivate({ key: "scaleY", to: scaleY, duration: duration, easing: easing }); + this.animate({key:"_d", from:0, to:1, duration: duration, easing: easing }) this.nodesContainer.animate({ key: "x", to: -x0 * scaleX, duration: duration, easing: easing }); this.nodesContainer.animate({ key: "y", to: -y0 * scaleY, duration: duration, easing: easing }); } diff --git a/src/.internal/charts/map/ClusteredPointSeries.ts b/src/.internal/charts/map/ClusteredPointSeries.ts index b0303f23..3a67e49b 100644 --- a/src/.internal/charts/map/ClusteredPointSeries.ts +++ b/src/.internal/charts/map/ClusteredPointSeries.ts @@ -139,6 +139,8 @@ export class ClusteredPointSeries extends MapPointSeries { protected _spiral: Array<{ x: number, y: number }> = []; + protected _clusterDone:boolean = false; + protected _afterNew() { this.fields.push("groupId"); this._setRawDefault("groupIdField", "groupId"); @@ -146,58 +148,61 @@ export class ClusteredPointSeries extends MapPointSeries { super._afterNew(); } - public _updateChildren() { - super._updateChildren(); + public _prepareChildren() { + super._prepareChildren(); - if (this.isDirty("scatterRadius")) { - this._spiral = $math.spiralPoints(0, 0, 300, 300, 0, 3, 3, 0, 0) - } + if(!this._clusterDone){ + if (this.isDirty("scatterRadius")) { + this._spiral = $math.spiralPoints(0, 0, 300, 300, 0, 3, 3, 0, 0) + } - const groups: { [index: string]: Array> } = {}; - // distribute to groups - $array.each(this.dataItems, (dataItem) => { - const groupId = dataItem.get("groupId", "_default"); + const groups: { [index: string]: Array> } = {}; + // distribute to groups + $array.each(this.dataItems, (dataItem) => { + const groupId = dataItem.get("groupId", "_default"); - if (!groups[groupId]) { - groups[groupId] = []; - } - groups[groupId].push(dataItem); - }) + if (!groups[groupId]) { + groups[groupId] = []; + } + groups[groupId].push(dataItem); + }) - this._scatterIndex = -1; - this._scatters = []; - this._clusterIndex = -1; - this._clusters = []; + this._scatterIndex = -1; + this._scatters = []; + this._clusterIndex = -1; + this._clusters = []; - $array.each(this.clusteredDataItems, (dataItem) => { - dataItem.setRaw("children", undefined); - }) + $array.each(this.clusteredDataItems, (dataItem) => { + dataItem.setRaw("children", undefined); + }) - $array.each(this.dataItems, (dataItem) => { - dataItem.setRaw("cluster", undefined); - }) + $array.each(this.dataItems, (dataItem) => { + dataItem.setRaw("cluster", undefined); + }) - $object.each(groups, (_key, group) => { - this._scatterGroup(group); - }) + $object.each(groups, (_key, group) => { + this._scatterGroup(group); + }) - $object.each(groups, (_key, group) => { - this._clusterGroup(group); - }) + $object.each(groups, (_key, group) => { + this._clusterGroup(group); + }) - $array.each(this.dataItems, (dataItem) => { - if (!dataItem.get("cluster")) { - const bullets = dataItem.bullets; - if (bullets) { - $array.each(bullets, (bullet) => { - const sprite = bullet.get("sprite"); - if (sprite) { - sprite.set("forceHidden", false); - } - }) + $array.each(this.dataItems, (dataItem) => { + if (!dataItem.get("cluster")) { + const bullets = dataItem.bullets; + if (bullets) { + $array.each(bullets, (bullet) => { + const sprite = bullet.get("sprite"); + if (sprite) { + sprite.set("forceHidden", false); + } + }) + } } - } - }) + }) + this._clusterDone = true; + } } /** @@ -328,7 +333,7 @@ export class ClusteredPointSeries extends MapPointSeries { } }) } - + protected _onDataClear() { super._onDataClear(); @@ -365,6 +370,11 @@ export class ClusteredPointSeries extends MapPointSeries { } } + public _clearDirty(): void { + super._clearDirty(); + this._clusterDone = false; + } + protected _scatterGroup(dataItems: Array>) { const chart = this.chart; if (chart && chart.get("zoomLevel", 1) >= chart.get("maxZoomLevel", 100) * this.get("stopClusterZoom", 0.95)) { diff --git a/src/.internal/charts/map/MapChartDefaultTheme.ts b/src/.internal/charts/map/MapChartDefaultTheme.ts index a514f7f1..87b1337a 100644 --- a/src/.internal/charts/map/MapChartDefaultTheme.ts +++ b/src/.internal/charts/map/MapChartDefaultTheme.ts @@ -1,5 +1,4 @@ import { Theme } from "../../core/Theme"; -import { p50, p100 } from "../../core/util/Percent"; import { setColor } from "../../themes/DefaultTheme"; import { geoMercator } from "d3-geo"; @@ -96,44 +95,11 @@ export class MapChartDefaultTheme extends Theme { setColor(rule, "stroke", ic, "background"); } - r("Button", ["zoomcontrol"]).setAll({ - marginTop: 1, - marginBottom: 2 - }) - - r("Graphics", ["map", "button", "plus", "icon"]).setAll({ - x: p50, - y: p50, - draw: (display) => { - display.moveTo(-4, 0); - display.lineTo(4, 0); - display.moveTo(0, -4); - display.lineTo(0, 4); - } - }); - r("Graphics", ["map", "button", "minus", "icon"]).setAll({ - x: p50, - y: p50, - draw: (display) => { - display.moveTo(-4, 0); - display.lineTo(4, 0); - } - }); - - - r("Button", ["zoomcontrol", "home"]).setAll({ + r("Button", ["zoomtools", "home"]).setAll({ visible: false }); - - r("Graphics", ["map", "button", "home", "icon"]).setAll({ - x: p50, - y: p50, - svgPath: "M 8 -1 L 6 -1 L 6 7 L 2 7 L 2 1 L -2 1 L -2 7 L -6 7 L -6 -1 L -8 -1 L 0 -9 L 8 -1 Z M 8 -1" - }); - - /** * ------------------------------------------------------------------------ * charts/map: Series @@ -143,15 +109,5 @@ export class MapChartDefaultTheme extends Theme { r("GraticuleSeries").setAll({ step: 10 }); - - - r("ZoomControl").setAll({ - x: p100, - centerX: p100, - y: p100, - centerY: p100, - paddingRight: 10, - paddingBottom: 10 - }) } } diff --git a/src/.internal/charts/map/ZoomControl.ts b/src/.internal/charts/map/ZoomControl.ts index a999fcd2..2ee7091b 100644 --- a/src/.internal/charts/map/ZoomControl.ts +++ b/src/.internal/charts/map/ZoomControl.ts @@ -1,21 +1,15 @@ import type { MapChart } from "./MapChart"; -import { Container, IContainerPrivate, IContainerSettings } from "../../core/render/Container"; -import { Button } from "../../core/render/Button"; -import { Graphics } from "../../core/render/Graphics"; -import { MultiDisposer } from "../../core/util/Disposer"; - -export interface IZoomControlSettings extends IContainerSettings { +import { ZoomTools, IZoomToolsPrivate, IZoomToolsSettings } from "../../core/render/ZoomTools"; +export interface IZoomControlSettings extends IZoomToolsSettings { } -export interface IZoomControlPrivate extends IContainerPrivate { - +export interface IZoomControlPrivate extends IZoomToolsPrivate { /** * @ignore */ chart?: MapChart; - } /** @@ -24,82 +18,23 @@ export interface IZoomControlPrivate extends IContainerPrivate { * @see {@link https://www.amcharts.com/docs/v5/charts/map-chart/map-pan-zoom/#Zoom_control} for more information * @important */ -export class ZoomControl extends Container { - - /** - * A [[Button]] for home. - * - * Home button is disabled by default. To enable it set its `visible: true`. - * - * @see (@link https://www.amcharts.com/docs/v5/charts/map-chart/map-pan-zoom/#Home_button) for more info - * @since 5.7.5 - */ - public readonly homeButton: Button = this.children.push(Button.new(this._root, { width: 35, height: 35, themeTags: ["home"] })); - - /** - * A [[Button]] for zoom in. - */ - public readonly plusButton: Button = this.children.push(Button.new(this._root, { width: 35, height: 35, themeTags: ["plus"] })); - - /** - * A [[Button]] for zoom out. - */ - public readonly minusButton: Button = this.children.push(Button.new(this._root, { width: 35, height: 35, themeTags: ["minus"] })); +export class ZoomControl extends ZoomTools { + public static className: string = "ZoomControl"; + public static classNames: Array = ZoomTools.classNames.concat([ZoomControl.className]); declare public _settings: IZoomControlSettings; declare public _privateSettings: IZoomControlPrivate; - public static className: string = "ZoomControl"; - public static classNames: Array = Container.classNames.concat([ZoomControl.className]); - - protected _disposer: MultiDisposer | undefined; - protected _afterNew() { super._afterNew(); - - this.set("position", "absolute"); - - this.set("layout", this._root.verticalLayout); - this.set("themeTags", ["zoomcontrol"]); - - this.plusButton.setAll({ - icon: Graphics.new(this._root, { themeTags: ["icon"] }), - layout: undefined - }); - - this.minusButton.setAll({ - icon: Graphics.new(this._root, { themeTags: ["icon"] }), - layout: undefined - }); - - this.homeButton.setAll({ - icon: Graphics.new(this._root, { themeTags: ["icon"] }), - layout: undefined - }); + this.addTag("zoomtools"); } public _prepareChildren() { super._prepareChildren(); if (this.isPrivateDirty("chart")) { - const chart = this.getPrivate("chart"); - const previous = this._prevPrivateSettings.chart; - if (chart) { - this._disposer = new MultiDisposer([ - this.plusButton.events.on("click", () => { - chart.zoomIn() - }), - this.minusButton.events.on("click", () => { - chart.zoomOut() - }), - this.homeButton.events.on("click", () => { - chart.goHome() - })]) - } - - if (previous && this._disposer) { - this._disposer.dispose(); - } + this.set("target", this.getPrivate("chart")); } } } diff --git a/src/.internal/charts/stock/StockChart.ts b/src/.internal/charts/stock/StockChart.ts index 9bc4a14d..0b555649 100644 --- a/src/.internal/charts/stock/StockChart.ts +++ b/src/.internal/charts/stock/StockChart.ts @@ -788,6 +788,8 @@ export class StockChart extends Container { }); } }) + + this._updateResizers(); } protected _processPanel(panel: StockPanel) { @@ -797,61 +799,61 @@ export class StockChart extends Container { panel.panelControls = panel.topPlotContainer.children.push(PanelControls.new(this._root, { stockPanel: panel, stockChart: this })); this._updateControls(); - if (this.panels.length > 1) { - const resizer = panel.children.push(Rectangle.new(this._root, { themeTags: ["panelresizer"] })) - panel.panelResizer = resizer; + const resizer = panel.children.push(Rectangle.new(this._root, { themeTags: ["panelresizer"] })) - resizer.events.on("pointerdown", (e) => { - const chartsContainer = this.panelsContainer; - this._downResizer = e.target; - this.panels.each((chart) => { - chart.set("height", percent(chart.height() / chartsContainer.height() * 100)) - }) + panel.panelResizer = resizer; - this._downY = chartsContainer.toLocal(e.point).y; + resizer.events.on("pointerdown", (e) => { + const chartsContainer = this.panelsContainer; + this._downResizer = e.target; + this.panels.each((chart) => { + chart.set("height", percent(chart.height() / chartsContainer.height() * 100)) + }) - const upperChart = this.panels.getIndex(this.panels.indexOf(panel) - 1); - this._upperPanel = upperChart; - if (upperChart) { - this._uhp = upperChart.get("height") as Percent; - } + this._downY = chartsContainer.toLocal(e.point).y; - this._dhp = panel.get("height") as Percent; - }) + const upperChart = chartsContainer.children.getIndex(chartsContainer.children.indexOf(panel) - 1) as StockPanel; + this._upperPanel = upperChart; + if (upperChart) { + this._uhp = upperChart.get("height") as Percent; + } - resizer.events.on("pointerup", () => { - this._downResizer = undefined; - }) + this._dhp = panel.get("height") as Percent; + }) - resizer.events.on("globalpointermove", (e) => { - if (e.target == this._downResizer) { - const chartsContainer = this.panelsContainer; - const height = chartsContainer.height(); - const upperChart = this._upperPanel; - - if (upperChart) { - const index = this.panels.indexOf(upperChart) + 2 - let max = height - panel.get("minHeight", 0); - const lowerChart = this.panels.getIndex(index); - if (lowerChart) { - max = lowerChart.y() - panel.get("minHeight", 0); - } - //console.log(upperChart.get("minHeight", 0)) - const y = Math.max(upperChart.y() + upperChart.get("minHeight", 0) + upperChart.get("paddingTop", 0), Math.min(chartsContainer.toLocal(e.point).y, max)); - - const downY = this._downY; - const dhp = this._dhp; - const uhp = this._uhp; - if (downY != null && dhp != null && uhp != null) { - const diff = (downY - y) / height; - panel.set("height", percent((dhp.value + diff) * 100)); - upperChart.set("height", percent((uhp.value - diff) * 100)) - } + resizer.events.on("pointerup", () => { + this._downResizer = undefined; + }) + + resizer.events.on("globalpointermove", (e) => { + if (e.target == this._downResizer) { + const chartsContainer = this.panelsContainer; + const height = chartsContainer.height(); + const upperChart = this._upperPanel; + + if (upperChart) { + const index = chartsContainer.children.indexOf(upperChart) + 2 + let max = height - panel.get("minHeight", 0); + const lowerChart = chartsContainer.children.getIndex(index); + if (lowerChart) { + max = lowerChart.y() - panel.get("minHeight", 0); + } + //console.log(upperChart.get("minHeight", 0)) + const y = Math.max(upperChart.y() + upperChart.get("minHeight", 0) + upperChart.get("paddingTop", 0), Math.min(chartsContainer.toLocal(e.point).y, max)); + + const downY = this._downY; + const dhp = this._dhp; + const uhp = this._uhp; + if (downY != null && dhp != null && uhp != null) { + const diff = (downY - y) / height; + panel.set("height", percent((dhp.value + diff) * 100)); + upperChart.set("height", percent((uhp.value - diff) * 100)) } } - }) - } + } + }) + panel.xAxes.events.onAll((change) => { if (change.type === "clear") { $array.each(change.oldValues, (axis) => { @@ -878,6 +880,17 @@ export class StockChart extends Container { this._syncYAxesSize(); }) + this._updateResizers(); + } + + public _updateResizers() { + this.panels.each((panel) => { + let hidden = false; + if (this.panelsContainer.children.indexOf(panel) == 0) { + hidden = true; + } + panel.panelResizer?.set("forceHidden", hidden); + }) } protected _syncYAxesSize() { diff --git a/src/.internal/charts/stock/StockChartDefaultTheme.ts b/src/.internal/charts/stock/StockChartDefaultTheme.ts index eda21eae..42549889 100644 --- a/src/.internal/charts/stock/StockChartDefaultTheme.ts +++ b/src/.internal/charts/stock/StockChartDefaultTheme.ts @@ -144,7 +144,7 @@ export class StockChartDefaultTheme extends Theme { r("Grid", ["renderer", "base", "y"]).setAll({ - strokeOpacity: 0.15 + strokeOpacity: 0.4 }) r("Button", ["zoom"]).setAll({ diff --git a/src/.internal/charts/stock/drawing/DrawingSeries.ts b/src/.internal/charts/stock/drawing/DrawingSeries.ts index cb37b2e1..3f638229 100644 --- a/src/.internal/charts/stock/drawing/DrawingSeries.ts +++ b/src/.internal/charts/stock/drawing/DrawingSeries.ts @@ -109,7 +109,6 @@ export class DrawingSeries extends LineSeries { protected _upDp?: IDisposer; protected _drawingEnabled: boolean = false; - protected _isDragging: boolean = false; protected _clickPointerPoint?: IPoint; protected _movePointerPoint?: IPoint; @@ -117,7 +116,7 @@ export class DrawingSeries extends LineSeries { protected _isDrawing: boolean = false; protected _isPointerDown: boolean = false; - protected _index: number = 0; + public _index: number = 0; protected _di: Array<{ [index: string]: DataItem }> = []; diff --git a/src/.internal/charts/stock/drawing/ParallelChannelSeries.ts b/src/.internal/charts/stock/drawing/ParallelChannelSeries.ts index 3cb24f57..d105bb5e 100644 --- a/src/.internal/charts/stock/drawing/ParallelChannelSeries.ts +++ b/src/.internal/charts/stock/drawing/ParallelChannelSeries.ts @@ -18,8 +18,6 @@ export class ParallelChannelSeries extends SimpleLineSeries { declare public _privateSettings: IParallelChannelSeriesPrivate; declare public _dataItemSettings: IParallelChannelSeriesDataItem; - protected _index: number = 0; - protected _di: Array<{ [index: string]: DataItem }> = [] protected _tag = "parallelchannel"; diff --git a/src/.internal/charts/stock/drawing/RectangleSeries.ts b/src/.internal/charts/stock/drawing/RectangleSeries.ts index bc92163f..5d8025d6 100644 --- a/src/.internal/charts/stock/drawing/RectangleSeries.ts +++ b/src/.internal/charts/stock/drawing/RectangleSeries.ts @@ -22,8 +22,6 @@ export class RectangleSeries extends SimpleLineSeries { declare public _privateSettings: IRectangleSeriesPrivate; declare public _dataItemSettings: IRectangleSeriesDataItem; - protected _index: number = 0; - protected _di: Array<{ [index: string]: DataItem }> = [] protected _tag = "rectangle"; diff --git a/src/.internal/charts/stock/indicators/VolumeProfile.ts b/src/.internal/charts/stock/indicators/VolumeProfile.ts index 1a49b9f6..2e4002fd 100644 --- a/src/.internal/charts/stock/indicators/VolumeProfile.ts +++ b/src/.internal/charts/stock/indicators/VolumeProfile.ts @@ -175,24 +175,26 @@ export class VolumeProfile extends Indicator { this.series = chart.series.unshift(ColumnSeries.new(root, { xAxis: this.xAxis, yAxis: yAxis, + snapTooltip:false, valueXField: "down", openValueXField: "xOpen", openValueYField: "yOpen", valueYField: "y", calculateAggregates: true, - themeTags: ["indicator", "volumeprofile"] + themeTags: ["indicator", "volumeprofile", "down"] })) this.upSeries = chart.series.unshift(ColumnSeries.new(root, { xAxis: this.xAxis, yAxis: yAxis, + snapTooltip:false, valueXField: "total", openValueXField: "down", openValueYField: "yOpen", valueYField: "y", calculateAggregates: true, - themeTags: ["indicator", "volumeprofile"] + themeTags: ["indicator", "volumeprofile", "up"] })) this.upSeries.setPrivate("doNotUpdateLegend", true); diff --git a/src/.internal/charts/stock/toolbar/DateRangeSelector.ts b/src/.internal/charts/stock/toolbar/DateRangeSelector.ts index f180a908..67be1966 100644 --- a/src/.internal/charts/stock/toolbar/DateRangeSelector.ts +++ b/src/.internal/charts/stock/toolbar/DateRangeSelector.ts @@ -241,6 +241,7 @@ export class DateRangeSelector extends StockControl { $utils.addEventListener(saveButton, "click", () => { const from = this._parseDate(fromField.value); const to = this._parseDate(toField.value); + to.setHours(23, 59, 59); this.setPrivate("fromDate", from); this.setPrivate("toDate", to); xAxis.zoomToDates(from, to); @@ -330,7 +331,7 @@ export class DateRangeSelector extends StockControl { } if (maxDate == "auto") { - const min = xAxis.getPrivate("maxFinal"); + const min = xAxis.getPrivate("maxFinal") - 1; if (min) { fromPicker.set("maxDate", new Date(min)); toPicker.set("maxDate", new Date(min)); diff --git a/src/.internal/charts/stock/toolbar/DrawingControl.ts b/src/.internal/charts/stock/toolbar/DrawingControl.ts index d4ad7f38..43534aef 100644 --- a/src/.internal/charts/stock/toolbar/DrawingControl.ts +++ b/src/.internal/charts/stock/toolbar/DrawingControl.ts @@ -776,18 +776,26 @@ export class DrawingControl extends StockControl { seriesList = []; } + // Get panels that are already initialized + const initializedPanels: StockPanel[] = []; + $array.each(seriesList, (series: DrawingSeries) => { + initializedPanels.push(series.chart as StockPanel); + }); + // Get target series const chartSeries: XYSeries[] = this.get("series", []); const stockChart = this.get("stockChart"); - if (chartSeries.length == 0) { - // No target series set, take first series out of each chart + // if (chartSeries.length == 0) { + // // No target series set, take first series out of each chart stockChart.panels.each((panel) => { - const targetSeries = this._getPanelMainSeries(panel); - if (targetSeries) { - chartSeries.push(targetSeries); + if (initializedPanels.indexOf(panel) == -1) { + const targetSeries = this._getPanelMainSeries(panel); + if (targetSeries) { + chartSeries.push(targetSeries); + } } }); - } + // } if (chartSeries.length > 0) { const toolSettings: any = this.get("toolSettings", {}); @@ -1204,6 +1212,7 @@ export class DrawingControl extends StockControl { // Parse JsonParser.new(this._root).parse(drawing.__drawing).then((drawingData: any) => { + this._updateDrawingIndexes(drawingData, drawingSeries._index, drawingSeries); drawingSeries.data.pushAll(drawingData); }); @@ -1218,4 +1227,26 @@ export class DrawingControl extends StockControl { } }) } -} + + protected _updateDrawingIndexes(drawingData: any, index: number, drawingSeries: DrawingSeries): void { + if ($type.isArray(drawingData)) { + $array.each(drawingData, (item: any) => { + this._updateDrawingIndexes(item, index, drawingSeries); + }) + } + else if ($type.isObject(drawingData as any) && drawingData.index !== undefined) { + drawingData.index += index; + drawingSeries._index = drawingData.index; + } + } + + /** + * Returns an object that holds all drawing series, arrange by tool (key). + * + * @since 5.8.0 + * @readonly + */ + public get drawingSeries(): { [index: string]: DrawingSeries[] } { + return this._drawingSeries; + } +} \ No newline at end of file diff --git a/src/.internal/charts/wordcloud/WordCloud.ts b/src/.internal/charts/wordcloud/WordCloud.ts index 7eda6200..d053c194 100644 --- a/src/.internal/charts/wordcloud/WordCloud.ts +++ b/src/.internal/charts/wordcloud/WordCloud.ts @@ -79,9 +79,10 @@ export interface IWordCloudSettings extends ISeriesSettings { autoFit?: boolean; /** + * Progress of current word layout animation. (0-1) + * * @readonly */ - progress?: number; /** diff --git a/src/.internal/charts/xy/XYChart.ts b/src/.internal/charts/xy/XYChart.ts index bf30756c..46f14cbc 100644 --- a/src/.internal/charts/xy/XYChart.ts +++ b/src/.internal/charts/xy/XYChart.ts @@ -1268,6 +1268,8 @@ export class XYChart extends SerialChart { series._markDirtyAxes(); yAxis.markDirtyExtremes(); xAxis.markDirtyExtremes(); + xAxis._seriesAdded = true; + yAxis._seriesAdded = true; this._colorize(series); } diff --git a/src/.internal/charts/xy/axes/Axis.ts b/src/.internal/charts/xy/axes/Axis.ts index ba109187..5a3e9298 100644 --- a/src/.internal/charts/xy/axes/Axis.ts +++ b/src/.internal/charts/xy/axes/Axis.ts @@ -350,6 +350,8 @@ export abstract class Axis extends Component { public _seriesValuesDirty = false; + public _seriesAdded = false; + /** * A container above the axis that can be used to add additional stuff into * it. For example a legend, label, or an icon. @@ -463,7 +465,7 @@ export abstract class Axis extends Component { let maxZoomFactorReal = maxZoomFactor; if (end === 1 && start !== 0) { - if (start < this.get("start")) { + if (start < this.get("start", 0)) { priority = "start"; } else { @@ -472,7 +474,7 @@ export abstract class Axis extends Component { } if (start === 0 && end !== 1) { - if (end > this.get("end")) { + if (end > this.get("end", 1)) { priority = "end"; } else { @@ -480,8 +482,8 @@ export abstract class Axis extends Component { } } - let minZoomCount = this.get("minZoomCount"); - let maxZoomCount = this.get("maxZoomCount"); + let minZoomCount = this.get("minZoomCount", 0); + let maxZoomCount = this.get("maxZoomCount", Infinity); if ($type.isNumber(minZoomCount)) { maxZoomFactor = maxZoomFactorReal / minZoomCount; @@ -990,6 +992,8 @@ export abstract class Axis extends Component { } this.get("renderer")._updatePositions(); + + this._seriesAdded = false; } /** @@ -1098,7 +1102,7 @@ export abstract class Axis extends Component { this._updateTooltipText(tooltip, position); renderer.positionTooltip(tooltip, position); - if (position < this.get("start") || position > this.get("end")) { + if (position < this.get("start", 0) || position > this.get("end", 1)) { tooltip.hide(0); } else { diff --git a/src/.internal/charts/xy/axes/AxisRenderer.ts b/src/.internal/charts/xy/axes/AxisRenderer.ts index 94c881c0..ed354442 100644 --- a/src/.internal/charts/xy/axes/AxisRenderer.ts +++ b/src/.internal/charts/xy/axes/AxisRenderer.ts @@ -475,10 +475,8 @@ export abstract class AxisRenderer extends Graphics { protected _positionTooltip(tooltip: Tooltip, point: IPoint) { const chart = this.chart; if (chart) { - if (chart.inPlot(point)) { - tooltip.set("pointTo", this._display.toGlobal(point)); - } - else { + tooltip.set("pointTo", this._display.toGlobal(point)); + if (!chart.inPlot(point)) { tooltip.hide(); } } diff --git a/src/.internal/charts/xy/axes/DateAxis.ts b/src/.internal/charts/xy/axes/DateAxis.ts index 2a743032..94802ad1 100644 --- a/src/.internal/charts/xy/axes/DateAxis.ts +++ b/src/.internal/charts/xy/axes/DateAxis.ts @@ -454,6 +454,12 @@ export class DateAxis extends ValueAxis { series.setDataSet(series._dataSetId); } this.markDirtySize(); + // solves problem if new series was added + if(this._seriesAdded){ + this._root.events.once("frameended", ()=>{ + this.markDirtySize(); + }) + } } } @@ -607,6 +613,7 @@ export class DateAxis extends ValueAxis { series.setPrivate("outOfSelection", outOfSelection); series.setPrivate("startIndex", startIndex); + series.setPrivate("adjustedStartIndex", series._adjustStartIndex(startIndex)); series.setPrivate("endIndex", endIndex); } }) diff --git a/src/.internal/charts/xy/series/XYSeries.ts b/src/.internal/charts/xy/series/XYSeries.ts index 8d661727..ca968d3e 100644 --- a/src/.internal/charts/xy/series/XYSeries.ts +++ b/src/.internal/charts/xy/series/XYSeries.ts @@ -813,7 +813,7 @@ export interface IXYSeriesPrivate extends ISeriesPrivate { outOfSelection?: boolean; - doNotUpdateLegend?:boolean; + doNotUpdateLegend?: boolean; } @@ -975,6 +975,14 @@ export abstract class XYSeries extends Series { this.states.create("hidden", { opacity: 1, visible: false }); + this.onPrivate("startIndex", ()=>{ + this.updateLegendValue(); + }) + + this.onPrivate("endIndex", ()=>{ + this.updateLegendValue(); + }) + this._makeFieldNames(); } @@ -1365,7 +1373,7 @@ export abstract class XYSeries extends Series { this._dataGrouped = true; } - if (this._valuesDirty || this.isPrivateDirty("startIndex") || this.isPrivateDirty("endIndex") || this.isDirty("vcx") || this.isDirty("vcy") || this._stackDirty || this._sizeDirty) { + if (this._valuesDirty || this.isPrivateDirty("startIndex") || this.isPrivateDirty("adjustedStartIndex") || this.isPrivateDirty("endIndex") || this.isDirty("vcx") || this.isDirty("vcy") || this._stackDirty || this._sizeDirty) { let startIndex = this.startIndex(); let endIndex = this.endIndex(); let minBulletDistance = this.get("minBulletDistance", 0); @@ -1378,7 +1386,7 @@ export abstract class XYSeries extends Series { } } - if ((this._psi != startIndex || this._pei != endIndex || this.isDirty("vcx") || this.isDirty("vcy") || this._stackDirty || this._valuesDirty) && !this._selectionProcessed) { + if ((this._psi != startIndex || this._pei != endIndex || this.isDirty("vcx") || this.isDirty("vcy") || this.isPrivateDirty("adjustedStartIndex") || this._stackDirty || this._valuesDirty) && !this._selectionProcessed) { this._selectionProcessed = true; const vcx = this.get("vcx", 1); @@ -2100,7 +2108,7 @@ export abstract class XYSeries extends Series { * @param dataItem Data item */ public showDataItemTooltip(dataItem: DataItem | undefined) { - if(!this.getPrivate("doNotUpdateLegend")){ + if (!this.getPrivate("doNotUpdateLegend")) { this.updateLegendMarker(dataItem); this.updateLegendValue(dataItem); } @@ -2316,4 +2324,46 @@ export abstract class XYSeries extends Series { public get mainDataItems(): Array> { return this._mainDataItems; } + + /** + * @ignore + */ + public _adjustStartIndex(index: number): number { + const xAxis = this.get("xAxis"); + const baseAxis = this.get("baseAxis"); + + if (baseAxis == xAxis && xAxis.isType>("DateAxis")) { + const baseDuration = xAxis.baseDuration(); + const minSelection = xAxis.getPrivate("selectionMin", xAxis.getPrivate("min", 0)); + const dl = baseDuration * this.get("locationX", 0.5); + + let value = -Infinity; + + while (value < minSelection) { + const dataItem = this.dataItems[index]; + if (dataItem) { + const open = dataItem.open; + if (open) { + value = open["valueX"]; + } + else { + value = dataItem.get("valueX", 0); + } + value += dl; + + if (value < minSelection) { + index++; + } + else { + break; + } + } + else { + break; + } + } + } + + return index; + } } diff --git a/src/.internal/core/Classes.ts b/src/.internal/core/Classes.ts index de4d3b26..ecc8ba87 100644 --- a/src/.internal/core/Classes.ts +++ b/src/.internal/core/Classes.ts @@ -224,6 +224,8 @@ import type { XYCursor } from "./../charts/xy/XYCursor.js"; import type { XYSeries } from "./../charts/xy/series/XYSeries.js"; import type { ZigZag } from "./../charts/stock/indicators/ZigZag.js"; import type { ZoomControl } from "./../charts/map/ZoomControl.js"; +import type { ZoomTools } from "./render/ZoomTools.js"; +import type { ZoomableContainer } from "./render/ZoomableContainer.js"; export interface IClasses { "AccelerationBands": AccelerationBands; @@ -447,4 +449,6 @@ export interface IClasses { "XYSeries": XYSeries; "ZigZag": ZigZag; "ZoomControl": ZoomControl; + "ZoomTools": ZoomTools; + "ZoomableContainer": ZoomableContainer; } diff --git a/src/.internal/core/Registry.ts b/src/.internal/core/Registry.ts index 16a1a6a6..ef495388 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.7.7"; + readonly version: string = "5.8.0"; /** * List of applied licenses. diff --git a/src/.internal/core/render/Container.ts b/src/.internal/core/render/Container.ts index ad3c8848..cc0f50e6 100644 --- a/src/.internal/core/render/Container.ts +++ b/src/.internal/core/render/Container.ts @@ -111,15 +111,10 @@ export interface IContainerEvents extends ISpriteEvents { } export interface IContainerPrivate extends ISpritePrivate { - /** * A `
` element used for HTML content of the `Container`. */ htmlElement?: HTMLDivElement; - -} - -export interface IContainerEvents extends ISpriteEvents { } /** diff --git a/src/.internal/core/render/HeatLegend.ts b/src/.internal/core/render/HeatLegend.ts index 8e6642d0..444ff87c 100644 --- a/src/.internal/core/render/HeatLegend.ts +++ b/src/.internal/core/render/HeatLegend.ts @@ -177,7 +177,7 @@ export class HeatLegend extends Container { let background = tooltip.get("background"); if (background) { - background.set("fill", Color.interpolate(c, startColor, endColor)) + background.set("fill", color) } tooltip.set("pointTo", p); tooltip.show(); diff --git a/src/.internal/core/render/Series.ts b/src/.internal/core/render/Series.ts index b6fb42f1..84c1cd28 100644 --- a/src/.internal/core/render/Series.ts +++ b/src/.internal/core/render/Series.ts @@ -206,6 +206,7 @@ export interface ISeriesPrivate extends IComponentPrivate { chart?: Chart; startIndex?: number; endIndex?: number; + adjustedStartIndex?:number; valueAverage?: number; valueCount?: number; @@ -437,7 +438,7 @@ export abstract class Series extends Component { } } - if ((this._psi != startIndex || this._pei != endIndex) && !this._selectionAggregatesCalculated) { + if ((this._psi != startIndex || this._pei != endIndex || this.isPrivateDirty("adjustedStartIndex")) && !this._selectionAggregatesCalculated) { if (startIndex === 0 && endIndex === this.dataItems.length && this._aggregatesCalculated) { // void } @@ -499,6 +500,13 @@ export abstract class Series extends Component { } + /** + * @ignore + */ + public _adjustStartIndex(index:number):number{ + return index; + } + protected _calculateAggregates(startIndex: number, endIndex: number) { let fields = this._valueFields; @@ -537,8 +545,9 @@ export abstract class Series extends Component { } const baseValueSeries = this.getPrivate("baseValueSeries"); + const adjustedStartIndex = this.getPrivate("adjustedStartIndex", startIndex); - for (let i = startIndex; i < endIndex; i++) { + for (let i = adjustedStartIndex; i < endIndex; i++) { const dataItem = this.dataItems[i]; let value = dataItem.get(key) @@ -582,6 +591,40 @@ export abstract class Series extends Component { previous[key] = value; } } + + if(endIndex < this.dataItems.length - 1){ + const dataItem = this.dataItems[endIndex]; + let value = dataItem.get(key) + dataItem.setRaw((changePrevious), value - previous[openKey]); + dataItem.setRaw((changePreviousPercent), (value - previous[openKey]) / previous[openKey] * 100); + dataItem.setRaw((changeSelection), value - open[openKey]); + dataItem.setRaw((changeSelectionPercent), (value - open[openKey]) / open[openKey] * 100); + } + + if(startIndex > 0){ + startIndex--; + } + + delete previous[key]; + + for (let i = startIndex; i < adjustedStartIndex; i++) { + const dataItem = this.dataItems[i]; + + let value = dataItem.get(key); + + if (previous[key] == null) { + previous[key] = value; + } + + if (value != null) { + dataItem.setRaw((changePrevious), value - previous[openKey]); + dataItem.setRaw((changePreviousPercent), (value - previous[openKey]) / previous[openKey] * 100); + dataItem.setRaw((changeSelection), value - open[openKey]); + dataItem.setRaw((changeSelectionPercent), (value - open[openKey]) / open[openKey] * 100); + + previous[key] = value; + } + } }) $array.each(fields, (key) => { @@ -689,36 +732,31 @@ export abstract class Series extends Component { } if(this.get("visible")){ - //if (this.bullets.length > 0) { - let count = this.dataItems.length; - let startIndex = this.startIndex(); - let endIndex = this.endIndex(); + let count = this.dataItems.length; + let startIndex = this.startIndex(); + let endIndex = this.endIndex(); - if(endIndex < count){ - endIndex++; - } - if(startIndex > 0){ - startIndex--; - } + if(endIndex < count){ + endIndex++; + } + if(startIndex > 0){ + startIndex--; + } - for (let i = 0; i < startIndex; i++) { - this._hideBullets(this.dataItems[i]); - } + for (let i = 0; i < startIndex; i++) { + this._hideBullets(this.dataItems[i]); + } - for (let i = startIndex; i < endIndex; i++) { - this._positionBullets(this.dataItems[i]); - } + for (let i = startIndex; i < endIndex; i++) { + this._positionBullets(this.dataItems[i]); + } - for (let i = endIndex; i < count; i++) { - this._hideBullets(this.dataItems[i]); - } - //} + for (let i = endIndex; i < count; i++) { + this._hideBullets(this.dataItems[i]); + } } } - - - public _positionBullets(dataItem: DataItem){ if(dataItem.bullets){ $array.each(dataItem.bullets, (bullet) => { diff --git a/src/.internal/core/render/Sprite.ts b/src/.internal/core/render/Sprite.ts index 5c2c1a3e..c4c78f39 100644 --- a/src/.internal/core/render/Sprite.ts +++ b/src/.internal/core/render/Sprite.ts @@ -893,7 +893,7 @@ export abstract class Sprite extends Entity { protected _sizeDirty: boolean = false; // Will be true only when dragging - protected _isDragging: boolean = false; + public _isDragging: boolean = false; // The event when the dragging starts protected _dragEvent: ISpritePointerEvent | undefined; @@ -1703,13 +1703,18 @@ export abstract class Sprite extends Entity { let angle = 0; let parent = this.parent; + let scale = 1; + while (parent != null) { angle += parent.get("rotation", 0); - parent = parent.parent; + parent = parent.parent; + if(parent){ + scale *= parent.get("scale", 1); + } } - let x = e.point.x - dragEvent.point.x; - let y = e.point.y - dragEvent.point.y; + let x = (e.point.x - dragEvent.point.x) / scale; + let y = (e.point.y - dragEvent.point.y) / scale; const events = this.events; diff --git a/src/.internal/core/render/ZoomTools.ts b/src/.internal/core/render/ZoomTools.ts new file mode 100644 index 00000000..a017e25b --- /dev/null +++ b/src/.internal/core/render/ZoomTools.ts @@ -0,0 +1,107 @@ +import { Container, IContainerPrivate, IContainerSettings, IContainerEvents } from "../../core/render/Container"; +import { Button } from "../../core/render/Button"; +import { Graphics } from "../../core/render/Graphics"; +import { MultiDisposer } from "../../core/util/Disposer"; + +export interface IZoomable { + zoomIn(): void; + zoomOut(): void; + goHome(): void; +} + +export interface IZoomToolsSettings extends IContainerSettings { + + /** + * A target element that zoom tools will control, e.g. [[ZoomableContainer]]. + */ + target?: IZoomable; + +} + +export interface IZoomToolsPrivate extends IContainerPrivate { +} + +export interface IZoomToolsEvents extends IContainerEvents { +} + +/** + * A tool that displays button for zoomable targets. + * + * @since 5.8.0 + * @important + */ +export class ZoomTools extends Container { + + public static className: string = "ZoomTools"; + public static classNames: Array = Container.classNames.concat([ZoomTools.className]); + declare public _events: IContainerEvents; + + /** + * A [[Button]] for home. + */ + public readonly homeButton: Button = this.children.push(Button.new(this._root, { width: 35, height: 35, themeTags: ["home"] })); + + /** + * A [[Button]] for zoom in. + */ + public readonly plusButton: Button = this.children.push(Button.new(this._root, { width: 35, height: 35, themeTags: ["plus"] })); + + /** + * A [[Button]] for zoom out. + */ + public readonly minusButton: Button = this.children.push(Button.new(this._root, { width: 35, height: 35, themeTags: ["minus"] })); + + declare public _settings: IZoomToolsSettings; + declare public _privateSettings: IZoomToolsPrivate; + + protected _disposer: MultiDisposer | undefined; + + protected _afterNew() { + super._afterNew(); + + this.set("position", "absolute"); + + this.set("layout", this._root.verticalLayout); + this.addTag("zoomtools"); + + this.plusButton.setAll({ + icon: Graphics.new(this._root, { themeTags: ["icon"] }), + layout: undefined + }); + + this.minusButton.setAll({ + icon: Graphics.new(this._root, { themeTags: ["icon"] }), + layout: undefined + }); + + this.homeButton.setAll({ + icon: Graphics.new(this._root, { themeTags: ["icon"] }), + layout: undefined + }); + } + + public _prepareChildren() { + super._prepareChildren(); + + if (this.isDirty("target")) { + const target = this.get("target"); + const previous = this._prevSettings.target; + if (target) { + this._disposer = new MultiDisposer([ + this.plusButton.events.on("click", () => { + target.zoomIn() + }), + this.minusButton.events.on("click", () => { + target.zoomOut() + }), + this.homeButton.events.on("click", () => { + target.goHome() + })]) + } + + if (previous && this._disposer) { + this._disposer.dispose(); + } + } + } +} diff --git a/src/.internal/core/render/ZoomableContainer.ts b/src/.internal/core/render/ZoomableContainer.ts new file mode 100644 index 00000000..8cee4300 --- /dev/null +++ b/src/.internal/core/render/ZoomableContainer.ts @@ -0,0 +1,376 @@ +import type { Time } from "../../core/util/Animation"; +import type { Animation } from "../../core/util/Entity"; +import type { IDisposer } from "../../core/util/Disposer"; +import type { IPoint } from "../../core/util/IPoint"; +import type { ISpritePointerEvent } from "../../core/render/Sprite"; + +import { Container, IContainerSettings, IContainerPrivate, IContainerEvents } from "../../core/render/Container"; +import { p100 } from "../../core/util/Percent"; +import { Rectangle } from "../../core/render/Rectangle"; +import { color } from "../../core/util/Color"; + +import * as $utils from "../../core/util/Utils"; +import * as $math from "../../core/util/Math"; +import * as $object from "../../core/util/Object"; + +export interface IZoomableContainerSettings extends IContainerSettings { + + /** + * Maximum zoom-in level. + * + * @default 32 + */ + maxZoomLevel?: number; + + /** + * Maximum zoom-out level. + * + * @default 1 + */ + minZoomLevel?: number; + + /** + * Zoom level increase/decrease factor with each zoom action. + * + * @defult 2 + */ + zoomStep?: number; + + /** + * Pinch-zooming is enabled on touch devices. + * + * @default true + */ + pinchZoom?: boolean; + + /** + * Animation duration (ms) for zoom animations. + * + * @default 600 + */ + animationDuration?: number; + + /** + * Easing function to use for zoom animations. + * + * @default am5.ease.out(am5.ease.cubic) + */ + animationEasing?: (t: Time) => Time; + +} + +export interface IZoomableContainerPrivate extends IContainerPrivate { +} + +export interface IZoomableContainerEvents extends IContainerEvents { +} + +/** + * A version of [[Container]] which adds zooming capabilities. + * + * @see {@link https://www.amcharts.com/docs/v5/concepts/common-elements/containers/#Zoomable_container} for more info + * @since 5.8.0 + * @important + */ +export class ZoomableContainer extends Container { + public static className: string = "ZoomableContainer"; + public static classNames: Array = Container.classNames.concat([ZoomableContainer.className]); + + declare public _settings: IZoomableContainerSettings; + declare public _privateSettings: IZoomableContainerPrivate; + declare public _events: IZoomableContainerEvents; + + protected _za?: Animation; + protected _txa?: Animation; + protected _tya?: Animation; + + + protected _movePoints: { [index: number]: IPoint } = {}; + protected _downScale: number = 1; + protected _downX: number = 0; + protected _downY: number = 0; + + protected _pinchDP?: IDisposer; + + /** + * All elements must be added to `contents.children` instead of `children` of + * [[ZoomableContainer]]. + * + * @see {@link https://www.amcharts.com/docs/v5/concepts/common-elements/containers/#Zoomable_container} for more info + */ + public contents = this.children.push(Container.new(this._root, { + width: p100, + height: p100, + x: 0, + y: 0, + draggable: true, + background: Rectangle.new(this._root, { + fill: color(0xffffff), + fillOpacity: 0 + }) + })) + + protected _wheelDp: IDisposer | undefined; + + protected _afterNew(): void { + super._afterNew(); + + const events = this.contents.events; + + this._disposers.push(events.on("pointerdown", (event) => { + this._handleThisDown(event); + })); + + this._disposers.push(events.on("globalpointerup", (event) => { + this._handleThisUp(event); + })); + + this._disposers.push(events.on("globalpointermove", (event) => { + this._handleThisMove(event); + })); + } + + public _prepareChildren() { + super._prepareChildren(); + if (this.isDirty("wheelable")) { + this._handleSetWheel(); + } + + this.contents._display.cancelTouch = this.get("pinchZoom", false); + } + + + protected _handleSetWheel() { + // const contents = this.contents; + + if (this.get("wheelable")) { + if (this._wheelDp) { + this._wheelDp.dispose(); + } + + this._wheelDp = this.events.on("wheel", (event) => { + const wheelEvent = event.originalEvent; + + // Ignore wheel event if it is happening on a non-ZoomableContainer element, e.g. if + // some page element is over the ZoomableContainer. + if ($utils.isLocalEvent(wheelEvent, this)) { + wheelEvent.preventDefault(); + } + else { + return; + } + + const point = this.toLocal(event.point); + + this._handleWheelZoom(wheelEvent.deltaY, point); + }); + + this._disposers.push(this._wheelDp); + } + else { + if (this._wheelDp) { + this._wheelDp.dispose(); + } + } + } + + protected _handleWheelZoom(delta: number, point: IPoint) { + let step = this.get("zoomStep", 2); + let zoomLevel = this.contents.get("scale", 1); + + let newZoomLevel = zoomLevel; + if (delta > 0) { + newZoomLevel = zoomLevel / step; + } + else if (delta < 0) { + newZoomLevel = zoomLevel * step; + } + + if (newZoomLevel != zoomLevel) { + this.zoomToPoint(point, newZoomLevel) + } + } + + /** + * Zooms to specific X/Y point. + * + * @param point Center point + * @param level Zoom level + * @return Zoom Animation object + */ + public zoomToPoint(point: IPoint, level: number): Animation | undefined { + if (level) { + level = $math.fitToRange(level, this.get("minZoomLevel", 1), this.get("maxZoomLevel", 32)); + } + + const zoomLevel = this.contents.get("scale", 1); + + let x = point.x; + let y = point.y; + + let cx = x; + let cy = y; + + const contents = this.contents; + + let tx = contents.x(); + let ty = contents.y(); + + let xx = cx - ((x - tx) / zoomLevel * level); + let yy = cy - ((y - ty) / zoomLevel * level); + + this._animateTo(xx, yy, level); + + return this._za; + } + + /** + * Zooms the container contents in by `zoomStep`. + * + * @return Zoom Animation object + */ + public zoomIn(): Animation | undefined { + return this.zoomToPoint({ x: this.width() / 2, y: this.height() / 2 }, this.contents.get("scale", 1) * this.get("zoomStep", 2)); + } + + /** + * Zooms the container contents out by `zoomStep`. + * + * @return Zoom Animation object + */ + public zoomOut(): Animation | undefined { + return this.zoomToPoint({ x: this.width() / 2, y: this.height() / 2 }, this.contents.get("scale", 1) / this.get("zoomStep", 2)); + } + + /** + * Fully zooms out the container contents. + * + * @return Zoom Animation object + */ + public goHome() { + return this._animateTo(0, 0, 1); + } + + protected _animateTo(x: number, y: number, scale: number) { + const duration = this.get("animationDuration", 0); + const easing = this.get("animationEasing"); + + const contents = this.contents; + this._txa = contents.animate({ key: "x", to: x, duration: duration, easing: easing }); + this._tya = contents.animate({ key: "y", to: y, duration: duration, easing: easing }); + this._za = contents.animate({ key: "scale", to: scale, duration: duration, easing: easing }); + } + + + protected _handleThisUp(_event: ISpritePointerEvent) { + this._downPoints = {} + } + + protected _handleThisDown(event: ISpritePointerEvent) { + + const contents = this.contents; + + this._downScale = contents.get("scale", 1); + const downPoints = contents._downPoints; + + let count = $object.keys(downPoints).length; + + if (count == 1) { + // workaround to solve a problem when events are added to some children of chart container (rotation stops working) + let downPoint = downPoints[1]; + if (!downPoint) { + downPoint = downPoints[0]; + } + + if (downPoint && (downPoint.x == event.point.x && downPoint.y == event.point.y)) { + count = 0; + } + } + + if (count > 0) { + this._downX = contents.x(); + this._downY = contents.y(); + + const downId = contents._getDownPointId(); + if (downId) { + let movePoint = this._movePoints[downId]; + if (movePoint) { + contents._downPoints[downId] = movePoint; + } + } + } + } + + + protected _handleThisMove(event: ISpritePointerEvent) { + const originalEvent = event.originalEvent as any; + + const pointerId = originalEvent.pointerId; + + if (this.get("pinchZoom")) { + if (pointerId) { + this._movePoints[pointerId] = event.point; + + if ($object.keys(this.contents._downPoints).length > 1) { + this._handlePinch(); + return; + } + } + } + } + + protected _handlePinch() { + let i = 0; + let downPoints: Array = []; + let movePoints: Array = []; + + $object.each(this.contents._downPoints, (k, point) => { + downPoints[i] = point; + let movePoint = this._movePoints[k]; + if (movePoint) { + movePoints[i] = movePoint; + } + i++; + }); + + if (downPoints.length > 1 && movePoints.length > 1) { + + this.contents._isDragging = false; + + let downPoint0 = downPoints[0]; + let downPoint1 = downPoints[1]; + + let movePoint0 = movePoints[0]; + let movePoint1 = movePoints[1]; + + if (downPoint0 && downPoint1 && movePoint0 && movePoint1) { + downPoint0 = this.toLocal(downPoint0); + downPoint1 = this.toLocal(downPoint1); + + movePoint0 = this.toLocal(movePoint0); + movePoint1 = this.toLocal(movePoint1); + + let initialDistance = Math.hypot(downPoint1.x - downPoint0.x, downPoint1.y - downPoint0.y); + let currentDistance = Math.hypot(movePoint1.x - movePoint0.x, movePoint1.y - movePoint0.y); + + let level = currentDistance / initialDistance * this._downScale; + let moveCenter = { x: movePoint0.x + (movePoint1.x - movePoint0.x) / 2, y: movePoint0.y + (movePoint1.y - movePoint0.y) / 2 }; + let downCenter = { x: downPoint0.x + (downPoint1.x - downPoint0.x) / 2, y: downPoint0.y + (downPoint1.y - downPoint0.y) / 2 }; + + let tx = this._downX || 0; + let ty = this._downY || 0; + + let zoomLevel = this._downScale; + + let xx = moveCenter.x - (- tx + downCenter.x) / zoomLevel * level; + let yy = moveCenter.y - (- ty + downCenter.y) / zoomLevel * level; + + this.contents.setAll({ + x: xx, + y: yy, + scale: level + }); + } + } + } +} \ No newline at end of file diff --git a/src/.internal/core/util/Time.ts b/src/.internal/core/util/Time.ts index 6a99e28a..f95c5411 100644 --- a/src/.internal/core/util/Time.ts +++ b/src/.internal/core/util/Time.ts @@ -306,8 +306,15 @@ export function add(date: Date, unit: TimeUnit, count: number, utc?: boolean, ti break; case "month": + const endDays = date.getUTCDate(); + const startDays = new Date(date.getUTCFullYear(), date.getUTCMonth(), 0).getUTCDate(); let month: number = date.getUTCMonth(); - date.setUTCMonth(month + count); + if (endDays > startDays) { + date.setUTCMonth(month + count, startDays); + } + else { + date.setUTCMonth(month + count); + } break; case "week": diff --git a/src/.internal/plugins/json/Classes-script.ts b/src/.internal/plugins/json/Classes-script.ts index ed26cfc1..b8e90628 100644 --- a/src/.internal/plugins/json/Classes-script.ts +++ b/src/.internal/plugins/json/Classes-script.ts @@ -224,6 +224,8 @@ import type { XYCursor } from "./../../../xy"; import type { XYSeries } from "./../../../xy"; import type { ZigZag } from "./../../../stock"; import type { ZoomControl } from "./../../../map"; +import type { ZoomTools } from "./../../../index"; +import type { ZoomableContainer } from "./../../../index"; export interface IClasses { "AccelerationBands": () => Promise; @@ -447,6 +449,8 @@ export interface IClasses { "XYSeries": () => Promise; "ZigZag": () => Promise; "ZoomControl": () => Promise; + "ZoomTools": () => Promise; + "ZoomableContainer": () => Promise; } const classes: IClasses = { @@ -671,6 +675,8 @@ const classes: IClasses = { "XYSeries": () => import(/* webpackExports: "XYSeries", webpackMode: "weak" */ "./../../../xy").then((m) => m.XYSeries), "ZigZag": () => import(/* webpackExports: "ZigZag", webpackMode: "weak" */ "./../../../stock").then((m) => m.ZigZag), "ZoomControl": () => import(/* webpackExports: "ZoomControl", webpackMode: "weak" */ "./../../../map").then((m) => m.ZoomControl), + "ZoomTools": () => import(/* webpackExports: "ZoomTools", webpackMode: "weak" */ "./../../../index").then((m) => m.ZoomTools), + "ZoomableContainer": () => import(/* webpackExports: "ZoomableContainer", webpackMode: "weak" */ "./../../../index").then((m) => m.ZoomableContainer), }; export default classes; diff --git a/src/.internal/plugins/json/Classes.ts b/src/.internal/plugins/json/Classes.ts index 76fdafa6..c9cb1983 100644 --- a/src/.internal/plugins/json/Classes.ts +++ b/src/.internal/plugins/json/Classes.ts @@ -224,6 +224,8 @@ import type { XYCursor } from "./../../../xy"; import type { XYSeries } from "./../../../xy"; import type { ZigZag } from "./../../../stock"; import type { ZoomControl } from "./../../../map"; +import type { ZoomTools } from "./../../../index"; +import type { ZoomableContainer } from "./../../../index"; export interface IClasses { "AccelerationBands": () => Promise; @@ -447,6 +449,8 @@ export interface IClasses { "XYSeries": () => Promise; "ZigZag": () => Promise; "ZoomControl": () => Promise; + "ZoomTools": () => Promise; + "ZoomableContainer": () => Promise; } const classes: IClasses = { @@ -671,6 +675,8 @@ const classes: IClasses = { "XYSeries": () => import(/* webpackExports: "XYSeries", webpackChunkName: "json_xy" */ "./../../../xy").then((m) => m.XYSeries), "ZigZag": () => import(/* webpackExports: "ZigZag", webpackChunkName: "json_stock" */ "./../../../stock").then((m) => m.ZigZag), "ZoomControl": () => import(/* webpackExports: "ZoomControl", webpackChunkName: "json_map" */ "./../../../map").then((m) => m.ZoomControl), + "ZoomTools": () => import(/* webpackExports: "ZoomTools", webpackChunkName: "json_index" */ "./../../../index").then((m) => m.ZoomTools), + "ZoomableContainer": () => import(/* webpackExports: "ZoomableContainer", webpackChunkName: "json_index" */ "./../../../index").then((m) => m.ZoomableContainer), }; export default classes; diff --git a/src/.internal/themes/DefaultTheme.ts b/src/.internal/themes/DefaultTheme.ts index c76b3025..6e414182 100644 --- a/src/.internal/themes/DefaultTheme.ts +++ b/src/.internal/themes/DefaultTheme.ts @@ -136,6 +136,18 @@ export class DefaultTheme extends Theme { interactiveChildren: false }); + r("ZoomableContainer").setAll({ + width: p100, + height: p100, + wheelable: true, + pinchZoom: true, + maxZoomLevel: 32, + minZoomLevel: 1, + zoomStep: 2, + animationEasing: $ease.out($ease.cubic), + animationDuration: 600 + }); + /** * ------------------------------------------------------------------------ @@ -1043,5 +1055,45 @@ export class DefaultTheme extends Theme { setColor(rule, "stroke", ic, "alternativeBackground"); } + + r("Graphics", ["button", "plus", "icon"]).setAll({ + x: p50, + y: p50, + draw: (display) => { + display.moveTo(-4, 0); + display.lineTo(4, 0); + display.moveTo(0, -4); + display.lineTo(0, 4); + } + }); + + r("Graphics", ["button", "minus", "icon"]).setAll({ + x: p50, + y: p50, + draw: (display) => { + display.moveTo(-4, 0); + display.lineTo(4, 0); + } + }); + + r("Graphics", ["button", "home", "icon"]).setAll({ + x: p50, + y: p50, + svgPath: "M 8 -1 L 6 -1 L 6 7 L 2 7 L 2 1 L -2 1 L -2 7 L -6 7 L -6 -1 L -8 -1 L 0 -9 L 8 -1 Z M 8 -1" + }); + + r("Button", ["zoomtools"]).setAll({ + marginTop: 1, + marginBottom: 2 + }) + + r("ZoomTools").setAll({ + x: p100, + centerX: p100, + y: p100, + centerY: p100, + paddingRight: 10, + paddingBottom: 10 + }) } } diff --git a/src/index.ts b/src/index.ts index 2d474ea3..618290a3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,6 +14,7 @@ export { Ellipse, IEllipseSettings, IEllipsePrivate } from "./.internal/core/ren export { Star, IStarSettings, IStarPrivate } from "./.internal/core/render/Star"; export { Component, DataItem, IComponentSettings, IComponentPrivate, IComponentEvents } from "./.internal/core/render/Component"; export { Container, IContainerSettings, IContainerPrivate, IContainerEvents } from "./.internal/core/render/Container"; +export { ZoomableContainer, IZoomableContainerSettings, IZoomableContainerPrivate, IZoomableContainerEvents } from "./.internal/core/render/ZoomableContainer"; export { Graphics, IGraphicsSettings, IGraphicsPrivate, IGraphicsEvents } from "./.internal/core/render/Graphics"; export { GridLayout } from "./.internal/core/render/GridLayout"; export { HeatLegend, IHeatLegendSettings, IHeatLegendPrivate } from "./.internal/core/render/HeatLegend"; @@ -42,6 +43,8 @@ export { Tooltip, ITooltipSettings, ITooltipPrivate } from "./.internal/core/ren export { VerticalLayout } from "./.internal/core/render/VerticalLayout"; export { Timezone } from "./.internal/core/util/Timezone"; +export { ZoomTools, IZoomToolsSettings, IZoomToolsPrivate, IZoomToolsEvents } from "./.internal/core/render/ZoomTools"; + export { GrainPattern } from "./.internal/core/render/patterns/GrainPattern"; export { BlendMode } from "./.internal/core/render/backend/Renderer"; @@ -73,6 +76,7 @@ export { TextFormatter } from "./.internal/core/util/TextFormatter"; export { SpriteResizer, ISpriteResizerPrivate, ISpriteResizerEvents, ISpriteResizerSettings } from "./.internal/core/render/SpriteResizer"; + export type { IBounds } from "./.internal/core/util/IBounds"; export type { IGeoPoint } from "./.internal/core/util/IGeoPoint"; export type { IPoint } from "./.internal/core/util/IPoint";