blob: ac2c75616e9d15a39a8e65a9c52cc401a1bd9079 [file]
/*
* 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, {
boundingRectApplyTransform,
boundingRectCalculateTransform, boundingRectContain, boundingRectCopy, boundingRectCreate,
} from 'zrender/src/core/BoundingRect';
import Transformable, {
copyTransform, transformableCreate, transformableGetLocalTransform
} from 'zrender/src/core/Transformable';
import Element from 'zrender/src/Element';
import { CoordinateSystemMaster, CoordinateSystem } from './CoordinateSystem';
import GlobalModel from '../model/Global';
import { ParsedModelFinder, ParsedModelFinderKnown } from '../util/model';
import { isPositionSizeOptionPercent, mathAbs, parsePercent } from '../util/number';
import {
AnimationOptionMixin,
ChartComponentRoamHostView, NullUndefined, RoamHostComponentOrSeries,
RoamOptionMixin, RoamPayload,
} from '../util/types';
import { assert, each } from 'zrender/src/core/util';
import ExtensionAPI, { getViewOfComponentOrSeries } from '../core/ExtensionAPI';
import {
decomposeTransform,
payloadDisableAnimation, updateProps, WH, XY
} from '../util/graphic';
import type ComponentModel from '../model/Component';
import type Model from '../model/Model';
import {
MatrixArray, invert as matrixInvert, mul as matrixMul, create as matrixCreate,
copy as matrixCopy
} from 'zrender/src/core/matrix';
import {
applyTransform as vectorApplyTransform, copy as vectorCopy, set as vectorSet
} from 'zrender/src/core/vector';
/**
* @tutorial [VIEW_COORD_SYS_ANIMATION]
*
* Some VIEW_COORD_SYS may need to support roaming animation, which may be required when the original
* inputs (such as `center/zoom/left/top/right/bottomwidth/height/etc.`) are changed via `setOption`.
* Roaming animation requires strict visual alignment between VIEW_COORD_SYS itself (consider "geo map")
* and its content (e.g., scatter series).
* A logically correct implementation is probably applying animation to VIEW_COORD_SYS's inputs and
* calculate VIEW_COORD_SYS's transformation and lay out its dependent series per frame via
* `dispatchAction`-ish pattern. But this approach require more mechanism to be introduced and careful
* refactoring.
* Instead, we currently still follow the conventional implementation - create animation separately on
* each element (typically via `graphic.updateProps`) . See VIEW_COORD_SYS_ANIMATION_PROPS_CONSTRAINT.
*
* [VIEW_COORD_SYS_ANIMATION_PROPS_CONSTRAINT]:
* The animation should only interpolate `x/y/scaleX/scaleY`, thereby ensuring visual alignment, even
* though `MapDraw` applies animations only on a "VIEW_COORD_SYS_TRANS_ROAM" but scatter series applies
* animation on a "VIEW_COORD_SYS_TRANS_OVERALL".
* But this constraint must always be respected by all relevant components and series.
*
* [VIEW_COORD_SYS_ANIMATION_INTERRUPTION]:
* The animation should be able to be interrupted, such as when users roam it by mouse or touch.
* When interrupted, `center` and `zoom` must be synchronized back to the VIEW_COORD_SYS and host models
* (e.g., geo components or map series). Otherwise, a next `setOption` can cause unexpected result due to
* the outdated `center` and `zoom`.
* But see FIXME_VIEW_COORD_SYS_SYNC_BACK for some existing bugs.
*
* [VIEW_COORD_SYS_UPDATE_FLOW]:
* To avoid bidirectional flow, we consider the original inputs (center/zoom/dataRect/viewRect/etc.) as
* the single source of truth, and always calculate transformations based on that.
* - Roaming interactions ('pan'/'zoom') can trigger an update to the original inputs via
* `dispatchAction({type: 'xxxRoam', ...})`.
* - Roaming animation can synchronize back to the original inputs when interrupted.
* See `syncBackEl`: It is zrender elements that are assigned with VIEW_COORD_SYS_TRANSFORMATION,
* and may be modified outside, typically by animation.
*/
/**
* @tutorial [VIEW_COORD_SYS_TRANSFORMATION]
*
* @tutorial [VIEW_COORD_SYS_TRANS_RAW]
* It is the transformation from `ViewInner['dataRect']` to `ViewInner['viewRect']`.
* - ViewInner['dataRect']:
* A rect in a source space (or say, data space).
* The unit is defined by the source. For example,
* - for geo source, the unit defined as lat/lng,
* - for SVG source, the unit is the same as the width/height defined in SVG.
* - for series.graph/series.tree/series.sankey, the unit defined as px directly.
* - ViewInner['viewRect']:
* A rect in the canvas pixel space.
* For example, it can be ec option left/right/top/bottom/width/height of a component or series.
* NOTICE:
* - ViewInner['dataRect'] and ViewInner['viewRect'] affect the behavior when centerOption contains
* absolute values. See VIEW_COORD_SYS_CENTER_ZOOM_DEFINITION .
*
* @tutorial [VIEW_COORD_SYS_TRANS_ROAM]
* It is built with respect to `RoamOptionMixin['center']` and `RoamOptionMixin['zoom']`.
* [VIEW_COORD_SYS_CENTER_ZOOM_DEFINITION]:
* {pxSpace} <-roamTrans- {viewRectSpace} <-rawTrans- {dataRectSpace} <-- ...
* | |
* zoomOption centerOption
* `RoamOptionMixin['center']` is a user specified point on the space of `ViewInner['dataRect']`,
* which will be located to the center of `ViewInner['viewRect']`.
* It can also be a percent value (e.g. '50%'), based on ViewInner['dataRect'].width/height` (since v6).
* Under this definition, users can use '0%' to map the top-left of `ViewInner['dataRect']`
* to the center of `ViewInner['viewRect']`.
* NOTICE:
* - `RoamOptionMixin['center']` is in a linear space regardless of `GeoCommonOptionMixin['projection']`
* - Absolute values in centerOption may hardly have a rigorous meaningful definition if the layout
* is auto-calculated, since users are unlikely to known the exact value of the bounding rect.
* And it is affected by the settings of ViewInner['dataRect'] and ViewInner['viewRect'].
* Therefore, percent centerOption is preferred.
* Use cases:
* geo and map series (`MapDraw`) use this transformation for roamming, since the source
* has been converted with respect to VIEW_COORD_SYS_TRANS_RAW.
*
* @tutorial [VIEW_COORD_SYS_TRANS_OVERALL]
* This is the result of
* `matrix_left_multiply(VIEW_COORD_SYS_TRANS_ROAM, VIEW_COORD_SYS_TRANS_RAW)`.
* Use case:
* sankey/tree/graph series use this transformation for roamming.
*
* @tutorial [VIEW_COORD_SYS_TRANSFORMATION_FORMULA]
* [Basic]:
* {pxSpace} <-roamTrans- {viewRectSpace} <-rawTrans- {dataRectSpace} <-- ...
* pxPoint = roamTrans * rawTrans * dataPoint
* overallTrans = roamTrans * rawTrans
* [e.g., geo case]:
* pxPoint = roamTrans * (rawTrans * (projection * dataPoint))
* Hence: pxPoint = group_syncBackType_ROAM * (rawTrans * (projection * dataPoint))
* Hence: pxPoint = group_syncBackType_ROAM * rawTrans_projection_precalculated_point
* [e.g., graph series case]:
* pxPoint = (roamTrans * rawTrans) * upper_els
* Hence: pxPoint = group_syncBackType_OVERALL * upper_els
* (NOTE: upper_els can have their own transformations)
*
* @see viewCoordSysCopyTrans for fetching transformations.
*/
export const VIEW_COORD_SYS_TRANS_RAW = 0;
export const VIEW_COORD_SYS_TRANS_ROAM = 1;
export const VIEW_COORD_SYS_TRANS_OVERALL = 2;
/**
* Private props of VIEW_COORD_SYS.
*/
interface ViewInner extends View {
/**
* @see useLegacyViewCoordSysCenterBase
*/
lgCt?: {w: number; h: number;} | NullUndefined;
lgGeo?: Transformable;
invertY: boolean;
/**
* @see VIEW_COORD_SYS_CENTER_ZOOM_DEFINITION
*/
centerOption: RoamOptionMixin['center'] | NullUndefined;
zoom: RoamOptionMixin['zoom'];
zoomLimit: RoamOptionMixin['scaleLimit'] | NullUndefined;
/**
* The index is `VIEW_COORD_SYS_TRANS_RAW`/`VIEW_COORD_SYS_TRANS_ROAM`/`VIEW_COORD_SYS_TRANS_OVERALL`.
*
* TRANSFORMABLE_PROPS on the returned transformable is ready to use.
*/
trans: Transformable[];
/**
* This is `transformableGetLocalTransform(viewInner.trans[VIEW_COORD_SYS_TRANS_RAW])`.
* It should follow each update of `viewInner.trans[VIEW_COORD_SYS_TRANS_RAW]`.
*/
mtRaw: MatrixArray;
/**
* This is the inverse matrix of `mtRaw`.
*/
mtRawInv: MatrixArray;
/**
* This is `transformableGetLocalTransform(viewInner.trans[VIEW_COORD_SYS_TRANS_OVERALL])`
* It should follow each update of `viewInner.trans[VIEW_COORD_SYS_TRANS_OVERALL]`.
*/
mtOverall: MatrixArray;
/**
* This is the inverse matrix of `mtOverall`.
*/
mtOverallInv: MatrixArray;
/**
* @see VIEW_COORD_SYS_TRANS_RAW
*/
dataRect: BoundingRect | NullUndefined;
/**
* @see VIEW_COORD_SYS_TRANS_RAW
*/
viewRect: BoundingRect | NullUndefined;
/**
* @see VIEW_COORD_SYS_ANIMATION
*/
syncBackEl: Element | NullUndefined;
syncBackType: typeof VIEW_COORD_SYS_TRANS_ROAM | typeof VIEW_COORD_SYS_TRANS_OVERALL;
}
function inner(viewCoordSys: View): ViewInner {
return viewCoordSys as ViewInner;
}
export const VIEW_COORD_SYS_TYPE = 'view';
/**
* [VIEW_COORD_SYS]
*
* @final [NOTICE] Inheritance of this class is not recommended. Use composition instead.
*/
class View extends Transformable implements CoordinateSystemMaster, CoordinateSystem {
readonly type: string = VIEW_COORD_SYS_TYPE;
static dimensions = ['x', 'y'];
readonly dimensions = ['x', 'y'];
constructor(
invertY?: boolean,
legacyCenterBase?: ViewInner['lgCt'],
legacyGeo?: Transformable
) {
super();
const viewInner = inner(this);
viewInner.invertY = invertY;
viewInner.lgCt = legacyCenterBase;
viewInner.lgGeo = legacyGeo;
const trans: ViewInner['trans'] = viewInner.trans = [];
trans[VIEW_COORD_SYS_TRANS_RAW] = transformableCreate();
trans[VIEW_COORD_SYS_TRANS_ROAM] = transformableCreate();
trans[VIEW_COORD_SYS_TRANS_OVERALL] = transformableCreate();
viewInner.mtRaw = matrixCreate();
viewInner.mtRawInv = matrixCreate();
viewInner.mtOverall = matrixCreate();
viewInner.mtOverallInv = matrixCreate();
viewInner.zoom = 1;
}
/**
* @implements CoordinateSystem['getBoundingRect']
* @see VIEW_COORD_SYS_TRANS_RAW
*
* This is a rect in data space.
* For historicall reason, the name is `getBoundingRect` - preserve it for backward compatibility.
*/
getBoundingRect(): BoundingRect {
return viewCoordSysCopyBoundingRect(null, this);
}
/**
* @implements CoordinateSystem['getViewRect']
* @see VIEW_COORD_SYS_TRANS_RAW
*/
getViewRect(): BoundingRect {
return viewCoordSysCopyViewRect(null, this);
}
/**
* @implements CoordinateSystem['getRoamTransform']
*/
getRoamTransform(): MatrixArray {
return transformableGetLocalTransform(inner(this).trans[VIEW_COORD_SYS_TRANS_ROAM]);
}
dataToPoint(data: number[], noRoam?: boolean, out?: number[]): number[] {
const transform = noRoam ? inner(this).mtRaw : inner(this).mtOverall;
out = out || [];
return transform
? vectorApplyTransform(out, data, transform)
: vectorCopy(out, data);
}
pointToData(point: number[], reserved?: unknown, out?: number[]): number[] {
out = out || [];
const invTransform = inner(this).mtOverallInv;
return invTransform
? vectorApplyTransform(out, point, invTransform)
: vectorCopy(out, point);
}
convertToPixel(
ecModel: GlobalModel, finder: ParsedModelFinder, value: number[]
): number[] {
const coordSys = getCoordSys(finder);
return coordSys === this ? coordSys.dataToPoint(value) : null;
}
convertFromPixel(
ecModel: GlobalModel, finder: ParsedModelFinder, pixel: number[]
): number[] {
const coordSys = getCoordSys(finder);
return coordSys === this ? coordSys.pointToData(pixel) : null;
}
containPoint(point: number[]): boolean {
const viewInner = inner(this);
boundingRectCopy(tmpPixelRectForContain, viewInner.dataRect);
boundingRectApplyTransform(tmpPixelRectForContain, tmpPixelRectForContain, viewInner.mtOverall);
return boundingRectContain(tmpPixelRectForContain, point[0], point[1]);
}
// getScalarScale() {
// // Use determinant square root of transform to multiply scalar
// let m = this.transform;
// let det = Math.sqrt(Math.abs(m[0] * m[3] - m[2] * m[1]));
// return det;
// }
}
const tmpPixelRectForContain = boundingRectCreate();
export function viewCoordSysCopyOverallMatrix(
out: MatrixArray | NullUndefined, viewCoordSys: View
): MatrixArray {
return matrixCopy(out || [], inner(viewCoordSys).mtOverall);
}
export function viewCoordSysGetZoomOption(viewCoordSys: View): RoamOptionMixin['zoom'] {
return inner(viewCoordSys).zoom;
}
export function viewCoordSysCopyBoundingRect(
out: BoundingRect | NullUndefined, viewCoordSys: View
): BoundingRect {
return boundingRectCopy(out || boundingRectCreate(), inner(viewCoordSys).dataRect);
}
export function viewCoordSysCopyViewRect(
out: BoundingRect | NullUndefined, viewCoordSys: View
): BoundingRect {
return boundingRectCopy(out || boundingRectCreate(), inner(viewCoordSys).viewRect);
}
/**
* Copy from `ViewInner['trans']`.
*/
export function viewCoordSysCopyTrans(
out: NullUndefined,
viewCoordSys: View,
transKind: typeof VIEW_COORD_SYS_TRANS_RAW | typeof VIEW_COORD_SYS_TRANS_ROAM | typeof VIEW_COORD_SYS_TRANS_OVERALL
): Transformable;
export function viewCoordSysCopyTrans<TOut extends Transformable>(
out: TOut,
viewCoordSys: View,
transKind: typeof VIEW_COORD_SYS_TRANS_RAW | typeof VIEW_COORD_SYS_TRANS_ROAM | typeof VIEW_COORD_SYS_TRANS_OVERALL
): TOut;
export function viewCoordSysCopyTrans(
out: Transformable | NullUndefined,
viewCoordSys: View,
transKind: typeof VIEW_COORD_SYS_TRANS_RAW | typeof VIEW_COORD_SYS_TRANS_ROAM | typeof VIEW_COORD_SYS_TRANS_OVERALL
// @return The input `out` or create an object.
): Transformable {
return copyTransform(
out || transformableCreate(),
inner(viewCoordSys).trans[transKind]
);
}
function viewCoordSysIsInputReady(viewInner: ViewInner): boolean {
return !!(viewInner.dataRect && viewInner.viewRect);
// NOTE: Other parameters has default values. Only parameters
// above are required to be input.
}
function calcOverallTransFromSyncBackEl(
out: Transformable,
viewInner: ViewInner,
syncBackEl: Transformable,
syncBackType: typeof VIEW_COORD_SYS_TRANS_ROAM | typeof VIEW_COORD_SYS_TRANS_OVERALL
): void {
if (syncBackType === VIEW_COORD_SYS_TRANS_ROAM) {
calcOverallTrans(
out,
viewInner.trans[VIEW_COORD_SYS_TRANS_RAW],
syncBackEl
);
}
else {
copyTransform(out, syncBackEl);
}
}
function calcRoamTransFromOverallTrans(
out: Transformable,
viewInner: ViewInner,
overallTrans: Transformable
): void {
// Convert overallTrans to roamTrans. If update animation is applied to overallTrans,
// we assume that rawTrans is changed instantly, and roamTrans takes that animation.
// Formula:
// overallTrans = roamTrans * rawTrans
// overallTrans * invert(rawTrans) = roamTrans
transformableGetLocalTransform(overallTrans, tmpMtRTO);
matrixMul(tmpMtRTO, tmpMtRTO, viewInner.mtRawInv);
decomposeTransform(out, tmpMtRTO);
}
const tmpMtRTO = matrixCreate();
/**
* [NOTICE]
* The definition of this center has always been irrelevant to some other series center like
* 'series-pie.center' - this center is a point in the space of `ViewInner['dataRect'].x/y`,
* rather than canvas viewport, and the unit is not necessarily pixel (e.g., in geo case).
*/
export function viewCoordSysSetRoamOptionFromModel(
viewCoordSys: View,
hostModel: RoamHostComponentOrSeries
): void {
const viewInner = inner(viewCoordSys);
viewInner.centerOption = hostModel.getShallow('center');
const zoomLimit = viewInner.zoomLimit = hostModel.getShallow('scaleLimit');
const zoomOption = hostModel.getShallow('zoom');
viewInner.zoom = clampByZoomLimit(zoomOption || 1, zoomLimit) || 1;
if (viewCoordSysIsInputReady(viewInner)) {
viewCoordSysUpdateTransform(viewInner);
}
}
/**
* @see ViewInner['dataRect']
*/
export function viewCoordSysSetBoundingRect(
viewCoordSys: View,
x: number, y: number, width: number, height: number
): void {
const viewInner = inner(viewCoordSys);
viewInner.dataRect = new BoundingRect(x, y, width, height);
if (viewCoordSysIsInputReady(viewInner)) {
viewCoordSysUpdateTransform(viewInner);
}
}
/**
* @see ViewInner['viewRect']
*/
export function viewCoordSysSetViewRect(
viewCoordSys: View,
x: number, y: number, width: number, height: number
): void {
const viewInner = inner(viewCoordSys);
viewInner.viewRect = new BoundingRect(x, y, width, height);
if (viewCoordSysIsInputReady(viewInner)) {
viewCoordSysUpdateTransform(viewInner);
}
}
function viewCoordSysUpdateTransform(
viewInner: ViewInner
): void {
// The order matters.
viewCoordSysUpdateRawTrans(viewInner);
viewCoordSysUpdateRoamTrans(viewInner);
viewCoordSysUpdateOverallTrans(viewInner);
}
function viewCoordSysUpdateRawTrans(
viewInner: ViewInner
): void {
let dataRect = viewInner.dataRect;
const viewRect = viewInner.viewRect;
const rawTrans = viewInner.trans[VIEW_COORD_SYS_TRANS_RAW];
const invertY = viewInner.invertY;
if (invertY) {
dataRect = boundingRectCopy(tmpRectURT, dataRect);
dataRect.y = -dataRect.y - dataRect.height;
}
boundingRectCalculateTransform(tmpMtURT, dataRect, viewRect);
decomposeTransform(rawTrans, tmpMtURT);
if (invertY) {
rawTrans.scaleY = -rawTrans.scaleY;
}
const mtRaw = transformableGetLocalTransform(rawTrans, viewInner.mtRaw);
matrixInvert(viewInner.mtRawInv, mtRaw);
}
const tmpMtURT = matrixCreate();
const tmpRectURT = boundingRectCreate();
/**
* NOTICE: It depends on `viewCoordSysUpdateRawTrans`.
*/
function viewCoordSysUpdateRoamTrans(viewInner: ViewInner): void {
const viewRectCenter = viewCoordSysGetViewRectCenter(viewInner);
const roamViewCenter = parseCenterOption(tmpCenterURT, viewInner, viewInner.centerOption)
? vectorApplyTransform(tmpCenterURT, tmpCenterURT, viewInner.mtRaw)
: viewRectCenter;
const zoom = viewInner.zoom;
const roamTrans = viewInner.trans[VIEW_COORD_SYS_TRANS_ROAM];
roamTrans.x = viewRectCenter[0] - zoom * roamViewCenter[0];
roamTrans.y = viewRectCenter[1] - zoom * roamViewCenter[1];
roamTrans.scaleX = roamTrans.scaleY = zoom;
// [VIEW_COORD_SYS_APPLY_ROAM_CENTER_AND_ZOOM]
// Ordinarily, the definition is:
// roamTrans.originX = roamViewCenter[0];
// roamTrans.originY = roamViewCenter[1];
// roamTrans.x = viewRectCenter[0] - roamViewCenter[0];
// roamTrans.y = viewRectCenter[1] - roamViewCenter[1];
// roamTrans.scaleX = roamTrans.scaleY = zoom;
// But `el.originX`/`originY` should not be set.
// (see VIEW_COORD_SYS_ANIMATION_PROPS_CONSTRAINT for the reason),
// so we use the above formula instead.
}
const tmpCenterURT: number[] = [];
/**
* NOTICE: It depends on `viewCoordSysUpdateRoamTrans` and `viewCoordSysUpdateRawTrans`
*/
function viewCoordSysUpdateOverallTrans(viewInner: ViewInner): void {
const trans = viewInner.trans;
const roamTrans = trans[VIEW_COORD_SYS_TRANS_ROAM];
const rawTrans = trans[VIEW_COORD_SYS_TRANS_RAW];
const overallTrans = trans[VIEW_COORD_SYS_TRANS_OVERALL];
calcOverallTrans(overallTrans, rawTrans, roamTrans);
const mtOverall = transformableGetLocalTransform(overallTrans, viewInner.mtOverall);
const mtOverallInv = matrixInvert(viewInner.mtOverallInv, mtOverall);
legacyCopyOverallTrans(viewInner, overallTrans, mtOverall, mtOverallInv);
legacyCopyOverallTrans(viewInner.lgGeo, overallTrans, mtOverall, mtOverallInv);
}
/**
* [VIEW_COORD_SYS_TRANS_OVERALL_BACKWARD_COMPATIBILITY]
* VIEW_COORD_SYS_TRANS_OVERALL transformable has long been View or Geo instance itself.
* We keep backward compatibility, since some users may have visited it directly (e.g.
* for drawing a computed bounding rect).
*/
function legacyCopyOverallTrans(
target: Transformable | NullUndefined,
overallTrans: Transformable,
mtOverall: MatrixArray,
mtOverallInv: MatrixArray | NullUndefined
): void {
if (target) {
copyTransform(target, overallTrans);
matrixCopy(target.transform || (target.transform = []), mtOverall);
const invTransform = target.invTransform || (target.invTransform = matrixCreate());
if (mtOverallInv) {
// `matrixInvert` returns null if the overall transform is singular (e.g., a
// zero-sized view rect when the chart container is hidden or sized 0x0);
// keep the last inverse in that case rather than crash.
matrixCopy(invTransform, mtOverallInv);
}
}
}
function calcOverallTrans(
out: Transformable,
rawTrans: Transformable,
roamTrans: Transformable
): void {
transformableGetLocalTransform(rawTrans, tmpMtCOT1);
transformableGetLocalTransform(roamTrans, tmpMtCOT2);
matrixMul(tmpMtCOT2, tmpMtCOT2, tmpMtCOT1);
decomposeTransform(out, tmpMtCOT2);
}
const tmpMtCOT1 = matrixCreate();
const tmpMtCOT2 = matrixCreate();
function getCoordSys(finder: ParsedModelFinderKnown): View {
const seriesModel = finder.seriesModel;
return seriesModel ? seriesModel.coordinateSystem as View : null; // e.g., graph.
}
function viewCoordSysGetViewRectCenter(viewInner: ViewInner): number[] {
const viewRect = viewInner.viewRect;
tmpViewRectCenter[0] = viewRect.x + viewRect.width / 2;
tmpViewRectCenter[1] = viewRect.y + viewRect.height / 2;
return tmpViewRectCenter;
}
const tmpViewRectCenter: number[] = [];
export function isViewCoordSys(coordSys: CoordinateSystem | NullUndefined): coordSys is View {
return coordSys && coordSys.type === 'view';
}
/**
* NOTICE:
* - `syncBackEl` should be in the pixel space without any other transformation
* in its accesters, otherwise the roaming may incorrect.
* - `syncBackEl` can be a `Group`, having its own descendants and transformation.
* But in this case, `dataToPoint` can only reach the space of `syncBackEl` itself.
*/
export function applyViewCoordSysTransToElement(
syncBackEl: Element | NullUndefined,
syncBackType: typeof VIEW_COORD_SYS_TRANS_ROAM | typeof VIEW_COORD_SYS_TRANS_OVERALL,
viewCoordSys: View,
// If NullUndefined, no animation.
// Typically, there should be no animation in the first render and in `__updateOnOwnRoam`.
animatableModel: Model<AnimationOptionMixin> | NullUndefined
): void {
const viewInner = inner(viewCoordSys);
viewInner.syncBackEl = syncBackEl;
viewInner.syncBackType = syncBackType;
if (!animatableModel) {
viewCoordSysCopyTrans(syncBackEl, viewCoordSys, syncBackType);
syncBackEl.dirty();
}
else {
updateProps(
syncBackEl,
viewCoordSysCopyTrans(null, viewCoordSys, syncBackType),
animatableModel
);
}
}
/**
* FIXME: [FIXME_VIEW_COORD_SYS_SYNC_BACK]
* Currently the "sync back" is only performed on roam action. But these cases are not
* covered:
* 1. Consider both center change and series data adding is requested via `setOption`
* and animation is not finished, the new added series symbols should lay out based
* on the "intermediate state" rather than the "final state" of the VIEW_COORD_SYS,
* otherwise, visual artifacts can arise.
* 2. See the case of FIXME_SYMBOL_CLIP_CONSIDERING_COORD_SYS_ALIGNMENT_DURING_ANIMATION .
*/
function viewCoordSysSyncBack(
viewCoordSys: View,
hostModel: RoamHostComponentOrSeries,
otherModelsToSync: RoamHostComponentOrSeries[] | NullUndefined,
payload: RoamPayload | NullUndefined
): void {
const viewInner = inner(viewCoordSys);
const syncBackEl = viewInner.syncBackEl;
if (syncBackEl) {
if (__DEV__) {
assert(viewInner.syncBackType != null);
}
// @see VIEW_COORD_SYS_ANIMATION_INTERRUPTION
syncBackEl.stopAnimation();
// syncBackEl may have been changed regardless of animation,
// therefore, need to sync back.
calcOverallTransFromSyncBackEl(tmpTransSB1, viewInner, syncBackEl, viewInner.syncBackType);
}
else {
copyTransform(tmpTransSB1, viewInner.trans[VIEW_COORD_SYS_TRANS_OVERALL]);
}
// Sync back to model.
calcRoamTransFromOverallTrans(tmpTransSB2, viewInner, tmpTransSB1);
payload
? applyRoamPayloadToOverallTrans(tmpTransSB1, tmpTransSB2, viewInner, payload)
: copyTransform(tmpTransSB1, tmpTransSB2);
calcRoamTransFromOverallTrans(tmpTransSB1, viewInner, tmpTransSB1);
syncBackToRoamOptionFromRoamTrans(viewInner, hostModel, otherModelsToSync, tmpTransSB1);
}
const tmpTransSB1 = transformableCreate();
const tmpTransSB2 = transformableCreate();
/**
* Invert to `center` and `zoom` of VIEW_COORD_SYS and host models based on
* - `syncBackEl` or VIEW_COORD_SYS_TRANS_OVERALL
* - Delta in `payload`.
*
* Should be only called in action handlers.
*
* @see RoamHostView['__updateOnOwnRoam']
*/
export function ownRoamModelCoordSysUpdateInAction(
payload: RoamPayload,
hostModel: RoamHostComponentOrSeries,
// This is only used for MAP_SERIES_GROUP, where the change need to sync to all series,
// otherwise when legend filter some series and the other series take the responsibility
// to draw map, or roam then legend restored, the result will be incorrect.
otherModelsToSync?: RoamHostComponentOrSeries[] | NullUndefined
): void {
// NOTE: VIEW_COORD_SYS instances are depended on by this action handler.
// Under the current design, actions should not be triggered before `setOption`
// being called, therefore coord sys instances always exist.
const viewCoordSys = getOwnRoamViewCoordSys(hostModel);
if (viewCoordSys) {
// Sync back to model.
viewCoordSysSyncBack(viewCoordSys, hostModel, otherModelsToSync, payload);
// Recalculate from model.
viewCoordSysSetRoamOptionFromModel(viewCoordSys, hostModel);
}
}
export function getOwnRoamViewCoordSys(hostModel: RoamHostComponentOrSeries): View | NullUndefined {
return hostModel.__ownRoamView ? hostModel.__ownRoamView() : null;
}
export function ownRoamViewUpdateDirectlyInAction(
payload: RoamPayload,
componentOrSeries: ComponentModel,
ecModel: GlobalModel,
api: ExtensionAPI
): void {
// Tricky: disable animation in `updateProps` of `graphic.ts`.
ecModel.setUpdatePayload(payloadDisableAnimation(payload));
const componentOrSeriesView = getViewOfComponentOrSeries(api, componentOrSeries);
if (componentOrSeriesView && (componentOrSeriesView as ChartComponentRoamHostView).__updateOnOwnRoam) {
(componentOrSeriesView as ChartComponentRoamHostView).__updateOnOwnRoam(payload, componentOrSeries, api);
}
}
function applyRoamPayloadToOverallTrans(
targetOverallTrans: Transformable,
roamTrans: Transformable,
viewInner: ViewInner,
payload: RoamPayload
): void {
// NOTE: payload.dx/dy should always applied to pixel space, i.e., "overallTrans".
if (payload.dx != null && payload.dy != null) {
targetOverallTrans.x += payload.dx;
targetOverallTrans.y += payload.dy;
}
const deltaZoom = payload.zoom;
if (deltaZoom != null) {
// Although `zoomOption` is defined on roamTrans
const oldZoom = getZoomFromRoamTrans(roamTrans);
const newZoom = clampByZoomLimit(oldZoom * deltaZoom, viewInner.zoomLimit);
const deltaZoom2 = newZoom / oldZoom;
// Keep the mouse center when scaling.
targetOverallTrans.x -= (payload.originX - targetOverallTrans.x) * (deltaZoom2 - 1);
targetOverallTrans.y -= (payload.originY - targetOverallTrans.y) * (deltaZoom2 - 1);
// Although `zoomOption` is defined on roamTrans,
// deltaZoom can be applied to overallTrans directly.
targetOverallTrans.scaleX *= deltaZoom2;
targetOverallTrans.scaleY *= deltaZoom2;
}
}
function getZoomFromRoamTrans(trans: Transformable): number {
if (__DEV__) {
// scaleX and scaleY should be the same even during animation.
// See VIEW_COORD_SYS_APPLY_ROAM_CENTER_AND_ZOOM .
assert(mathAbs(trans.scaleX - trans.scaleY) < 1e-5);
}
return trans.scaleX;
}
function parseCenterOption(
// `out` is a center in `dataRect` space.
out: number[],
viewInner: ViewInner,
centerOption: RoamOptionMixin['center']
// @return Whether a valid center is obtained.
): boolean {
if (__DEV__) {
assert(viewCoordSysIsInputReady(viewInner));
}
const dataRect = viewInner.dataRect;
if (!centerOption) {
return false;
}
// #16904 introduced percentage string here, such as '33%'. But it was based on canvas
// width/height, which is not reasonable - the unit may incorrect, and it is unpredictable if
// the `ViewInner['dataRect']` is not calculated based on the current canvas rect. Therefore the percentage
// value is changed to based on `ViewInner['dataRect'].width/height` since v6. Under this definition, users
// can use '0%' to map the top-left of `ViewInner['dataRect']` to the center of `ViewInner['viewRect']`.
const lgCt = viewInner.lgCt;
if (lgCt) {
vectorSet(
out,
parsePercent(centerOption[0], lgCt.w),
parsePercent(centerOption[1], lgCt.h)
);
}
else if (dataRect) {
vectorSet(
out,
parsePercent(centerOption[0], dataRect.width, dataRect.x),
parsePercent(centerOption[1], dataRect.height, dataRect.y)
);
}
return true;
}
/**
* An inverse operation to `parseCenterOption`.
* Mainly for percentage center option.
*/
function invertBackToCenterOption(
viewInner: ViewInner,
center: number[]
): RoamOptionMixin['center'] {
const lastCenterOption = viewInner.centerOption;
const dataRect = viewInner.dataRect;
if (__DEV__) {
assert(center && dataRect);
}
return (!lastCenterOption || viewInner.lgCt)
? center.slice()
: [
invertToPercentPerCenterDim(0, center, lastCenterOption, dataRect),
invertToPercentPerCenterDim(1, center, lastCenterOption, dataRect),
];
}
function invertToPercentPerCenterDim(
dimIdx: 0 | 1,
center: number[],
lastCenterOption: RoamOptionMixin['center'],
dataRect: BoundingRect
): RoamOptionMixin['center'][number] {
return (
lastCenterOption
&& dataRect
&& dataRect[WH[dimIdx]]
&& isPositionSizeOptionPercent(lastCenterOption[dimIdx])
)
? (
(center[dimIdx] - dataRect[XY[dimIdx]]) / dataRect[WH[dimIdx]] * 100
) + '%'
: center[dimIdx];
}
export function useLegacyViewCoordSysCenterBase(
ecModel: GlobalModel,
api: ExtensionAPI
): ViewInner['lgCt'] {
return (api && ecModel && ecModel.getShallow('legacyViewCoordSysCenterBase'))
? {w: api.getWidth(), h: api.getHeight()}
: null;
}
/**
* @see VIEW_COORD_SYS_ANIMATION_INTERRUPTION
*/
function syncBackToRoamOptionFromRoamTrans(
viewInner: ViewInner,
hostModel: RoamHostComponentOrSeries,
otherModelsToSync: RoamHostComponentOrSeries[] | NullUndefined,
roamTrans: Transformable,
): void {
// This is the inverse operation of `viewCoordSysSetRoamOptionFromModel`.
const viewRectCenter = viewCoordSysGetViewRectCenter(viewInner);
const zoom = getZoomFromRoamTrans(roamTrans);
const notZoomNearZero = mathAbs(zoom) > 1e-6;
tmpCenterITR[0] = notZoomNearZero
? (viewRectCenter[0] - roamTrans.x) / zoom
: viewRectCenter[0]; // Unlikely to occur.
tmpCenterITR[1] = notZoomNearZero
? (viewRectCenter[1] - roamTrans.y) / zoom
: viewRectCenter[1]; // Unlikely to occur.
vectorApplyTransform(tmpCenterITR, tmpCenterITR, viewInner.mtRawInv);
const centerOption = invertBackToCenterOption(viewInner, tmpCenterITR);
syncBackRoamOptionToRoamHostModel(hostModel, centerOption, zoom);
each(otherModelsToSync, function (otherModel) {
if (otherModel !== hostModel) {
syncBackRoamOptionToRoamHostModel(otherModel, centerOption.slice(), zoom);
}
});
}
const tmpCenterITR: number[] = [];
/**
* Models should be updated, otherwise consequent `setOption()` can cause outdated `center`
* and `zoom` to be used.
*/
function syncBackRoamOptionToRoamHostModel(
hostModel: RoamHostComponentOrSeries,
center: RoamOptionMixin['center'],
zoom: number
): void {
const option = hostModel.option;
option.center = center;
option.zoom = zoom;
}
export function clampByZoomLimit(
zoom: number,
zoomLimit: RoamOptionMixin['scaleLimit'] | NullUndefined
): number {
if (zoomLimit) {
const zoomMin = zoomLimit.min || 0;
const zoomMax = zoomLimit.max || Infinity;
zoom = Math.max(
Math.min(zoomMax, zoom),
zoomMin
);
}
return zoom;
}
export function calcCompensationScaleToPreserveNodeSize(
viewCoordSys: View,
model: Model<Pick<RoamOptionMixin, 'nodeScaleRatio'>>
) {
const nodeScaleRatio = (model.getShallow('nodeScaleRatio', true) || 1);
const viewInner = inner(viewCoordSys);
// Scale node when zoom changes
return ((viewInner.zoom - 1) * nodeScaleRatio + 1)
/ (viewInner.trans[VIEW_COORD_SYS_TRANS_OVERALL].scaleX || 1);
}
export default View;