@amcharts/amcharts5
Version:
amCharts 5
1,419 lines • 65.1 kB
JavaScript
;
import { XYChartDefaultTheme } from "./XYChartDefaultTheme";
import { Container } from "../../core/render/Container";
import { Rectangle } from "../../core/render/Rectangle";
import { SerialChart } from "../../core/render/SerialChart";
import { ListAutoDispose } from "../../core/util/List";
import { p100 } from "../../core/util/Percent";
import { Button } from "../../core/render/Button";
import { Graphics } from "../../core/render/Graphics";
import { Percent } from "../../core/util/Percent";
import * as $array from "../../core/util/Array";
import * as $type from "../../core/util/Type";
import * as $order from "../../core/util/Order";
import * as $object from "../../core/util/Object";
import * as $utils from "../../core/util/Utils";
import * as $math from "../../core/util/Math";
/**
* Creates an XY chart.
*
* @see {@link https://www.amcharts.com/docs/v5/charts/xy-chart/} for more info
* @important
*/
export class XYChart extends SerialChart {
constructor() {
super(...arguments);
/**
* A list of horizontal axes.
*/
this.xAxes = new ListAutoDispose();
/**
* A list of vertical axes.
*/
this.yAxes = new ListAutoDispose();
/**
* A [[Container]] located on top of the chart, used to store top horizontal
* axes.
*
* @see {@link https://www.amcharts.com/docs/v5/charts/xy-chart/xy-chart-containers/} for more info
* @default Container.new()
*/
this.topAxesContainer = this.chartContainer.children.push(Container.new(this._root, { width: p100, layout: this._root.verticalLayout })._markC("width", "layout"));
/**
* A [[Container]] located in the middle the chart, used to store vertical axes
* and plot area container.
*
* @see {@link https://www.amcharts.com/docs/v5/charts/xy-chart/xy-chart-containers/} for more info
* @default Container.new()
*/
this.yAxesAndPlotContainer = this.chartContainer.children.push(Container.new(this._root, { width: p100, height: p100, layout: this._root.horizontalLayout })._markC("width", "height", "layout"));
/**
* A [[Container]] located on bottom of the chart, used to store bottom
* horizontal axes.
*
* @see {@link https://www.amcharts.com/docs/v5/charts/xy-chart/xy-chart-containers/} for more info
* @default Container.new()
*/
this.bottomAxesContainer = this.chartContainer.children.push(Container.new(this._root, { width: p100, layout: this._root.verticalLayout })._markC("width", "layout"));
/**
* A [[Container]] located on left of the chart, used to store left-hand
* vertical axes.
*
* @see {@link https://www.amcharts.com/docs/v5/charts/xy-chart/xy-chart-containers/} for more info
* @default Container.new()
*/
this.leftAxesContainer = this.yAxesAndPlotContainer.children.push(Container.new(this._root, { height: p100, layout: this._root.horizontalLayout })._markC("height", "layout"));
/**
* A [[Container]] located in the middle of the chart, used to store plotContainer and topPlotContainer
*
* @see {@link https://www.amcharts.com/docs/v5/charts/xy-chart/xy-chart-containers/} for more info
* @default Container.new()
*/
this.plotsContainer = this.yAxesAndPlotContainer.children.push(Container.new(this._root, { width: p100, height: p100, maskContent: false })._markC("width", "height", "maskContent"));
/**
* A [[Container]] located in the middle of the chart, used to store actual
* plots (series).
*
* NOTE: `plotContainer` will automatically have its `background` preset. If
* you need to modify background or outline for chart's plot area, you can
* use `plotContainer.get("background")` for that.*
*
* @see {@link https://www.amcharts.com/docs/v5/charts/xy-chart/xy-chart-containers/} for more info
* @default Container.new()
*/
this.plotContainer = this.plotsContainer.children.push(Container.new(this._root, { width: p100, height: p100 })._markC("width", "height"));
/**
* A [[Container]] used for any elements that need to be displayed over
* regular `plotContainer`.
*
* @see {@link https://www.amcharts.com/docs/v5/charts/xy-chart/xy-chart-containers/} for more info
* @default Container.new()
*/
this.topPlotContainer = this.plotsContainer.children.push(Container.new(this._root, { width: p100, height: p100 })._markC("width", "height"));
/**
* A [[Container]] axis grid elements are stored in.
*
* @see {@link https://www.amcharts.com/docs/v5/charts/xy-chart/xy-chart-containers/} for more info
* @default Container.new()
*/
this.gridContainer = this.plotContainer.children.push(Container.new(this._root, { width: p100, height: p100, isMeasured: false })._markC("width", "height", "isMeasured"));
/**
* A [[Container]] axis background grid elements are stored in.
*
* @see {@link https://www.amcharts.com/docs/v5/charts/xy-chart/xy-chart-containers/} for more info
* @default Container.new()
*/
this.topGridContainer = Container.new(this._root, { width: p100, height: p100, isMeasured: false })._markC("width", "height", "isMeasured");
/**
* A [[Container]] located on right of the chart, used to store right-hand
* vertical axes.
*
* @see {@link https://www.amcharts.com/docs/v5/charts/xy-chart/xy-chart-containers/} for more info
* @default Container.new()
*/
this.rightAxesContainer = this.yAxesAndPlotContainer.children.push(Container.new(this._root, { height: p100, layout: this._root.horizontalLayout })._markC("height", "layout"));
/**
* A [[Container]] axis headers are stored in.
*
* @see {@link https://www.amcharts.com/docs/v5/charts/xy-chart/axes/axis-headers/} for more info
* @default Container.new()
*/
this.axisHeadersContainer = this.plotContainer.children.push(Container.new(this._root, {}));
/**
* A button that is shown when chart is not fully zoomed out.
*
* @see {@link https://www.amcharts.com/docs/v5/charts/xy-chart/zoom-and-pan/#Zoom_out_button} for more info
* @default Button.new()
*/
this.zoomOutButton = this.topPlotContainer.children.push(Button.new(this._root, {
themeTags: ["zoom"],
icon: Graphics.new(this._root, {
themeTags: ["button", "icon"]
})
}));
this._movePoint = { x: 0, y: 0 };
this._movePoints = {};
}
_afterNew() {
this._defaultThemes.push(XYChartDefaultTheme.new(this._root));
super._afterNew();
this._disposers.push(this.xAxes);
this._disposers.push(this.yAxes);
const root = this._root;
let verticalLayout = this._root.verticalLayout;
const zoomOutButton = this.zoomOutButton;
zoomOutButton.events.on("click", () => {
this.zoomOut();
});
zoomOutButton.hide(0);
zoomOutButton.states.lookup("default")._setC("opacity", 1);
this.chartContainer.set("layout", verticalLayout);
const plotContainer = this.plotContainer;
plotContainer.children.push(this.seriesContainer);
this._disposers.push(this._processAxis(this.xAxes, this.bottomAxesContainer));
this._disposers.push(this._processAxis(this.yAxes, this.leftAxesContainer));
plotContainer.children.push(this.topGridContainer);
plotContainer.children.push(this.bulletsContainer);
// Setting trasnparent background so that full body of the plot container
// is interactive
plotContainer._setC("interactive", true);
plotContainer._setC("interactiveChildren", false);
plotContainer._setC("background", Rectangle.new(root, {
themeTags: ["plotbackground", "xy", "background"]
})._markC("themeTags"));
this._disposers.push(plotContainer.events.on("pointerdown", (event) => {
this._handlePlotDown(event);
}));
this._disposers.push(plotContainer.events.on("globalpointerup", (event) => {
this._handlePlotUp(event);
}));
this._disposers.push(plotContainer.events.on("globalpointermove", (event) => {
this._handlePlotMove(event);
}));
this._maskGrid();
this._setUpTouch();
}
_beforeChanged() {
super._beforeChanged();
if (this.isDirty("pinchZoomX") || this.isDirty("pinchZoomY") || this.get("panX") || this.get("panY")) {
this._setUpTouch();
}
}
_setUpTouch() {
if (!this.plotContainer._display.cancelTouch) {
this.plotContainer._display.cancelTouch = (this.get("pinchZoomX") || this.get("pinchZoomY") || this.get("panX") || this.get("panY")) ? true : false;
}
}
_maskGrid() {
this.gridContainer.set("maskContent", true);
this.topGridContainer.set("maskContent", true);
}
_removeSeries(series) {
series._unstack();
if (series._posXDp) {
series._posXDp.dispose();
}
if (series._posYDp) {
series._posYDp.dispose();
}
series.set("baseAxis", undefined);
const xAxis = series.get("xAxis");
if (xAxis) {
$array.remove(xAxis.series, series);
xAxis.markDirtyExtremes();
}
const yAxis = series.get("yAxis");
if (yAxis) {
$array.remove(yAxis.series, series);
yAxis.markDirtyExtremes();
}
const cursor = this.get("cursor");
if (cursor) {
const snapToSeries = cursor.get("snapToSeries");
if (snapToSeries) {
$array.remove(snapToSeries, series);
}
}
super._removeSeries(series);
}
_applyThemes(force = false) {
const colors = this.get("colors");
if (colors) {
colors.reset();
}
const changed = super._applyThemes(force);
// Recolorize all series that might've not been added before theme was
// applied to the chart
this.series.each((series) => {
this._colorize(series);
});
return changed;
}
/**
* This method is invoked when mouse wheel is used over chart's plot
* container, and handles zooming/pan.
*
* You can invoke this method manually, if you need to mimic chart's wheel
* behavior over other elements of the chart.
*/
handleWheel(event) {
const wheelX = this.get("wheelX");
const wheelY = this.get("wheelY");
const plotContainer = this.plotContainer;
const wheelEvent = event.originalEvent;
// Ignore wheel event if it is happening on a non-chart element, e.g. if
// some page element is over the chart.
let prevent = false;
if ($utils.isLocalEvent(wheelEvent, this)) {
prevent = true;
}
else {
return;
}
const plotPoint = plotContainer.toLocal(event.point);
const wheelStep = this.get("wheelStep", 0.2);
const shiftY = wheelEvent.deltaY / 100;
const shiftX = wheelEvent.deltaX / 100;
const wheelZoomPositionX = this.get("wheelZoomPositionX");
const wheelZoomPositionY = this.get("wheelZoomPositionY");
if ((wheelX === "zoomX" || wheelX === "zoomXY") && shiftX != 0) {
this.xAxes.each((axis) => {
if (axis.get("zoomX")) {
let start = axis.get("start");
let end = axis.get("end");
let position = axis.fixPosition(plotPoint.x / plotContainer.width());
if (wheelZoomPositionX != null) {
position = wheelZoomPositionX;
}
let maxDeviation = axis.get("maxDeviation", 0);
let newStart = Math.min(1 + maxDeviation, Math.max(-maxDeviation, start - wheelStep * (end - start) * shiftX * position));
let newEnd = Math.max(-maxDeviation, Math.min(1 + maxDeviation, end + wheelStep * (end - start) * shiftX * (1 - position)));
if (newStart == start && newEnd == end) {
prevent = false;
}
if (1 / (newEnd - newStart) < axis.getPrivate("maxZoomFactor", Infinity) / axis.get("minZoomCount", 1)) {
this._handleWheelAnimation(axis.zoom(newStart, newEnd));
}
else {
prevent = false;
}
}
});
}
if ((wheelY === "zoomX" || wheelY === "zoomXY") && shiftY != 0) {
this.xAxes.each((axis) => {
if (axis.get("zoomX")) {
let start = axis.get("start");
let end = axis.get("end");
let position = axis.fixPosition(plotPoint.x / plotContainer.width());
if (wheelZoomPositionX != null) {
position = wheelZoomPositionX;
}
let maxDeviation = axis.get("maxDeviation", 0);
let newStart = Math.min(1 + maxDeviation, Math.max(-maxDeviation, start - wheelStep * (end - start) * shiftY * position));
let newEnd = Math.max(-maxDeviation, Math.min(1 + maxDeviation, end + wheelStep * (end - start) * shiftY * (1 - position)));
if (newStart == start && newEnd == end) {
prevent = false;
}
if (1 / (newEnd - newStart) < axis.getPrivate("maxZoomFactor", Infinity) / axis.get("minZoomCount", 1)) {
this._handleWheelAnimation(axis.zoom(newStart, newEnd));
}
else {
prevent = false;
}
}
});
}
if ((wheelX === "zoomY" || wheelX === "zoomXY") && shiftX != 0) {
this.yAxes.each((axis) => {
if (axis.get("zoomY")) {
let start = axis.get("start");
let end = axis.get("end");
let position = axis.fixPosition(plotPoint.y / plotContainer.height());
if (wheelZoomPositionY != null) {
position = wheelZoomPositionY;
}
let maxDeviation = axis.get("maxDeviation", 0);
let newStart = Math.min(1 + maxDeviation, Math.max(-maxDeviation, start - wheelStep * (end - start) * shiftX * position));
let newEnd = Math.max(-maxDeviation, Math.min(1 + maxDeviation, end + wheelStep * (end - start) * shiftX * (1 - position)));
if (newStart == start && newEnd == end) {
prevent = false;
}
if (1 / (newEnd - newStart) < axis.getPrivate("maxZoomFactor", Infinity) / axis.get("minZoomCount", 1)) {
this._handleWheelAnimation(axis.zoom(newStart, newEnd));
}
else {
prevent = false;
}
}
});
}
if ((wheelY === "zoomY" || wheelY === "zoomXY") && shiftY != 0) {
this.yAxes.each((axis) => {
if (axis.get("zoomY")) {
let start = axis.get("start");
let end = axis.get("end");
let position = axis.fixPosition(plotPoint.y / plotContainer.height());
if (wheelZoomPositionY != null) {
position = wheelZoomPositionY;
}
let maxDeviation = axis.get("maxDeviation", 0);
let newStart = Math.min(1 + maxDeviation, Math.max(-maxDeviation, start - wheelStep * (end - start) * shiftY * position));
let newEnd = Math.max(-maxDeviation, Math.min(1 + maxDeviation, end + wheelStep * (end - start) * shiftY * (1 - position)));
if (newStart == start && newEnd == end) {
prevent = false;
}
if (1 / (newEnd - newStart) < axis.getPrivate("maxZoomFactor", Infinity) / axis.get("minZoomCount", 1)) {
this._handleWheelAnimation(axis.zoom(newStart, newEnd));
}
else {
prevent = false;
}
}
});
}
if ((wheelX === "panX" || wheelX === "panXY") && shiftX != 0) {
this.xAxes.each((axis) => {
if (axis.get("panX")) {
let start = axis.get("start");
let end = axis.get("end");
let delta = this._getWheelSign(axis) * wheelStep * (end - start) * shiftX;
let newStart = start + delta;
let newEnd = end + delta;
let se = this._fixWheel(newStart, newEnd);
newStart = se[0];
newEnd = se[1];
if (newStart == start && newEnd == end) {
prevent = false;
}
this._handleWheelAnimation(axis.zoom(newStart, newEnd));
}
});
}
if ((wheelY === "panX" || wheelY === "panXY") && shiftY != 0) {
this.xAxes.each((axis) => {
if (axis.get("panX")) {
let start = axis.get("start");
let end = axis.get("end");
let delta = this._getWheelSign(axis) * wheelStep * (end - start) * shiftY;
let newStart = start + delta;
let newEnd = end + delta;
let se = this._fixWheel(newStart, newEnd);
newStart = se[0];
newEnd = se[1];
if (newStart == start && newEnd == end) {
prevent = false;
}
this._handleWheelAnimation(axis.zoom(newStart, newEnd));
}
});
}
if ((wheelX === "panY" || wheelX === "panXY") && shiftX != 0) {
this.yAxes.each((axis) => {
if (axis.get("panY")) {
let start = axis.get("start");
let end = axis.get("end");
let delta = this._getWheelSign(axis) * wheelStep * (end - start) * shiftX;
let newStart = start + delta;
let newEnd = end + delta;
let se = this._fixWheel(newStart, newEnd);
newStart = se[0];
newEnd = se[1];
if (newStart == start && newEnd == end) {
prevent = false;
}
this._handleWheelAnimation(axis.zoom(newStart, newEnd));
}
});
}
if ((wheelY === "panY" || wheelY === "panXY") && shiftY != 0) {
this.yAxes.each((axis) => {
if (axis.get("panY")) {
let start = axis.get("start");
let end = axis.get("end");
let delta = this._getWheelSign(axis) * wheelStep * (end - start) * shiftY;
let newStart = start - delta;
let newEnd = end - delta;
let se = this._fixWheel(newStart, newEnd);
newStart = se[0];
newEnd = se[1];
if (newStart == start && newEnd == end) {
prevent = false;
}
this._handleWheelAnimation(axis.zoom(newStart, newEnd));
}
});
}
if (prevent) {
wheelEvent.preventDefault();
}
}
_handleSetWheel() {
const wheelX = this.get("wheelX");
const wheelY = this.get("wheelY");
const plotContainer = this.plotContainer;
if (wheelX !== "none" || wheelY !== "none") {
this._wheelDp = plotContainer.events.on("wheel", (event) => {
const wheelEvent = event.originalEvent;
if ((wheelX !== "none" && Math.abs(wheelEvent.deltaX) != 0) || (wheelY !== "none" && Math.abs(wheelEvent.deltaY) != 0)) {
this.handleWheel(event);
}
});
this._disposers.push(this._wheelDp);
}
else {
if (this._wheelDp) {
this._wheelDp.dispose();
}
}
}
_getWheelSign(axis) {
let sign = 1;
if (axis.get("renderer").get("inversed")) {
sign = -1;
}
return sign;
}
_fixWheel(start, end) {
const diff = end - start;
if (start < 0) {
start = 0;
end = start + diff;
}
if (end > 1) {
end = 1;
start = end - diff;
}
return [start, end];
}
_handlePlotDown(event) {
const originalEvent = event.originalEvent;
if (originalEvent.button == 2) {
return;
}
const plotContainer = this.plotContainer;
let local = plotContainer.toLocal(event.point);
if (this.get("pinchZoomX") || this.get("pinchZoomY")) {
const pointerId = originalEvent.pointerId;
if (pointerId) {
if ($object.keys(plotContainer._downPoints).length > 0) {
const xAxis = this.xAxes.getIndex(0);
const yAxis = this.yAxes.getIndex(0);
if (xAxis) {
this._downStartX = xAxis.get("start", 0);
this._downEndX = xAxis.get("end", 1);
}
if (yAxis) {
this._downStartY = yAxis.get("start", 0);
this._downEndY = yAxis.get("end", 1);
}
}
}
}
if (this.get("panX") || this.get("panY")) {
if (local.x >= 0 && local.y >= 0 && local.x <= plotContainer.width() && local.y <= this.height()) {
//this._downPoint = local;
this._downPoint = { x: originalEvent.clientX, y: originalEvent.clientY };
const panX = this.get("panX");
const panY = this.get("panY");
if (panX) {
this.xAxes.each((axis) => {
axis._panStart = axis.get("start");
axis._panEnd = axis.get("end");
});
}
if (panY) {
this.yAxes.each((axis) => {
axis._panStart = axis.get("start");
axis._panEnd = axis.get("end");
});
}
const eventType = "panstarted";
if (this.events.isEnabled(eventType)) {
this.events.dispatch(eventType, { type: eventType, target: this, originalEvent: event.originalEvent });
}
}
}
}
_handleWheelAnimation(animation) {
if (animation) {
animation.events.on("stopped", () => {
this._dispatchWheelAnimation();
});
}
else {
this._dispatchWheelAnimation();
}
}
_dispatchWheelAnimation() {
const eventType = "wheelended";
if (this.events.isEnabled(eventType)) {
this.events.dispatch(eventType, { type: eventType, target: this });
}
}
_handlePlotUp(event) {
const downPoint = this._downPoint;
if (downPoint) {
if (this.get("panX") || this.get("panY")) {
if (event.originalEvent.clientX == downPoint.x && event.originalEvent.clientY == downPoint.y) {
const eventType = "pancancelled";
if (this.events.isEnabled(eventType)) {
this.events.dispatch(eventType, { type: eventType, target: this, originalEvent: event.originalEvent });
}
}
const eventType = "panended";
if (this.events.isEnabled(eventType)) {
this.events.dispatch(eventType, { type: eventType, target: this, originalEvent: event.originalEvent });
}
}
}
// TODO: handle multitouch
this._downPoint = undefined;
this.xAxes.each((xAxis) => {
xAxis._isPanning = false;
});
this.yAxes.each((yAxis) => {
yAxis._isPanning = false;
});
}
_handlePlotMove(event) {
const plotContainer = this.plotContainer;
if (this.get("pinchZoomX") || this.get("pinchZoomY")) {
const touchEvent = event.originalEvent;
const pointerId = touchEvent.pointerId;
if (pointerId) {
this._movePoints[pointerId] = event.point;
if ($object.keys(plotContainer._downPoints).length > 1) {
this._handlePinch();
return;
}
}
}
let downPoint = this._downPoint;
if (downPoint) {
downPoint = plotContainer.toLocal(this._root.documentPointToRoot(downPoint));
let local = plotContainer.toLocal(event.point);
const panX = this.get("panX");
const panY = this.get("panY");
if (panX) {
let scrollbarX = this.get("scrollbarX");
if (scrollbarX) {
scrollbarX.events.disableType("rangechanged");
}
this.xAxes.each((axis) => {
if (axis.get("panX")) {
axis._isPanning = true;
//const maxDeviation = axis.get("maxDeviation", 0);
let panStart = axis._panStart;
let panEnd = axis._panEnd;
let difference = (panEnd - panStart);
let deltaX = difference * (downPoint.x - local.x) / plotContainer.width();
if (axis.get("renderer").get("inversed")) {
deltaX *= -1;
}
let start = panStart + deltaX;
let end = panEnd + deltaX;
if (end - start < 1 + axis.get("maxDeviation", 1) * 2) {
axis.set("start", start);
axis.set("end", end);
}
}
});
if (scrollbarX) {
scrollbarX.events.enableType("rangechanged");
}
}
if (panY) {
let scrollbarY = this.get("scrollbarY");
if (scrollbarY) {
scrollbarY.events.disableType("rangechanged");
}
this.yAxes.each((axis) => {
if (axis.get("panY")) {
axis._isPanning = true;
//const maxDeviation = axis.get("maxDeviation", 0);
let panStart = axis._panStart;
let panEnd = axis._panEnd;
let difference = (panEnd - panStart);
let deltaY = difference * (downPoint.y - local.y) / plotContainer.height();
if (axis.get("renderer").get("inversed")) {
deltaY *= -1;
}
let start = panStart - deltaY;
let end = panEnd - deltaY;
if (end - start < 1 + axis.get("maxDeviation", 1) * 2) {
axis.set("start", start);
axis.set("end", end);
}
}
});
if (scrollbarY) {
scrollbarY.events.enableType("rangechanged");
}
}
}
}
_handlePinch() {
const plotContainer = this.plotContainer;
let i = 0;
let downPoints = [];
let movePoints = [];
$object.each(plotContainer._downPoints, (k, point) => {
downPoints[i] = point;
let movePoint = this._movePoints[k];
if (movePoint) {
movePoints[i] = movePoint;
}
i++;
});
if (downPoints.length > 1 && movePoints.length > 1) {
const w = plotContainer.width();
const h = plotContainer.height();
let downPoint0 = downPoints[0];
let downPoint1 = downPoints[1];
let movePoint0 = movePoints[0];
let movePoint1 = movePoints[1];
if (downPoint0 && downPoint1 && movePoint0 && movePoint1) {
movePoint0 = plotContainer.toLocal(movePoint0);
movePoint1 = plotContainer.toLocal(movePoint1);
downPoint0 = plotContainer.toLocal(downPoint0);
downPoint1 = plotContainer.toLocal(downPoint1);
if (this.get("pinchZoomX")) {
const downStartX = this._downStartX;
const downEndX = this._downEndX;
if (downStartX != null && downEndX != null) {
if (downPoint0.x > downPoint1.x) {
[downPoint0, downPoint1] = [downPoint1, downPoint0];
[movePoint0, movePoint1] = [movePoint1, movePoint0];
}
let downPos0 = downStartX + (downPoint0.x / w) * (downEndX - downStartX);
let downPos1 = downStartX + (downPoint1.x / w) * (downEndX - downStartX);
let movePos0 = downStartX + (movePoint0.x / w) * (downEndX - downStartX);
let movePos1 = downStartX + (movePoint1.x / w) * (downEndX - downStartX);
let initialDistance = Math.max(0.001, downPos1 - downPos0);
let currentDistance = Math.max(0.001, movePos1 - movePos0);
let d = initialDistance / currentDistance;
let s = downStartX * d + downPos0 - movePos0 * d;
let e = downEndX * d + downPos1 - movePos1 * d;
this.xAxes.each((xAxis) => {
let sa = xAxis.fixPosition(s);
let ea = xAxis.fixPosition(e);
xAxis.zoom(sa, ea, 0);
});
}
}
if (this.get("pinchZoomY")) {
const downStartY = this._downStartY;
const downEndY = this._downEndY;
if (downStartY != null && downEndY != null) {
if (downPoint0.y < downPoint1.y) {
[downPoint0, downPoint1] = [downPoint1, downPoint0];
[movePoint0, movePoint1] = [movePoint1, movePoint0];
}
let downPos0 = downStartY + (1 - downPoint0.y / h) * (downEndY - downStartY);
let downPos1 = downStartY + (1 - downPoint1.y / h) * (downEndY - downStartY);
let movePos0 = downStartY + (1 - movePoint0.y / h) * (downEndY - downStartY);
let movePos1 = downStartY + (1 - movePoint1.y / h) * (downEndY - downStartY);
let initialDistance = Math.max(0.001, downPos1 - downPos0);
let currentDistance = Math.max(0.001, movePos1 - movePos0);
let d = initialDistance / currentDistance;
let s = downStartY * d + downPos0 - movePos0 * d;
let e = downEndY * d + downPos1 - movePos1 * d;
this.yAxes.each((yAxis) => {
let sa = yAxis.fixPosition(s);
let ea = yAxis.fixPosition(e);
yAxis.zoom(sa, ea, 0);
});
}
}
}
}
}
_handleCursorPosition() {
const cursor = this.get("cursor");
if (cursor) {
const cursorPoint = cursor.getPrivate("point");
let snapToSeries = cursor.get("snapToSeries");
if (cursor._downPoint) {
snapToSeries = undefined;
}
if (snapToSeries && cursorPoint) {
const snapToSeriesBy = cursor.get("snapToSeriesBy");
const dataItems = [];
$array.each(snapToSeries, (series) => {
if (!series.isHidden() && !series.isHiding()) {
if (snapToSeriesBy != "x!" && snapToSeriesBy != "y!") {
const startIndex = series.startIndex();
const endIndex = series.endIndex();
for (let i = startIndex; i < endIndex; i++) {
const dataItem = series.dataItems[i];
if (dataItem && !dataItem.isHidden()) {
dataItems.push(dataItem);
}
}
}
else {
const tooltipDataItem = series.get("tooltipDataItem");
if (tooltipDataItem) {
dataItems.push(tooltipDataItem);
}
}
}
});
let minDistance = Infinity;
let closestItem;
$array.each(dataItems, (dataItem) => {
const point = dataItem.get("point");
if (point) {
let distance = 0;
if (snapToSeriesBy == "x" || snapToSeriesBy == "x!") {
distance = Math.abs(cursorPoint.x - point.x);
}
else if (snapToSeriesBy == "y" || snapToSeriesBy == "y!") {
distance = Math.abs(cursorPoint.y - point.y);
}
else {
distance = Math.hypot(cursorPoint.x - point.x, cursorPoint.y - point.y);
}
if (distance < minDistance) {
minDistance = distance;
closestItem = dataItem;
}
}
});
$array.each(snapToSeries, (series) => {
const tooltip = series.get("tooltip");
if (tooltip) {
tooltip._setDataItem(undefined);
}
});
if (closestItem) {
let series = closestItem.component;
series.showDataItemTooltip(closestItem);
series.setRaw("tooltipDataItem", closestItem);
const point = closestItem.get("point");
if (point) {
// removing x and y to solve #72225
cursor.handleMove(series.toGlobal({ x: point.x - series.x(), y: point.y - series.y() }), true);
}
}
}
}
}
_updateCursor() {
let cursor = this.get("cursor");
if (cursor) {
cursor.updateCursor();
}
}
_addCursor(cursor) {
this.plotContainer.children.push(cursor);
}
_prepareChildren() {
super._prepareChildren();
this.series.each((series) => {
this._colorize(series);
});
if (this.isDirty("wheelX") || this.isDirty("wheelY")) {
this._handleSetWheel();
}
if (this.isDirty("cursor")) {
const previous = this._prevSettings.cursor;
const cursor = this.get("cursor");
if (cursor !== previous) {
this._disposeProperty("cursor");
if (previous) {
previous.dispose();
}
if (cursor) {
cursor._setChart(this);
this._addCursor(cursor);
this._pushPropertyDisposer("cursor", cursor.events.on("selectended", () => {
this._handleCursorSelectEnd();
}));
}
//this.setRaw("cursor", cursor) // to reset previous value
this._prevSettings.cursor = cursor;
}
}
if (this.isDirty("scrollbarX")) {
const previous = this._prevSettings.scrollbarX;
const scrollbarX = this.get("scrollbarX");
if (scrollbarX !== previous) {
this._disposeProperty("scrollbarX");
if (previous) {
previous.dispose();
}
if (scrollbarX) {
this._placeScrollbar("scrollbarX", scrollbarX, this.topAxesContainer, this.bottomAxesContainer);
this._pushPropertyDisposer("scrollbarX", scrollbarX.events.on("rangechanged", (e) => {
this._handleScrollbar(this.xAxes, e.start, e.end, e.grip);
}));
this._pushPropertyDisposer("scrollbarX", scrollbarX.events.on("released", () => {
this.xAxes.each((axis) => {
if (axis.get("zoomable")) {
this._handleAxisSelection(axis);
}
});
}));
// Used to populate `ariaLabel` with meaningful values
scrollbarX.setPrivate("positionTextFunction", (position) => {
const axis = this.xAxes.getIndex(0);
return axis ? axis.getTooltipText(position, false) || "" : "";
});
}
this._prevSettings.scrollbarX = scrollbarX;
}
}
if (this.isDirty("scrollbarY")) {
const previous = this._prevSettings.scrollbarY;
const scrollbarY = this.get("scrollbarY");
if (scrollbarY !== previous) {
this._disposeProperty("scrollbarY");
if (previous) {
previous.dispose();
}
if (scrollbarY) {
this._placeScrollbar("scrollbarY", scrollbarY, this.rightAxesContainer, this.leftAxesContainer, true);
this._pushPropertyDisposer("scrollbarY", scrollbarY.events.on("rangechanged", (e) => {
this._handleScrollbar(this.yAxes, e.start, e.end, e.grip);
}));
this._pushPropertyDisposer("scrollbarY", scrollbarY.events.on("released", () => {
this.yAxes.each((axis) => {
if (axis.get("zoomable")) {
this._handleAxisSelection(axis);
}
});
}));
// Used to populate `ariaLabel` with meaningful values
scrollbarY.setPrivate("positionTextFunction", (position) => {
const axis = this.yAxes.getIndex(0);
return axis ? axis.getTooltipText(position, false) || "" : "";
});
}
this._prevSettings.scrollbarY = scrollbarY;
}
}
this._handleZoomOut();
}
/**
* Puts a scrollbar into one of two containers, depending on its `opposite`
* setting, and moves it over if the setting changes later. `unshift` is for
* the left container, which fills left to right, so the scrollbar has to go
* in at the front to end up outside the axes.
*
* @ignore
*/
_placeScrollbar(key, scrollbar, container, oppositeContainer, unshift) {
const place = () => {
const opposite = scrollbar.get("opposite");
const target = opposite ? oppositeContainer : container;
const children = target.children;
const index = (opposite && unshift) ? 0 : children.length - 1;
if (scrollbar.parent != target || children.indexOf(scrollbar) != index) {
// A vertical layout only ever sets a child's private `y` and a
// horizontal one only `x`, so whatever the new container does not
// own would keep the value the previous one gave it. Drop both, the
// same way a container resets its children when its own layout
// changes.
scrollbar.removePrivate("x");
scrollbar.removePrivate("y");
if (opposite && unshift) {
children.moveValue(scrollbar, 0);
}
else {
children.moveValue(scrollbar);
}
// Inserting a child only marks the container it came from, so make
// the receiving side re-measure and lay out again.
target.markDirtyBounds();
// A vertical scrollbar changes the width of the left/right
// containers, which the top/bottom padding is derived from. Those
// widths only settle once the layout has run, hence the wait.
this._root.events.once("frameended", () => {
if (!this.isDisposed()) {
this._updateChartLayout();
}
});
}
};
if (!scrollbar.parent) {
place();
}
this._pushPropertyDisposer(key, scrollbar.on("opposite", () => {
// only move scrollbars the chart placed itself
const parent = scrollbar.parent;
if (parent == container || parent == oppositeContainer) {
place();
}
}));
}
_processSeries(series) {
super._processSeries(series);
const xAxis = series.get("xAxis");
const yAxis = series.get("yAxis");
$array.move(xAxis.series, series);
$array.move(yAxis.series, series);
series._posXDp = series.addDisposer(xAxis.events.on("positionchanged", () => {
series._fixPosition();
}));
series._posYDp = series.addDisposer(yAxis.events.on("positionchanged", () => {
series._fixPosition();
}));
if (!series.get("baseAxis")) {
// the chart working it out, not something anyone asked for
if (yAxis.isType("CategoryAxis") || yAxis.isType("DateAxis")) {
series._setC("baseAxis", yAxis);
}
else {
series._setC("baseAxis", xAxis);
}
}
if (series.get("stacked")) {
series._markDirtyKey("stacked");
$array.each(series.dataItems, (dataItem) => {
dataItem.set("stackToItemY", undefined);
dataItem.set("stackToItemX", undefined);
});
}
series._markDirtyAxes();
yAxis.markDirtyExtremes();
xAxis.markDirtyExtremes();
xAxis._seriesAdded = true;
yAxis._seriesAdded = true;
this._colorize(series);
}
_colorize(series) {
const colorSet = this.get("colors");
// A series coloring its own data items still takes a color of its own: it
// is what the columns go back to when that is switched off again
if (colorSet) {
if (series.get("fill") == null) {
const color = colorSet.next();
series._setSoft("stroke", color);
series._setSoft("fill", color);
}
}
const patternSet = this.get("patterns");
if (patternSet) {
if (series.get("fillPattern") == null) {
const pattern = patternSet.next();
series._setSoft("fillPattern", pattern);
}
}
// Cycle stroke widths / dash arrays for line series (those that have a
// `strokes` list), keyed by the series' position — so line series can be
// told apart by weight and dashiness without relying on color.
const strokeWidths = this.get("strokeWidths");
const strokeDasharrays = this.get("strokeDasharrays");
if (strokeWidths || strokeDasharrays) {
const strokes = series.strokes;
if (strokes && strokes.template) {
const index = this.series.indexOf(series);
if (strokeWidths && strokeWidths.length > 0) {
strokes.template._setC("strokeWidth", strokeWidths[index % strokeWidths.length]);
}
if (strokeDasharrays && strokeDasharrays.length > 0) {
strokes.template._setC("strokeDasharray", strokeDasharrays[index % strokeDasharrays.length]);
}
}
}
}
_handleCursorSelectEnd() {
const cursor = this.get("cursor");
const behavior = cursor.get("behavior");
const downPositionX = cursor.getPrivate("downPositionX", 0);
const downPositionY = cursor.getPrivate("downPositionY", 0);
const positionX = Math.min(1, Math.max(0, cursor.getPrivate("positionX", 0.5)));
const positionY = Math.min(1, Math.max(0, cursor.getPrivate("positionY", 0.5)));
this.xAxes.each((axis) => {
if (behavior === "zoomX" || behavior === "zoomXY") {
let position0 = axis.toAxisPosition(downPositionX);
let position1 = axis.toAxisPosition(positionX);
axis.zoom(position0, position1);
}
axis.setPrivate("updateScrollbar", true);
});
this.yAxes.each((axis) => {
if (behavior === "zoomY" || behavior === "zoomXY") {
let position0 = axis.toAxisPosition(downPositionY);
let position1 = axis.toAxisPosition(positionY);
axis.zoom(position0, position1);
}
axis.setPrivate("updateScrollbar", true);
});
}
_handleScrollbar(axes, start, end, priority) {
axes.each((axis) => {
let axisStart = axis.fixPosition(start);
let axisEnd = axis.fixPosition(end);
let zoomAnimation = axis.zoom(axisStart, axisEnd, undefined, priority);
const updateScrollbar = "updateScrollbar";
axis.setPrivateRaw(updateScrollbar, false);
if (zoomAnimation) {
zoomAnimation.events.on("stopped", () => {
axis.setPrivateRaw(updateScrollbar, true);
});
}
else {
axis.setPrivateRaw(updateScrollbar, true);
}
});
}
_processAxis(axes, container) {
return axes.events.onAll((change) => {
if (change.type === "clear") {
$array.each(change.oldValues, (axis) => {
this._removeAxis(axis);
});
}
else if (change.type === "push") {
container.children.push(change.newValue);
change.newValue.processChart(this);
}
else if (change.type === "setIndex") {
container.children.setIndex(change.index, change.newValue);
change.newValue.processChart(this);
}
else if (change.type === "insertIndex") {
container.children.insertIndex(change.index, change.newValue);
change.newValue.processChart(this);
}
else if (change.type === "removeIndex") {
this._removeAxis(change.oldValue);
}
else if (change.type === "moveIndex") {
container.children.moveValue(change.value, change.newIndex);
change.value.processChart(this);
}
else {
throw new Error("Unknown IListEvent type");
}
});
}
_removeAxis(axis) {
if (!axis.isDisposed()) {
const axisParent = axis.parent;
if (axisParent) {
axisParent.children.removeValue(axis);
}
const gridContainer = axis.gridContainer;
const gridParent = gridContainer.parent;
if (gridParent) {
gridParent.children.removeValue(gridContainer);
}
const topGridContainer = axis.topGridContainer;
const topGridParent = topGridContainer.parent;
if (topGridParent) {
topGridParent.children.removeValue(topGridContainer);
}
}
}
_updateChartLayout() {
const left = this.leftAxesContainer.width();
const right = this.rightAxesContainer.width();
const bottomAxesContainer = this.bottomAxesContainer;
bottomAxesContainer.set("paddingLeft", left);
bottomAxesContainer.set("paddingRight", right);
const topAxesContainer = this.topAxesContainer;
topAxesContainer.set("paddingLeft", left);
topAxesContainer.set("paddingRight", right);
}
/**
* @ignore
*/
processAxis(axis) {
var cursor = this.get("cursor");
if (cursor) {
this.addDisposer(axis.on("start", () => {
this._updateCursor();
}));
this.addDisposer(axis.on("end", () => {
this._updateCursor();
}));
}
}
_handleAxisSelection(axis, force) {
let start = axis.fixPosition(axis.get("start", 0));
let end = axis.fixPosition(axis.get("end", 1));
if (start > end) {
[start, end] = [end, start];
}
if (this.xAxes.indexOf(axis) != -1) {
if (force || axis.getPrivate("updateScrollbar")) {
let scrollbarX = this.get("scrollbarX");
if (scrollbarX && (!scrollbarX.getPrivate("isBusy") || force)) {
scrollbarX._setCRaw("start", start);
scrollbarX._setCRaw("end", end);
scrollbarX.updateGrips();
}
}
}
else if (this.yAxes.indexOf(axis) != -1) {
if (force || axis.getPrivate("updateScrollbar")) {
let scrollbarY = this.get("scrollbarY");
if (scrollbarY && (!scrollbarY.getPrivate("isBusy") || force)) {
scrollbarY._setCRaw("start", start);
scrollbarY._setCRaw("end", end);
scrollbarY.updateGrips();
}
}
}
this._handleZoomOut();
}
_handleZoomOut() {
let zoomOutButton = this.zoomOutButton;
if (zoomOutButton && zoomOutButton.parent) {
let visible = false;
this.xAxes.each((axis) => {
if (axis.get("zoomOut", true) && !axis._isSynced()) {
if ($math.round(axis.get("start", 0), 5) != 0 || $math.round(axis.get("end", 1), 5) != 1) {
visible = true;
}
}
});
this.yAxes.each((axis) => {
if (axis.get("zoomOut", true) && !axis._isSynced()) {
if ($math.round(axis.get("start", 0), 5) != 0 || $math.round(axis.get("end", 1), 5) != 1) {
visible = true;
}
}
});
if (visible) {
if (zoomOutButton.isHidden()) {
zoomOutButton.show();
}
}
else {
zoomOutButton.hide();
}
}
}
/**
* Checks if point is within plot area.
*
* @param point Reference point
* @return Is within plot area?
*/
inPlot(point) {
const plotContainer = this.plotContainer;
const otherCharts = this.getPrivate("otherCharts", this._otherCharts);
const global = plotContainer.toGlobal(point);
if (point.x >= -0.5 && point.y >= -0.5 && point.x <= plotContainer.width() + 0.5 && point.y <= plotContainer.height() + 0.5) {
return true;
}
if (otherCharts) {
for (let i = otherCharts.length - 1; i >= 0; i--) {
const chart = otherCharts[i];
if (chart != this) {
const chartPlotContainer = chart.plotContainer;
const documentPoint = this._root.rootPointToDocument(global);
const chartRoot = chart._root.documentPointToRoot(documentPoint);
const local = chartPlotContainer.toLocal(chartRoot);
if (local.x >= -0.1 && local.y >= -0.1 && local.x <= chartPlotContainer.width() + 0.1 && local.y <= chartPlotContainer.height() + 0.1) {
return true;
}
}
}
}
return false;
}
/**
* @ignore
*/
arrangeTooltips() {
const plotContainer = this.plotContainer;
const w = plotContainer.width();
const h = plotContainer.height();
let hh = this.height();
const bounds = this._root.tooltipContainer.get("layerMargin");
if (bounds) {
if (bounds.bottom > hh) {
hh = bounds.bottom;
}
}
let plotT = plotContainer._display.toGlobal({ x: 0, y: 0 });
let plotB = plotContainer._display.toGlobal({ x: w, y: h });
const tooltips = [];
let sum = 0;
let minDistance = Infinity;
let movePoint = this._movePoint;
let maxTooltipDistance = this.get("maxTooltipDistance");
let maxTooltipDistanceBy = this.get("maxTooltipDistanceBy", "xy");
let closest;
let closestPoint;
if ($type.isNumber(maxTooltipDistance)) {
this.series.each((series) => {
if (!series.isHidden()) {
const tooltip = series.get("tooltip");
if (tooltip) {
let point = tooltip.get("pointTo");
if (point) {
let distance = Math.hypot(movePoint.x - point.x, movePoint.y - point.y);
if (maxTooltipDistanceBy == "x") {
distance = Math.abs(movePoint.x - point.x);
}
else if (maxTooltipDistanceBy == "y") {
distance = Math.abs(movePoint.y - point.y);
}
if (distance < minDistance) {
minDistance = distance;
closest = series;
closestPoint = point;
}
}
}
}
});
}
const tooltipSeries = [];
this.series.each((series) => {
const tooltip = series.get("tooltip");
if (tooltip && !tooltip.get("forceHidden")) {
let hidden = false;
let point = tooltip.get("pointTo");
if (point) {
if (maxTooltipDistance >= 0) {
let point = tooltip.get("pointTo");
if (point && closestPoint) {
if (series != closest) {
let distance = Math.hypot(closestPoint.x - point.x, closestPoint.y - point.y);
if (maxTooltipDistanceBy == "x") {
distance = Math.abs(closestPoint.x - point.x);
}
else if (maxTooltipDistanceBy == "y") {
distance = Math.abs(closestPoint.y - point.y);
}
if (distance > maxTooltipDistance) {
hidden = true;
}
}
}
}
else if (maxTooltipDistance == -1) {
if (series != closest) {
hidden = true;
}
}
if (!this.inPlot(this._tooltipToLocal(point)) || !tooltip.dataItem) {
hidden = true;
}
else {
if (!hidden) {
sum += point.y;
}
}
if (hidden || series.isHidden() || series.isHiding()) {
tooltip.hide(0);
}
else {
tooltip.show();
tooltips.push(tooltip);
tooltipSeries.push(series);
}
}
}
});
this.setPrivate("tooltipSeries", tooltipSeries);
if (this.get("arrangeTooltips")) {
let totalTooltipH = 0;
let tooltipCount = 0;
const tooltipContainer = this._root.tooltipContainer;
const count = tooltips.length;
const average = sum / count;
if (average > h / 2 + plotT.y) {
tooltips.sort((a, b) => $order.compareNumber(b.get("pointTo").y, a.get("pointTo").y));
let prevY = plotB.y;
$array.each(tooltips, (tooltip) => {
let height = tooltip.height();
tooltipCount++;
totalTooltipH += height;
let centerY = tooltip.get("centerY");
if (centerY instanceof Percent) {
height *= centerY.value;
}
height += tooltip.get("marginBottom", 0);
tooltip.set("bounds", { left: plotT.x, top: plotT.y, right: plotB.x, bottom: prevY });
tooltip.setPrivate("customData", { left: plotT.x, top: plotT.y, right: plotB.x, bottom: prevY });
prevY = Math.min(prevY - height, tooltip._fy - height);
if (tooltip.parent == tooltipContainer) {
tooltipContainer.children.moveValue(tooltip, 0);
}
});
if (prevY < 0) {
tooltips.reverse();
let prevBottom = prevY;
$array.each(tooltips, (tooltip) => {
tooltipCount++;
let bounds = tooltip.get("bounds");
if (bounds) {
let top = bounds.top - prevY;
let bottom = bounds.bottom - prevY;
if (top < prevBottom) {
top = prevBottom;
bottom = top + tooltip.height();
}
tooltip.set("bounds", { left: bounds.left, top: top, right: bounds.right, bottom: bottom });
prevBottom = bounds.bottom - prevY + tooltip.get("marginBottom", 0);
}
});
}
}
else {
tooltips.reverse();
tooltips.sort((a, b) => $order.compareNumber(a.get("pointTo").y, b.get("pointTo").y));
let prevY = 0;
$array.each(tooltips, (tooltip) => {
tooltipCount++;
let height = tooltip.height();
totalTooltipH += height;
let centerY = tooltip.get("centerY");
if (centerY instanceof Percent) {
height *= centerY.value;
}
height += tooltip.get("marginBottom", 0);
tooltip.set("bounds", { left: plotT.x, top: prevY, right: plotB.x, bottom: Math.max(plotT.y + hh, prevY + height) });
if (tooltip.parent == tooltipContainer) {
tooltipContainer.children.moveValue(tooltip, 0);
}
prevY = Math.max(prevY + height, tooltip._fy + height);
});
if (prevY > hh) {
tooltips.reverse();
let prevBottom = hh;
$array.each(tooltips, (tooltip) => {
tooltipCount++;
let bounds = tooltip.get("bounds");
if (bounds) {
let top = bounds.top - (hh - prevY);
let bottom = bounds.bottom - (hh - prevY);
if (bottom > prevBottom) {
bottom = prevBottom;
top = bottom - tooltip.height();
}
tooltip.set("bounds", { left: bounds.left, top: top, right: bounds.right, bottom: bottom });
prevBottom = bottom - tooltip.height() - tooltip.get("marginBottom", 0);
}
});
}
}
if (totalTooltipH == 0 && tooltipCount > 0) {
this._disposers.push(this.root.events.once("frameended", () => {
this.arrangeTooltips();
}));
}
}
}
_tooltipToLocal(point) {
return this.plotContainer.toLocal(point);
}
/**
* Fully zooms out the chart.
*/
zoomOut() {
this.xAxes.each((axis) => {
if (axis.get("zoomOut", true) && !axis._isSynced()) {
axis.setPrivate("updateScrollbar", true);
axis.zoom(0, 1);
}
});
this.yAxes.each((axis) => {
if (axis.get("zoomOut", true) && !axis._isSynced()) {
axis.setPrivate("updateScrollbar", true);
axis.zoom(0, 1);
}
});
}
_dispose() {
super._dispose();
// Explicitly disposing cursor to avoid memory leak of cursor adding
// keyboard events after parent chart has been disposed
const cursor = this.get("cursor");
if (cursor) {
cursor.dispose();
}
}
}
XYChart.className = "XYChart";
XYChart.classNames = SerialChart.classNames.concat([XYChart.className]);
//# sourceMappingURL=XYChart.js.map