/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import BoundingRect from 'zrender/src/core/BoundingRect'; import Cartesian from './Cartesian'; import { ScaleDataValue } from '../../util/types'; import Axis2D from './Axis2D'; import { CoordinateSystem } from '../CoordinateSystem'; import GridModel from './GridModel'; import Grid from './Grid'; import Scale from '../../scale/Scale'; import { invert } from 'zrender/src/core/matrix'; import { applyTransform } from 'zrender/src/core/vector'; export const cartesian2DDimensions = ['x', 'y']; function canCalculateAffineTransform(scale: Scale) { return scale.type === 'interval' || scale.type === 'time'; } class Cartesian2D extends Cartesian implements CoordinateSystem { readonly type = 'cartesian2d'; readonly dimensions = cartesian2DDimensions; model: GridModel; master: Grid; private _transform: number[]; private _invTransform: number[]; /** * Calculate an affine transform matrix if two axes are time or value. * It's mainly for accelartion on the large time series data. */ calcAffineTransform() { this._transform = this._invTransform = null; const xAxisScale = this.getAxis('x').scale; const yAxisScale = this.getAxis('y').scale; if (!canCalculateAffineTransform(xAxisScale) || !canCalculateAffineTransform(yAxisScale)) { return; } const xScaleExtent = xAxisScale.getExtent(); const yScaleExtent = yAxisScale.getExtent(); const start = this.dataToPoint([xScaleExtent[0], yScaleExtent[0]]); const end = this.dataToPoint([xScaleExtent[1], yScaleExtent[1]]); const xScaleSpan = xScaleExtent[1] - xScaleExtent[0]; const yScaleSpan = yScaleExtent[1] - yScaleExtent[0]; if (!xScaleSpan || !yScaleSpan) { return; } // Accelerate data to point calculation on the special large time series data. const scaleX = (end[0] - start[0]) / xScaleSpan; const scaleY = (end[1] - start[1]) / yScaleSpan; const translateX = start[0] - xScaleExtent[0] * scaleX; const translateY = start[1] - yScaleExtent[0] * scaleY; const m = this._transform = [scaleX, 0, 0, scaleY, translateX, translateY]; this._invTransform = invert([], m); } /** * Base axis will be used on stacking. */ getBaseAxis(): Axis2D { return this.getAxesByScale('ordinal')[0] || this.getAxesByScale('time')[0] || this.getAxis('x'); } containPoint(point: number[]): boolean { const axisX = this.getAxis('x'); const axisY = this.getAxis('y'); return axisX.contain(axisX.toLocalCoord(point[0])) && axisY.contain(axisY.toLocalCoord(point[1])); } containData(data: ScaleDataValue[]): boolean { return this.getAxis('x').containData(data[0]) && this.getAxis('y').containData(data[1]); } containZone(data1: ScaleDataValue[], data2: ScaleDataValue[]): boolean { const zoneDiag1 = this.dataToPoint(data1); const zoneDiag2 = this.dataToPoint(data2); const area = this.getArea(); const zone = new BoundingRect( zoneDiag1[0], zoneDiag1[1], zoneDiag2[0] - zoneDiag1[0], zoneDiag2[1] - zoneDiag1[1]); return area.intersect(zone); } dataToPoint(data: ScaleDataValue[], clamp?: boolean, out?: number[]): number[] { out = out || []; const xVal = data[0]; const yVal = data[1]; // Fast path if (this._transform // It's supported that if data is like `[Inifity, 123]`, where only Y pixel calculated. && xVal != null && isFinite(xVal as number) && yVal != null && isFinite(yVal as number) ) { return applyTransform(out, data as number[], this._transform); } const xAxis = this.getAxis('x'); const yAxis = this.getAxis('y'); out[0] = xAxis.toGlobalCoord(xAxis.dataToCoord(xVal, clamp)); out[1] = yAxis.toGlobalCoord(yAxis.dataToCoord(yVal, clamp)); return out; } clampData(data: ScaleDataValue[], out?: number[]): number[] { const xScale = this.getAxis('x').scale; const yScale = this.getAxis('y').scale; const xAxisExtent = xScale.getExtent(); const yAxisExtent = yScale.getExtent(); const x = xScale.parse(data[0]); const y = yScale.parse(data[1]); out = out || []; out[0] = Math.min( Math.max(Math.min(xAxisExtent[0], xAxisExtent[1]), x), Math.max(xAxisExtent[0], xAxisExtent[1]) ); out[1] = Math.min( Math.max(Math.min(yAxisExtent[0], yAxisExtent[1]), y), Math.max(yAxisExtent[0], yAxisExtent[1]) ); return out; } pointToData(point: number[], clamp?: boolean): number[] { const out: number[] = []; if (this._invTransform) { return applyTransform(out, point, this._invTransform); } const xAxis = this.getAxis('x'); const yAxis = this.getAxis('y'); out[0] = xAxis.coordToData(xAxis.toLocalCoord(point[0]), clamp); out[1] = yAxis.coordToData(yAxis.toLocalCoord(point[1]), clamp); return out; } getOtherAxis(axis: Axis2D): Axis2D { return this.getAxis(axis.dim === 'x' ? 'y' : 'x'); } /** * Get rect area of cartesian. * Area will have a contain function to determine if a point is in the coordinate system. */ getArea(): Cartesian2DArea { const xExtent = this.getAxis('x').getGlobalExtent(); const yExtent = this.getAxis('y').getGlobalExtent(); const x = Math.min(xExtent[0], xExtent[1]); const y = Math.min(yExtent[0], yExtent[1]); const width = Math.max(xExtent[0], xExtent[1]) - x; const height = Math.max(yExtent[0], yExtent[1]) - y; return new BoundingRect(x, y, width, height); } }; interface Cartesian2DArea extends BoundingRect {} export default Cartesian2D;