diff --git a/package-lock.json b/package-lock.json index 59ed1fa361..470c5cc217 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "Apache-2.0", "dependencies": { "tslib": "2.3.0", - "zrender": "5.6.1" + "zrender": "github:ecomfe/zrender#v6" }, "devDependencies": { "@babel/code-frame": "7.10.4", @@ -11800,8 +11800,7 @@ }, "node_modules/zrender": { "version": "5.6.1", - "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz", - "integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==", + "resolved": "git+ssh://git@github.com/ecomfe/zrender.git#31a654f2c28580cf479ea7b8531c987b3cb63000", "license": "BSD-3-Clause", "dependencies": { "tslib": "2.3.0" @@ -20569,9 +20568,8 @@ } }, "zrender": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz", - "integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==", + "version": "git+ssh://git@github.com/ecomfe/zrender.git#31a654f2c28580cf479ea7b8531c987b3cb63000", + "from": "zrender@github:ecomfe/zrender#v6", "requires": { "tslib": "2.3.0" } diff --git a/package.json b/package.json index a690b60dc3..e9cd3bf68b 100644 --- a/package.json +++ b/package.json @@ -181,6 +181,8 @@ "./lib/component/axisPointer": "./lib/component/axisPointer.js", "./lib/component/brush": "./lib/component/brush.js", "./lib/component/calendar": "./lib/component/calendar.js", + "./lib/component/matrix": "./lib/component/matrix.js", + "./lib/component/thumbnail": "./lib/component/thumbnail.js", "./lib/component/dataZoom": "./lib/component/dataZoom.js", "./lib/component/dataZoomInside": "./lib/component/dataZoomInside.js", "./lib/component/dataZoomSelect": "./lib/component/dataZoomSelect.js", diff --git a/src/action/roamHelper.ts b/src/action/roamHelper.ts deleted file mode 100644 index f14c8ccfbe..0000000000 --- a/src/action/roamHelper.ts +++ /dev/null @@ -1,92 +0,0 @@ -/* -* 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 type Geo from '../coord/geo/Geo'; -import type View from '../coord/View'; -import type ExtensionAPI from '../core/ExtensionAPI'; -import type { Payload } from '../util/types'; - -export interface RoamPayload extends Payload { - dx: number - dy: number - zoom: number - originX: number - originY: number -} - -function getCenterCoord(view: View, point: number[]) { - // Use projected coord as center because it's linear. - return (view as Geo).pointToProjected - ? (view as Geo).pointToProjected(point) - : view.pointToData(point); -} - -export function updateCenterAndZoom( - view: View, - payload: RoamPayload, - zoomLimit?: { - min?: number, - max?: number - }, - api?: ExtensionAPI -) { - const previousZoom = view.getZoom(); - const center = view.getCenter(); - let zoom = payload.zoom; - - const point = (view as Geo).projectedToPoint - ? (view as Geo).projectedToPoint(center) - : view.dataToPoint(center); - - if (payload.dx != null && payload.dy != null) { - point[0] -= payload.dx; - point[1] -= payload.dy; - - view.setCenter(getCenterCoord(view, point), api); - } - if (zoom != null) { - if (zoomLimit) { - const zoomMin = zoomLimit.min || 0; - const zoomMax = zoomLimit.max || Infinity; - zoom = Math.max( - Math.min(previousZoom * zoom, zoomMax), - zoomMin - ) / previousZoom; - } - - // Zoom on given point(originX, originY) - view.scaleX *= zoom; - view.scaleY *= zoom; - const fixX = (payload.originX - view.x) * (zoom - 1); - const fixY = (payload.originY - view.y) * (zoom - 1); - - view.x -= fixX; - view.y -= fixY; - - view.updateTransform(); - // Get the new center - view.setCenter(getCenterCoord(view, point), api); - view.setZoom(zoom * previousZoom); - } - - return { - center: view.getCenter(), - zoom: view.getZoom() - }; -} diff --git a/src/chart/bar/BarSeries.ts b/src/chart/bar/BarSeries.ts index 13eca219a2..b3f8b9b65b 100644 --- a/src/chart/bar/BarSeries.ts +++ b/src/chart/bar/BarSeries.ts @@ -38,11 +38,13 @@ import SeriesData from '../../data/SeriesData'; import { BrushCommonSelectorsForSeries } from '../../component/brush/selector'; import tokens from '../../visual/tokens'; -export type PolarBarLabelPosition = SeriesLabelOption['position'] - | 'start' | 'insideStart' | 'middle' | 'end' | 'insideEnd'; +type PolarBarLabelPositionExtra = 'start' | 'insideStart' | 'middle' | 'end' | 'insideEnd'; +export type PolarBarLabelPosition = SeriesLabelOption['position'] | PolarBarLabelPositionExtra; -export type BarSeriesLabelOption = Omit - & {position?: PolarBarLabelPosition | 'outside'}; +export type BarSeriesLabelOption = SeriesLabelOption< + CallbackDataParams, + {positionExtra: PolarBarLabelPositionExtra | 'outside'} +>; export interface BarStateOption { itemStyle?: BarItemStyleOption diff --git a/src/chart/bar/PictorialBarSeries.ts b/src/chart/bar/PictorialBarSeries.ts index 37ba25a99c..e1650a7d2e 100644 --- a/src/chart/bar/PictorialBarSeries.ts +++ b/src/chart/bar/PictorialBarSeries.ts @@ -27,7 +27,8 @@ import { StatesOptionMixin, OptionDataItemObject, DefaultEmphasisFocus, - SeriesEncodeOptionMixin + SeriesEncodeOptionMixin, + CallbackDataParams } from '../../util/types'; import type Cartesian2D from '../../coord/cartesian/Cartesian2D'; import { inheritDefaultOption } from '../../util/component'; @@ -35,7 +36,7 @@ import tokens from '../../visual/tokens'; export interface PictorialBarStateOption { itemStyle?: ItemStyleOption - label?: SeriesLabelOption + label?: SeriesLabelOption } interface PictorialBarSeriesSymbolOption { diff --git a/src/chart/custom/CustomSeries.ts b/src/chart/custom/CustomSeries.ts index 49a5159147..0b5bb5db34 100644 --- a/src/chart/custom/CustomSeries.ts +++ b/src/chart/custom/CustomSeries.ts @@ -26,10 +26,12 @@ import { AnimationOption, BlurScope, CallbackDataParams, + CoordinateSystemDataLayout, Dictionary, DimensionLoose, ItemStyleOption, LabelOption, + NullUndefined, OptionDataValue, OrdinalRawValue, ParsedValue, @@ -303,13 +305,30 @@ export interface CustomSeriesRenderItemParamsCoordSys { } export interface CustomSeriesRenderItemCoordinateSystemAPI { coord( - data: OptionDataValue | OptionDataValue[], - clamp?: boolean + // @see `CoordinateSystemDataCoord` + data: (OptionDataValue | NullUndefined) + | (OptionDataValue | NullUndefined)[] + | (OptionDataValue | OptionDataValue[] | NullUndefined)[], + // Some coord sys may support `clamp?: boolean` there. + // Can also be an `{xxx?: ...}` here. + opt?: unknown ): number[]; size?( + // Represents a range, rather than a absolute value. + // e.g., `dataSize: [5, 100]` represents + // data range `5` in x and data range `100` in y. dataSize: OptionDataValue | OptionDataValue[], + // Represents a data point, based on which to calculate size. + // Some axis, such as logarithm, size varies in different points. dataItem?: OptionDataValue | OptionDataValue[] ): number | number[]; + layout?( + // @see `CoordinateSystemDataCoord` + data: (OptionDataValue | NullUndefined) + | (OptionDataValue | NullUndefined)[] + | (OptionDataValue | OptionDataValue[] | NullUndefined)[], + opt?: unknown + ): CoordinateSystemDataLayout; } export type WrapEncodeDefRet = Dictionary; @@ -396,7 +415,7 @@ export default class CustomSeriesModel extends SeriesModel { static type = 'series.custom'; readonly type = CustomSeriesModel.type; - static dependencies = ['grid', 'polar', 'geo', 'singleAxis', 'calendar']; + static dependencies = ['grid', 'polar', 'geo', 'singleAxis', 'calendar', 'matrix']; // preventAutoZ = true; diff --git a/src/chart/custom/CustomView.ts b/src/chart/custom/CustomView.ts index e241352994..08264fd272 100644 --- a/src/chart/custom/CustomView.ts +++ b/src/chart/custom/CustomView.ts @@ -51,6 +51,7 @@ import prepareGeo from '../../coord/geo/prepareCustom'; import prepareSingleAxis from '../../coord/single/prepareCustom'; import preparePolar from '../../coord/polar/prepareCustom'; import prepareCalendar from '../../coord/calendar/prepareCustom'; +import prepareMatrix from '../../coord/matrix/prepareCustom'; import SeriesData, { DefaultDataVisual } from '../../data/SeriesData'; import GlobalModel from '../../model/Global'; import ExtensionAPI from '../../core/ExtensionAPI'; @@ -155,6 +156,8 @@ const attachedTxInfoTmp = { /** + * FIXME: register rather than import directly, for size. + * * To reduce total package size of each coordinate systems, the modules `prepareCustom` * of each coordinate systems are not required by each coordinate systems directly, but * required by the module `custom`. @@ -170,7 +173,8 @@ const prepareCustoms: Dictionary = { geo: prepareGeo, single: prepareSingleAxis, polar: preparePolar, - calendar: prepareCalendar + calendar: prepareCalendar, + matrix: prepareMatrix }; diff --git a/src/chart/funnel/FunnelSeries.ts b/src/chart/funnel/FunnelSeries.ts index 6de44d5c10..e53eb9036c 100644 --- a/src/chart/funnel/FunnelSeries.ts +++ b/src/chart/funnel/FunnelSeries.ts @@ -148,6 +148,7 @@ class FunnelSeriesModel extends SeriesModel { } static defaultOption: FunnelSeriesOption = { + coordinateSystemUsage: 'box', // zlevel: 0, // 一级层叠 z: 2, // 二级层叠 legendHoverLink: true, diff --git a/src/chart/funnel/funnelLayout.ts b/src/chart/funnel/funnelLayout.ts index d1f1708724..952904117a 100644 --- a/src/chart/funnel/funnelLayout.ts +++ b/src/chart/funnel/funnelLayout.ts @@ -25,6 +25,7 @@ import SeriesData from '../../data/SeriesData'; import GlobalModel from '../../model/Global'; import { isFunction } from 'zrender/src/core/util'; + function getSortedIndices(data: SeriesData, sort: FunnelSeriesOption['sort']) { const valueDim = data.mapDimension('value'); const valueArr = data.mapArray(valueDim, function (val: number) { @@ -242,7 +243,10 @@ export default function funnelLayout(ecModel: GlobalModel, api: ExtensionAPI) { const data = seriesModel.getData(); const valueDim = data.mapDimension('value'); const sort = seriesModel.get('sort'); - const viewRect = layout.getViewRect(seriesModel, api); + + const layoutRef = layout.createBoxLayoutReference(seriesModel, api); + const viewRect = layout.getLayoutRect(seriesModel.getBoxLayoutParams(), layoutRef.refContainer); + const orient = seriesModel.get('orient'); const viewWidth = viewRect.width; const viewHeight = viewRect.height; diff --git a/src/chart/graph/GraphSeries.ts b/src/chart/graph/GraphSeries.ts index 1c3c8fe628..88aee86d30 100644 --- a/src/chart/graph/GraphSeries.ts +++ b/src/chart/graph/GraphSeries.ts @@ -43,7 +43,8 @@ import { GraphEdgeItemObject, OptionDataValueNumeric, CallbackDataParams, - DefaultEmphasisFocus + DefaultEmphasisFocus, + PreserveAspectMixin } from '../../util/types'; import SeriesModel from '../../model/Series'; import Graph from '../../data/Graph'; @@ -54,7 +55,6 @@ import { LineDataVisual } from '../../visual/commonVisualTypes'; import { createTooltipMarkup } from '../../component/tooltip/tooltipMarkup'; import { defaultSeriesFormatTooltip } from '../../component/tooltip/seriesFormatTooltip'; import {initCurvenessList, createEdgeMapForCurveness} from '../helper/multipleGraphEdgeHelper'; -import Thumbnail, { ThumbnailOption } from './Thumbnail'; import tokens from '../../visual/tokens'; @@ -144,7 +144,8 @@ export interface GraphSeriesOption SeriesOnGeoOptionMixin, SeriesOnSingleOptionMixin, SymbolOptionMixin, RoamOptionMixin, - BoxLayoutOptionMixin { + BoxLayoutOptionMixin, + PreserveAspectMixin { type?: 'graph' @@ -230,8 +231,6 @@ export interface GraphSeriesOption * auto curveness for multiple edge, invalid when `lineStyle.curveness` is set */ autoCurveness?: boolean | number | number[] - - thumbnail?: ThumbnailOption } class GraphSeriesModel extends SeriesModel { @@ -514,8 +513,6 @@ class GraphSeriesModel extends SeriesModel { borderColor: tokens.color.primary } }, - - thumbnail: Thumbnail.defaultOption }; } diff --git a/src/chart/graph/GraphView.ts b/src/chart/graph/GraphView.ts index 614a0043be..0311b59f66 100644 --- a/src/chart/graph/GraphView.ts +++ b/src/chart/graph/GraphView.ts @@ -23,9 +23,9 @@ import RoamController from '../../component/helper/RoamController'; import { updateViewOnZoom, updateViewOnPan, - RoamControllerHost + RoamControllerHost, + RoamPayload } from '../../component/helper/roamHelper'; -import {onIrrelevantElement} from '../../component/helper/cursorHelper'; import * as graphic from '../../util/graphic'; import adjustEdge from './adjustEdge'; import {getNodeGlobalScale} from './graphHelper'; @@ -39,12 +39,13 @@ import Symbol from '../helper/Symbol'; import SeriesData from '../../data/SeriesData'; import Line from '../helper/Line'; import { getECData } from '../../util/innerStore'; -import Thumbnail from './Thumbnail'; import { simpleLayoutEdge } from './simpleLayoutHelper'; import { circularLayout, rotateNodeLabel } from './circularLayoutHelper'; import { clone, extend } from 'zrender/src/core/util'; import ECLinePath from '../helper/LinePath'; +import { NullUndefined } from '../../util/types'; +import { getThumbnailBridge, ThumbnailBridge } from '../../component/helper/thumbnailBridge'; function isViewCoordSys(coordSys: CoordinateSystem): coordSys is View { return coordSys.type === 'view'; @@ -65,14 +66,16 @@ class GraphView extends ChartView { private _model: GraphSeriesModel; + private _api: ExtensionAPI; + private _layoutTimeout: number; private _layouting: boolean; - private _thumbnail: Thumbnail = new Thumbnail(); - private _mainGroup: graphic.Group; + private _active: boolean; + init(ecModel: GlobalModel, api: ExtensionAPI) { const symbolDraw = new SymbolDraw(); const lineDraw = new LineDraw(); @@ -100,6 +103,13 @@ class GraphView extends ChartView { let isForceLayout = false; this._model = seriesModel; + this._api = api; + this._active = true; + + const thumbnailInfo = this._getThumbnailInfo(); + if (thumbnailInfo) { + thumbnailInfo.bridge.reset(api); + } const symbolDraw = this._symbolDraw; const lineDraw = this._lineDraw; @@ -127,7 +137,7 @@ class GraphView extends ChartView { this._updateNodeAndLinkScale(); - this._updateController(seriesModel, ecModel, api); + this._updateController(null, seriesModel, api); clearTimeout(this._layoutTimeout); const forceLayout = seriesModel.forceLayout; @@ -218,7 +228,10 @@ class GraphView extends ChartView { }); this._firstRender = false; - isForceLayout || this._renderThumbnail(seriesModel, api, this._symbolDraw, this._lineDraw); + // Force layout will render thumbnail when layout is finished. + if (!isForceLayout) { + this._renderThumbnail(seriesModel, api, this._symbolDraw, this._lineDraw); + } } dispose() { @@ -226,7 +239,6 @@ class GraphView extends ChartView { this._controller && this._controller.dispose(); this._controllerHost = null; - this._thumbnail.dispose(); } private _startForceLayoutIteration( @@ -235,12 +247,14 @@ class GraphView extends ChartView { layoutAnimation?: boolean ) { const self = this; + let firstRendered = false; (function step() { forceLayout.step(function (stopped) { - if (stopped) { + self.updateLayout(self._model); + if (stopped || !firstRendered) { + firstRendered = true; self._renderThumbnail(self._model, api, self._symbolDraw, self._lineDraw); } - self.updateLayout(self._model); (self._layouting = !stopped) && ( layoutAnimation ? (self._layoutTimeout = setTimeout(step, 16) as any) @@ -251,78 +265,89 @@ class GraphView extends ChartView { } private _updateController( + clipRect: graphic.BoundingRect | NullUndefined, seriesModel: GraphSeriesModel, - ecModel: GlobalModel, api: ExtensionAPI ) { const controller = this._controller; const controllerHost = this._controllerHost; - const group = this.group; - - controller.setPointerChecker((e, x, y) => { - const rect = group.getBoundingRect(); - rect.applyTransform(group.transform); - return rect.contain(x, y) - && !this._thumbnail.contain(x, y) - && !onIrrelevantElement(e, api, seriesModel); - }); + const coordSys = seriesModel.coordinateSystem; - if (!isViewCoordSys(seriesModel.coordinateSystem)) { + if (!isViewCoordSys(coordSys)) { controller.disable(); return; } - controller.enable(seriesModel.get('roam')); + controller.enable(seriesModel.get('roam'), { + api, + zInfo: {component: seriesModel}, + triggerInfo: { + roamTrigger: seriesModel.get('roamTrigger'), + isInSelf: (e, x, y) => coordSys.containPoint([x, y]), + isInClip: (e, x, y) => !clipRect || clipRect.contain(x, y), + }, + }); controllerHost.zoomLimit = seriesModel.get('scaleLimit'); - controllerHost.zoom = seriesModel.coordinateSystem.getZoom(); + controllerHost.zoom = coordSys.getZoom(); controller .off('pan') .off('zoom') .on('pan', (e) => { - this._updateViewOnPan(seriesModel, api, e.dx, e.dy); + api.dispatchAction({ + seriesId: seriesModel.id, + type: 'graphRoam', + dx: e.dx, + dy: e.dy + }); }) .on('zoom', (e) => { - this._updateViewOnZoom(seriesModel, api, e.scale, e.originX, e.originY); + api.dispatchAction({ + seriesId: seriesModel.id, + type: 'graphRoam', + zoom: e.scale, + originX: e.originX, + originY: e.originY + }); }); } - private _updateViewOnPan( + /** + * A performance shortcut - called by action handler to update the view directly + * without any data/visual processing (which are assumed to be unchanged), while + * ensuring consistent behavior between internal and external action triggers. + */ + updateViewOnPan( seriesModel: GraphSeriesModel, api: ExtensionAPI, - dx: number, - dy: number + params: Pick ): void { - updateViewOnPan(this._controllerHost, dx, dy); - api.dispatchAction({ - seriesId: seriesModel.id, - type: 'graphRoam', - dx: dx, - dy: dy - }); - this._thumbnail.updateWindow(); + if (!this._active) { + return; + } + updateViewOnPan(this._controllerHost, params.dx, params.dy); + this._updateThumbnailWindow(); } - private _updateViewOnZoom( + /** + * A performance shortcut - called by action handler to update the view directly + * without any data/visual processing (which are assumed to be unchanged), while + * ensuring consistent behavior between internal and external action triggers. + */ + updateViewOnZoom( seriesModel: GraphSeriesModel, api: ExtensionAPI, - scale: number, - originX: number, - originY: number + params: Pick ) { - updateViewOnZoom(this._controllerHost, scale, originX, originY); - api.dispatchAction({ - seriesId: seriesModel.id, - type: 'graphRoam', - zoom: scale, - originX: originX, - originY: originY - }); + if (!this._active) { + return; + } + updateViewOnZoom(this._controllerHost, params.zoom, params.originX, params.originY); this._updateNodeAndLinkScale(); adjustEdge(seriesModel.getGraph(), getNodeGlobalScale(seriesModel)); this._lineDraw.updateLayout(); // Only update label layout on zoom api.updateLabelLayout(); - this._thumbnail.updateWindow(); + this._updateThumbnailWindow(); } private _updateNodeAndLinkScale() { @@ -337,6 +362,10 @@ class GraphView extends ChartView { } updateLayout(seriesModel: GraphSeriesModel) { + if (!this._active) { + return; + } + adjustEdge(seriesModel.getGraph(), getNodeGlobalScale(seriesModel)); this._symbolDraw.updateLayout(); @@ -344,6 +373,7 @@ class GraphView extends ChartView { } remove() { + this._active = false; clearTimeout(this._layoutTimeout); this._layouting = false; this._layoutTimeout = null; @@ -351,84 +381,99 @@ class GraphView extends ChartView { this._symbolDraw && this._symbolDraw.remove(); this._lineDraw && this._lineDraw.remove(); this._controller && this._controller.disable(); - this._thumbnail.remove(); } - // TODO: register thumbnail (consider code size). + /** + * Get thumbnail data structure only if supported. + */ + private _getThumbnailInfo(): { + bridge: ThumbnailBridge + coordSys: View + } | NullUndefined { + const model = this._model; + const coordSys = model.coordinateSystem; + if (coordSys.type !== 'view') { + return; + } + const bridge = getThumbnailBridge(model); + if (!bridge) { + return; + } + return { + bridge, + coordSys: coordSys as View, + }; + } + + private _updateThumbnailWindow() { + const info = this._getThumbnailInfo(); + if (info) { + info.bridge.updateWindow(info.coordSys.transform, this._api); + } + } + private _renderThumbnail( seriesModel: GraphSeriesModel, api: ExtensionAPI, symbolDraw: SymbolDraw, lineDraw: LineDraw ) { - const thumbnail = this._thumbnail; - this.group.add(thumbnail.group); - - const renderThumbnailContent = (viewGroup: graphic.Group) => { - const symbolNodes = symbolDraw.group.children(); - const lineNodes = lineDraw.group.children(); - - const lineGroup = new graphic.Group(); - const symbolGroup = new graphic.Group(); - viewGroup.add(symbolGroup); - viewGroup.add(lineGroup); - - for (let i = 0; i < symbolNodes.length; i++) { - const node = symbolNodes[i]; - const sub = (node as graphic.Group).children()[0]; - const x = (node as Symbol).x; - const y = (node as Symbol).y; - const subShape = clone((sub as graphic.Path).shape); - const shape = extend(subShape, { - width: sub.scaleX, - height: sub.scaleY, - x: x - sub.scaleX / 2, - y: y - sub.scaleY / 2 - }); - const style = clone((sub as graphic.Path).style); - const subThumbnail = new (sub as any).constructor({ - shape, - style, - z2: 151 - }); - symbolGroup.add(subThumbnail); - } + const info = this._getThumbnailInfo(); + if (!info) { + return; + } - for (let i = 0; i < lineNodes.length; i++) { - const node = lineNodes[i]; - const line = (node as graphic.Group).children()[0]; - const style = clone((line as ECLinePath).style); - const shape = clone((line as ECLinePath).shape); - const lineThumbnail = new ECLinePath({ - style, - shape, - z2: 151 - }); - lineGroup.add(lineThumbnail); - } - }; + const bridgeGroup = new graphic.Group(); + const symbolNodes = symbolDraw.group.children(); + const lineNodes = lineDraw.group.children(); + + const lineGroup = new graphic.Group(); + const symbolGroup = new graphic.Group(); + bridgeGroup.add(symbolGroup); + bridgeGroup.add(lineGroup); + + // TODO: reuse elemenents for performance in large graph? + for (let i = 0; i < symbolNodes.length; i++) { + const node = symbolNodes[i]; + const sub = (node as graphic.Group).children()[0]; + const x = (node as Symbol).x; + const y = (node as Symbol).y; + const subShape = clone((sub as graphic.Path).shape); + const shape = extend(subShape, { + width: sub.scaleX, + height: sub.scaleY, + x: x - sub.scaleX / 2, + y: y - sub.scaleY / 2 + }); + const style = clone((sub as graphic.Path).style); + const subThumbnail = new (sub as any).constructor({ + shape, + style, + z2: 151 + }); + symbolGroup.add(subThumbnail); + } + + for (let i = 0; i < lineNodes.length; i++) { + const node = lineNodes[i]; + const line = (node as graphic.Group).children()[0]; + const style = clone((line as ECLinePath).style); + const shape = clone((line as ECLinePath).shape); + const lineThumbnail = new ECLinePath({ + style, + shape, + z2: 151 + }); + lineGroup.add(lineThumbnail); + } - thumbnail.render({ - seriesModel, + info.bridge.renderContent({ api, roamType: seriesModel.get('roam'), - z2Setting: { - background: 150, - window: 160 - }, - seriesBoundingRect: this._mainGroup.getBoundingRect(), - renderThumbnailContent + viewportRect: null, + group: bridgeGroup, + targetTrans: info.coordSys.transform, }); - - thumbnail - .off('pan') - .off('zoom') - .on('pan', (event) => { - this._updateViewOnPan(seriesModel, api, event.dx, event.dy); - }) - .on('zoom', (event) => { - this._updateViewOnZoom(seriesModel, api, event.scale, event.originX, event.originY); - }); } } diff --git a/src/chart/graph/Thumbnail.ts b/src/chart/graph/Thumbnail.ts deleted file mode 100644 index 02c04a1b2f..0000000000 --- a/src/chart/graph/Thumbnail.ts +++ /dev/null @@ -1,273 +0,0 @@ -import * as graphic from '../../util/graphic'; -import ExtensionAPI from '../../core/ExtensionAPI'; -import * as layout from '../../util/layout'; -import GraphSeriesModel from './GraphSeries'; -import * as zrUtil from 'zrender/src/core/util'; -import View from '../../coord/View'; -import BoundingRect from 'zrender/src/core/BoundingRect'; -import * as matrix from 'zrender/src/core/matrix'; -import * as vector from 'zrender/src/core/vector'; -import SeriesModel from '../../model/Series'; -import { BoxLayoutOptionMixin, ItemStyleOption } from '../../util/types'; -import RoamController, { RoamEventDefinition, RoamType } from '../../component/helper/RoamController'; -import Eventful from 'zrender/src/core/Eventful'; -import tokens from '../../visual/tokens'; - - -// TODO: -// Thumbnail should not be bound to a single series when used on -// coordinate system like cartesian and geo/map? -// Should we make thumbnail as a component like markers/axisPointer/brush did? - - -interface BorderRadiusOption { - borderRadius?: number | number[] -} - -// TODO: apply to other series -export interface ThumbnailOption extends BoxLayoutOptionMixin { - show?: boolean, - itemStyle?: ItemStyleOption & BorderRadiusOption - windowStyle?: ItemStyleOption & BorderRadiusOption -} - -interface WindowRect extends graphic.Rect { - __r?: BorderRadiusOption['borderRadius']; -} - -export interface ThumbnailZ2Setting { - background: number; - window: number; -} - -class Thumbnail extends Eventful> { - - group = new graphic.Group(); - - private _api: ExtensionAPI; - private _seriesModel: GraphSeriesModel; - - private _windowRect: WindowRect; - private _contentBoundingRect: BoundingRect; - private _thumbnailCoordSys: View; - - private _mtSeriesToThumbnail: matrix.MatrixArray; - private _mtThumbnailToSerise: matrix.MatrixArray; - - private _thumbnailController: RoamController; - private _isEnabled: boolean; - - render(opt: { - seriesModel: GraphSeriesModel; - api: ExtensionAPI; - roamType: RoamType; - z2Setting: ThumbnailZ2Setting; - seriesBoundingRect: BoundingRect, - renderThumbnailContent: (viewGroup: graphic.Group) => void - }) { - const seriesModel = this._seriesModel = opt.seriesModel; - const api = this._api = opt.api; - - const thumbnailModel = seriesModel.getModel('thumbnail'); - const group = this.group; - - this._isEnabled = thumbnailModel.get('show', true) && isSeriesSupported(seriesModel); - if (!this._isEnabled) { - this._clear(); - return; - } - - group.removeAll(); - - const z2Setting = opt.z2Setting; - const cursor = opt.roamType ? 'pointer' : 'default'; - const itemStyleModel = thumbnailModel.getModel('itemStyle'); - const itemStyle = itemStyleModel.getItemStyle(); - itemStyle.fill = seriesModel.ecModel.get('backgroundColor') || tokens.color.neutral00; - - // Try to use border-box in thumbnail, see https://github.com/apache/echarts/issues/18022 - const boxBorderWidth = itemStyle.lineWidth || 0; - const boxContainBorder = layout.getLayoutRect( - { - left: thumbnailModel.get('left', true), - top: thumbnailModel.get('top', true), - right: thumbnailModel.get('right', true), - bottom: thumbnailModel.get('bottom', true), - width: thumbnailModel.get('width', true), - height: thumbnailModel.get('height', true) - }, - { - width: api.getWidth(), - height: api.getHeight() - } - ); - const borderBoundingRect = - layout.applyPedding(boxContainBorder.clone(), boxBorderWidth / 2); - const contentBoundingRect = this._contentBoundingRect = - layout.applyPedding(boxContainBorder.clone(), boxBorderWidth); - - const clipGroup = new graphic.Group(); - group.add(clipGroup); - clipGroup.setClipPath(new graphic.Rect({ - shape: contentBoundingRect.plain() - })); - - const seriesViewGroup = new graphic.Group(); - clipGroup.add(seriesViewGroup); - opt.renderThumbnailContent(seriesViewGroup); - - // Draw border and background and shadow of thumbnail box. - group.add(new graphic.Rect({ - style: itemStyle, - shape: zrUtil.extend(borderBoundingRect.plain(), { - r: itemStyleModel.get('borderRadius', true) - }), - cursor, - z2: z2Setting.background - })); - - const coordSys = this._thumbnailCoordSys = new View(); - const seriesBoundingRect = opt.seriesBoundingRect; - coordSys.setBoundingRect( - seriesBoundingRect.x, seriesBoundingRect.y, seriesBoundingRect.width, seriesBoundingRect.height - ); - - // Find an approperiate rect in contentBoundingRect for the entire graph. - const graphViewRect = layout.getLayoutRect( - { - left: 'center', - top: 'center', - aspect: seriesBoundingRect.width / seriesBoundingRect.height - }, - contentBoundingRect - ); - coordSys.setViewRect(graphViewRect.x, graphViewRect.y, graphViewRect.width, graphViewRect.height); - seriesViewGroup.attr(coordSys.getTransformInfo().raw); - - const windowStyleModel = thumbnailModel.getModel('windowStyle'); - const windowRect: WindowRect = this._windowRect = new graphic.Rect({ - style: windowStyleModel.getItemStyle(), - cursor, - z2: z2Setting.window - }); - windowRect.__r = windowStyleModel.get('borderRadius', true); - clipGroup.add(windowRect); - - this._resetRoamController(opt.roamType); - - this.updateWindow(); - } - - /** - * Update window by series view roam status. - */ - updateWindow(): void { - if (!this._isEnabled) { - return; - } - - this._updateTransform(); - - const rect = new BoundingRect(0, 0, this._api.getWidth(), this._api.getHeight()); - rect.applyTransform(this._mtSeriesToThumbnail); - const windowRect = this._windowRect; - windowRect.setShape(zrUtil.defaults({r: windowRect.__r}, rect)); - } - - /** - * Create transform that convert pixel vector from - * series coordinate system to thumbnail coordinate system. - * - * TODO: consider other type of series. - */ - private _updateTransform(): void { - const seriesCoordSys = this._seriesModel.coordinateSystem as View; - this._mtSeriesToThumbnail = matrix.mul([], this._thumbnailCoordSys.transform, seriesCoordSys.invTransform); - this._mtThumbnailToSerise = matrix.invert([], this._mtSeriesToThumbnail); - } - - private _resetRoamController(roamType: RoamType): void { - let thumbnailController = this._thumbnailController; - if (!thumbnailController) { - thumbnailController = this._thumbnailController = new RoamController(this._api.getZr()); - thumbnailController.setPointerChecker((e, x, y) => this.contain(x, y)); - } - - thumbnailController.enable(roamType); - thumbnailController - .off('pan') - .off('zoom') - .on('pan', (event) => { - const transform = this._mtThumbnailToSerise; - const oldOffset = vector.applyTransform([], [event.oldX, event.oldY], transform); - // reverse old and new because we pan window rather graph in thumbnail. - const newOffset = vector.applyTransform([], [event.oldX - event.dx, event.oldY - event.dy], transform); - this.trigger('pan', { - dx: newOffset[0] - oldOffset[0], - dy: newOffset[1] - oldOffset[1], - oldX: oldOffset[0], - oldY: oldOffset[1], - newX: newOffset[0], - newY: newOffset[1], - isAvailableBehavior: event.isAvailableBehavior - }); - }) - .on('zoom', (event) => { - const offset = vector.applyTransform([], [event.originX, event.originY], this._mtThumbnailToSerise); - this.trigger('zoom', { - scale: 1 / event.scale, - originX: offset[0], - originY: offset[1], - isAvailableBehavior: event.isAvailableBehavior - }); - }); - } - - contain(x: number, y: number): boolean { - return this._contentBoundingRect && this._contentBoundingRect.contain(x, y); - } - - private _clear(): void { - this.group.removeAll(); - this._thumbnailController && this._thumbnailController.disable(); - } - - remove() { - this._clear(); - } - - dispose() { - this._clear(); - } - - static defaultOption: ThumbnailOption = { - show: false, - - right: 0, - bottom: 0, - - height: '25%', - width: '25%', - - itemStyle: { - // Use echarts option.backgorundColor by default. - borderColor: tokens.color.border, - borderWidth: 1 - }, - - windowStyle: { - borderWidth: 1, - color: tokens.color.neutral30, - borderColor: tokens.color.neutral40, - opacity: 0.3 - } - }; -} - -// TODO: other coordinate system. -function isSeriesSupported(seriesModel: SeriesModel): boolean { - const seriesCoordSys = seriesModel.coordinateSystem; - return seriesCoordSys && seriesCoordSys.type === 'view'; -} - -export default Thumbnail; \ No newline at end of file diff --git a/src/chart/graph/createView.ts b/src/chart/graph/createView.ts index 238c4b6184..1cc6071772 100644 --- a/src/chart/graph/createView.ts +++ b/src/chart/graph/createView.ts @@ -19,29 +19,35 @@ // FIXME Where to create the simple view coordinate system import View from '../../coord/View'; -import {getLayoutRect} from '../../util/layout'; +import {createBoxLayoutReference, getLayoutRect, applyPreserveAspect} from '../../util/layout'; import * as bbox from 'zrender/src/core/bbox'; import GraphSeriesModel, { GraphNodeItemOption } from './GraphSeries'; import ExtensionAPI from '../../core/ExtensionAPI'; import GlobalModel from '../../model/Global'; import { extend } from 'zrender/src/core/util'; +import { injectCoordSysByOption } from '../../core/CoordinateSystem'; function getViewRect(seriesModel: GraphSeriesModel, api: ExtensionAPI, aspect: number) { + const layoutRef = createBoxLayoutReference(seriesModel, api); const option = extend(seriesModel.getBoxLayoutParams(), { aspect: aspect }); - return getLayoutRect(option, { - width: api.getWidth(), - height: api.getHeight() - }); + const viewRect = getLayoutRect(option, layoutRef.refContainer); + return applyPreserveAspect(seriesModel, viewRect, aspect); } export default function createViewCoordSys(ecModel: GlobalModel, api: ExtensionAPI) { const viewList: View[] = []; ecModel.eachSeriesByType('graph', function (seriesModel: GraphSeriesModel) { - const coordSysType = seriesModel.get('coordinateSystem'); - if (!coordSysType || coordSysType === 'view') { + injectCoordSysByOption({ + targetModel: seriesModel, + coordSysType: 'view', + coordSysProvider: createViewCoordSys, + isDefaultDataCoordSys: true, + }); + + function createViewCoordSys() { const data = seriesModel.getData(); const positions = data.mapArray(function (idx) { const itemModel = data.getItemModel(idx); @@ -74,24 +80,23 @@ export default function createViewCoordSys(ecModel: GlobalModel, api: ExtensionA const bbWidth = max[0] - min[0]; const bbHeight = max[1] - min[1]; - const viewWidth = viewRect.width; - const viewHeight = viewRect.height; - - const viewCoordSys = seriesModel.coordinateSystem = new View(); + const viewCoordSys = new View(null, {api, ecModel}); viewCoordSys.zoomLimit = seriesModel.get('scaleLimit'); viewCoordSys.setBoundingRect( min[0], min[1], bbWidth, bbHeight ); viewCoordSys.setViewRect( - viewRect.x, viewRect.y, viewWidth, viewHeight + viewRect.x, viewRect.y, viewRect.width, viewRect.height ); // Update roam info - viewCoordSys.setCenter(seriesModel.get('center'), api); + viewCoordSys.setCenter(seriesModel.get('center')); viewCoordSys.setZoom(seriesModel.get('zoom')); viewList.push(viewCoordSys); + + return viewCoordSys; } }); diff --git a/src/chart/graph/install.ts b/src/chart/graph/install.ts index 2ffec7b035..a92dbb1159 100644 --- a/src/chart/graph/install.ts +++ b/src/chart/graph/install.ts @@ -29,16 +29,11 @@ import createView from './createView'; import View from '../../coord/View'; import GraphView from './GraphView'; import GraphSeriesModel from './GraphSeries'; -import { RoamPayload, updateCenterAndZoom } from '../../action/roamHelper'; +import { RoamPayload, updateCenterAndZoomInAction } from '../../component/helper/roamHelper'; import GlobalModel from '../../model/Global'; import { noop } from 'zrender/src/core/util'; import type ExtensionAPI from '../../core/ExtensionAPI'; -const actionInfo = { - type: 'graphRoam', - event: 'graphRoam', - update: 'none' -}; export function install(registers: EChartsExtensionInstallRegisters) { @@ -73,13 +68,27 @@ export function install(registers: EChartsExtensionInstallRegisters) { }, noop); // Register roam action. - registers.registerAction(actionInfo, function (payload: RoamPayload, ecModel: GlobalModel, api: ExtensionAPI) { + registers.registerAction({ + type: 'graphRoam', + event: 'graphRoam', + update: 'none' + }, function (payload: RoamPayload, ecModel: GlobalModel, api: ExtensionAPI) { ecModel.eachComponent({ mainType: 'series', query: payload }, function (seriesModel: GraphSeriesModel) { - const coordSys = seriesModel.coordinateSystem as View; - const res = updateCenterAndZoom(coordSys, payload, undefined, api); + const graphView = api.getViewOfSeriesModel(seriesModel) as GraphView; + if (graphView) { + if (payload.dx != null && payload.dy != null) { + graphView.updateViewOnPan(seriesModel, api, payload); + } + if (payload.zoom != null && payload.originX != null && payload.originY != null) { + graphView.updateViewOnZoom(seriesModel, api, payload); + } + } + + const coordSys = seriesModel.coordinateSystem as View; + const res = updateCenterAndZoomInAction(coordSys, payload, seriesModel.get('scaleLimit')); seriesModel.setCenter && seriesModel.setCenter(res.center); diff --git a/src/chart/heatmap/HeatmapSeries.ts b/src/chart/heatmap/HeatmapSeries.ts index 9202603f36..84b0a756c5 100644 --- a/src/chart/heatmap/HeatmapSeries.ts +++ b/src/chart/heatmap/HeatmapSeries.ts @@ -38,6 +38,7 @@ import SeriesData from '../../data/SeriesData'; import type Geo from '../../coord/geo/Geo'; import type Cartesian2D from '../../coord/cartesian/Cartesian2D'; import type Calendar from '../../coord/calendar/Calendar'; +import Matrix from '../../coord/matrix/Matrix'; import tokens from '../../visual/tokens'; type HeatmapDataValue = OptionDataValue[]; @@ -50,16 +51,16 @@ export interface HeatmapStateOption { label?: SeriesLabelOption } -interface FunnelStatesMixin { +interface HeatmapStatesMixin { emphasis?: DefaultStatesMixinEmphasis } export interface HeatmapDataItemOption extends HeatmapStateOption, - StatesOptionMixin { + StatesOptionMixin { value: HeatmapDataValue } export interface HeatmapSeriesOption - extends SeriesOption, FunnelStatesMixin>, + extends SeriesOption, HeatmapStatesMixin>, HeatmapStateOption, SeriesOnCartesianOptionMixin, SeriesOnGeoOptionMixin, @@ -68,7 +69,7 @@ export interface HeatmapSeriesOption type?: 'heatmap' - coordinateSystem?: 'cartesian2d' | 'geo' | 'calendar' + coordinateSystem?: 'cartesian2d' | 'geo' | 'calendar' | 'matrix' // Available on geo coordinate system blurSize?: number @@ -85,9 +86,8 @@ class HeatmapSeriesModel extends SeriesModel { static readonly type = 'series.heatmap'; readonly type = HeatmapSeriesModel.type; - static readonly dependencies = ['grid', 'geo', 'calendar']; - // @ts-ignore - coordinateSystem: Cartesian2D | Geo | Calendar; + static readonly dependencies = ['grid', 'geo', 'calendar', 'matrix']; + coordinateSystem: Cartesian2D | Geo | Calendar | Matrix; getInitialData(option: HeatmapSeriesOption, ecModel: GlobalModel): SeriesData { return createSeriesData(null, this, { diff --git a/src/chart/heatmap/HeatmapView.ts b/src/chart/heatmap/HeatmapView.ts index 235d757ab9..4e21c179e6 100644 --- a/src/chart/heatmap/HeatmapView.ts +++ b/src/chart/heatmap/HeatmapView.ts @@ -34,6 +34,7 @@ import type Cartesian2D from '../../coord/cartesian/Cartesian2D'; import type Calendar from '../../coord/calendar/Calendar'; import { setLabelStyle, getLabelStatesModels } from '../../label/labelStyle'; import type Element from 'zrender/src/Element'; +import type Matrix from '../../coord/matrix/Matrix'; // Coord can be 'geo' 'bmap' 'amap' 'leaflet'... interface GeoLikeCoordSys extends CoordinateSystem { @@ -129,8 +130,11 @@ class HeatmapView extends ChartView { this.group.removeAll(); const coordSys = seriesModel.coordinateSystem; - if (coordSys.type === 'cartesian2d' || coordSys.type === 'calendar') { - this._renderOnCartesianAndCalendar(seriesModel, api, 0, seriesModel.getData().count()); + if (coordSys.type === 'cartesian2d' + || coordSys.type === 'calendar' + || coordSys.type === 'matrix' + ) { + this._renderOnGridLike(seriesModel, api, 0, seriesModel.getData().count()); } else if (isGeoCoordSys(coordSys)) { this._renderOnGeo( @@ -157,7 +161,7 @@ class HeatmapView extends ChartView { } else { this._progressiveEls = []; - this._renderOnCartesianAndCalendar(seriesModel, api, params.start, params.end, true); + this._renderOnGridLike(seriesModel, api, params.start, params.end, true); } } } @@ -166,16 +170,16 @@ class HeatmapView extends ChartView { graphic.traverseElements(this._progressiveEls || this.group, cb); } - _renderOnCartesianAndCalendar( + _renderOnGridLike( seriesModel: HeatmapSeriesModel, api: ExtensionAPI, start: number, end: number, incremental?: boolean ) { - - const coordSys = seriesModel.coordinateSystem as Cartesian2D | Calendar; + const coordSys = seriesModel.coordinateSystem as Cartesian2D | Calendar | Matrix; const isCartesian2d = isCoordinateSystemType(coordSys, 'cartesian2d'); + const isMatrix = isCoordinateSystemType(coordSys, 'matrix'); let width; let height; let xAxisExtent; @@ -214,7 +218,7 @@ class HeatmapView extends ChartView { let blurScope = emphasisModel.get('blurScope'); let emphasisDisabled = emphasisModel.get('disabled'); - const dataDims = isCartesian2d + const dataDims = (isCartesian2d || isMatrix) ? [ data.mapDimension('x'), data.mapDimension('y'), @@ -260,22 +264,34 @@ class HeatmapView extends ChartView { style }); } - else { + else if (isMatrix) { + const shape = coordSys.dataToLayout([ + data.get(dataDims[0], idx), + data.get(dataDims[1], idx) + ]).rect; + if (zrUtil.eqNaN(shape.x)) { + continue; + } + rect = new graphic.Rect({ + z2: 1, + shape, + style, + }); + } + else { // Calendar // Ignore empty data if (isNaN(data.get(dataDims[1], idx) as number)) { continue; } - - const contentShape = coordSys.dataToRect([data.get(dataDims[0], idx)]).contentShape; - // Ignore data that are not in range - if (isNaN(contentShape.x) || isNaN(contentShape.y)) { + const layout = coordSys.dataToLayout([data.get(dataDims[0], idx)]); + const shape = layout.contentRect || layout.rect; + if (zrUtil.eqNaN(shape.x) || zrUtil.eqNaN(shape.y)) { continue; } - rect = new graphic.Rect({ z2: 1, - shape: contentShape, - style + shape, + style, }); } diff --git a/src/chart/helper/createGraphFromNodeEdge.ts b/src/chart/helper/createGraphFromNodeEdge.ts index 14eea23fd3..a6fcaaed95 100644 --- a/src/chart/helper/createGraphFromNodeEdge.ts +++ b/src/chart/helper/createGraphFromNodeEdge.ts @@ -69,7 +69,7 @@ export default function createGraphFromNodeEdge( const coordSys = seriesModel.get('coordinateSystem'); let nodeData; - if (coordSys === 'cartesian2d' || coordSys === 'polar') { + if (coordSys === 'cartesian2d' || coordSys === 'polar' || coordSys === 'matrix') { nodeData = createSeriesData(nodes, seriesModel); } else { diff --git a/src/chart/helper/createSeriesData.ts b/src/chart/helper/createSeriesData.ts index 4e1bfd714c..a154906435 100644 --- a/src/chart/helper/createSeriesData.ts +++ b/src/chart/helper/createSeriesData.ts @@ -161,6 +161,16 @@ function createSeriesData( const dimValueGetter = firstCategoryDimIndex != null && isNeedCompleteOrdinalData(source) + /** + * This serves this case: + * var echarts_option = { + * xAxis: { data: ['a', 'b', 'c'] }, + * yAxis: {} + * series: { data: [555, 666, 777] } + * }; + * The `series.data` is completed to: + * [[0, 555], [1, 666], [2, 777]] + */ ? function (this: DataStore, itemOpt: any, dimName: string, dataIndex: number, dimIndex: number) { // Use dataIndex as ordinal value in categoryAxis return dimIndex === firstCategoryDimIndex diff --git a/src/chart/map/MapSeries.ts b/src/chart/map/MapSeries.ts index 89975368a9..294c1a57df 100644 --- a/src/chart/map/MapSeries.ts +++ b/src/chart/map/MapSeries.ts @@ -45,6 +45,7 @@ import { createTooltipMarkup } from '../../component/tooltip/tooltipMarkup'; import {createSymbol, ECSymbol} from '../../util/symbol'; import {LegendIconParams} from '../../component/legend/LegendModel'; import {Group} from '../../util/graphic'; +import { CoordinateSystemUsageKind, decideCoordSysUsageKind } from '../../core/CoordinateSystem'; import { GeoJSONRegion } from '../../coord/geo/Region'; import tokens from '../../visual/tokens'; @@ -159,10 +160,15 @@ class MapSeries extends SeriesModel { * inner exclusive geo model. */ getHostGeoModel(): GeoModel { - const geoIndex = this.option.geoIndex; - return geoIndex != null - ? this.ecModel.getComponent('geo', geoIndex) as GeoModel - : null; + if (decideCoordSysUsageKind(this).kind === CoordinateSystemUsageKind.boxCoordSys) { + // Always use an internal geo if specify a boxCoordSys. + // Notice that currently we do not support laying out a geo based on + // another geo, but preserve the possibility. + return; + } + return this.getReferringComponents( + 'geo', {useDefault: false, enableAll: false, enableNone: false} + ).models[0] as GeoModel; } getMapType(): string { diff --git a/src/chart/pie/PieSeries.ts b/src/chart/pie/PieSeries.ts index 3845a8e470..e86b645610 100644 --- a/src/chart/pie/PieSeries.ts +++ b/src/chart/pie/PieSeries.ts @@ -39,6 +39,7 @@ import { DefaultEmphasisFocus } from '../../util/types'; import type SeriesData from '../../data/SeriesData'; +import { registerLayOutOnCoordSysUsage } from '../../core/CoordinateSystem'; interface PieItemStyleOption extends ItemStyleOption { // can be 10 @@ -100,7 +101,7 @@ export interface PieDataItemOption extends export interface PieSeriesOption extends Omit, ExtraStateOption>, 'labelLine'>, PieStateOption, - Omit, + CircleLayoutOptionMixin<{centerExtra: string | number}>, BoxLayoutOptionMixin, SeriesEncodeOptionMixin { @@ -108,8 +109,6 @@ export interface PieSeriesOption extends roseType?: 'radius' | 'area' - center?: string | number | (string | number)[] - clockwise?: boolean startAngle?: number endAngle?: number | 'auto' @@ -243,6 +242,7 @@ class PieSeriesModel extends SeriesModel { stillShowZeroSum: true, // cursor: null, + coordinateSystemUsage: 'box', left: 0, top: 0, @@ -265,7 +265,8 @@ class PieSeriesModel extends SeriesModel { // Works only position is 'outer' and alignTo is 'edge'. edgeDistance: '25%', // Works only position is 'outer' and alignTo is not 'edge'. - bleedMargin: 10, + // The default `bleedMargin` is auto determined according to view rect size. + // bleedMargin: 10, // Distance between text and label line. distanceToLabelLine: 5 // formatter: 标签文本格式器,同 tooltip.formatter,不支持异步回调 @@ -327,4 +328,13 @@ class PieSeriesModel extends SeriesModel { } +registerLayOutOnCoordSysUsage({ + fullType: PieSeriesModel.type, + getCoord2(model: PieSeriesModel) { + // Not able to validate `center` type here. + // But percentage center, such as '12%', is not allowed in this case. + return model.getShallow('center'); + } +}); + export default PieSeriesModel; diff --git a/src/chart/pie/PieView.ts b/src/chart/pie/PieView.ts index efead3b9cc..26b657c429 100644 --- a/src/chart/pie/PieView.ts +++ b/src/chart/pie/PieView.ts @@ -19,14 +19,13 @@ */ -import { extend, retrieve3 } from 'zrender/src/core/util'; +import { clone, extend, retrieve3 } from 'zrender/src/core/util'; import * as graphic from '../../util/graphic'; import { setStatesStylesFromModel, toggleHoverEmphasis } from '../../util/states'; import ChartView from '../../view/Chart'; import GlobalModel from '../../model/Global'; import ExtensionAPI from '../../core/ExtensionAPI'; import { Payload, ColorString, CircleLayoutOptionMixin, SeriesOption } from '../../util/types'; -import SeriesModel from '../../model/Series'; import SeriesData from '../../data/SeriesData'; import PieSeriesModel, {PieDataItemOption} from './PieSeries'; import labelLayout from './labelLayout'; @@ -35,7 +34,7 @@ import { setLabelStyle, getLabelStatesModels } from '../../label/labelStyle'; import { getSectorCornerRadius } from '../helper/sectorHelper'; import { saveOldStyle } from '../../animation/basicTransition'; import { getSeriesLayoutData } from './pieLayout'; -import { getCircleLayout } from '../../util/layout'; + /** * Piece of pie including Sector, Label, LabelLine @@ -264,12 +263,7 @@ class PieView extends ChartView { if (data.count() === 0 && seriesModel.get('showEmptyCircle')) { const layoutData = getSeriesLayoutData(seriesModel); const sector = new graphic.Sector({ - shape: extend( - getCircleLayout( - seriesModel as unknown as SeriesModel>, api - ), - layoutData - ) + shape: clone(layoutData) }); sector.useStyle(seriesModel.getModel('emptyCircleStyle').getItemStyle()); this._emptyCircleSector = sector; diff --git a/src/chart/pie/labelLayout.ts b/src/chart/pie/labelLayout.ts index 79dfb820a6..35edb358c3 100644 --- a/src/chart/pie/labelLayout.ts +++ b/src/chart/pie/labelLayout.ts @@ -384,7 +384,11 @@ export default function pieLabelLayout( const labelDistance = labelModel.get('distanceToLabelLine'); const labelAlignTo = labelModel.get('alignTo'); const edgeDistance = parsePercent(labelModel.get('edgeDistance'), viewWidth); - const bleedMargin = labelModel.get('bleedMargin'); + let bleedMargin = labelModel.get('bleedMargin'); + if (bleedMargin == null) { + // An arbitrary strategy for small viewRect - especial pie is layout in calendar or matrix coord sys. + bleedMargin = Math.min(viewWidth, viewHeight) > 200 ? 10 : 2; + } const labelLineModel = itemModel.getModel('labelLine'); let labelLineLen = labelLineModel.get('length'); diff --git a/src/chart/pie/pieLayout.ts b/src/chart/pie/pieLayout.ts index a94eb42fcf..937f129c76 100644 --- a/src/chart/pie/pieLayout.ts +++ b/src/chart/pie/pieLayout.ts @@ -18,14 +18,13 @@ */ import { linearMap } from '../../util/number'; -import * as layout from '../../util/layout'; import GlobalModel from '../../model/Global'; import ExtensionAPI from '../../core/ExtensionAPI'; import PieSeriesModel from './PieSeries'; import { normalizeArcAngles } from 'zrender/src/core/PathProxy'; import { makeInner } from '../../util/model'; -import SeriesModel from '../../model/Series'; -import { CircleLayoutOptionMixin, SeriesOption } from '../../util/types'; +import { getCircleLayout } from '../../util/layout'; + const PI2 = Math.PI * 2; const RADIAN = Math.PI / 180; @@ -39,12 +38,8 @@ export default function pieLayout( ecModel.eachSeriesByType(seriesType, function (seriesModel: PieSeriesModel) { const data = seriesModel.getData(); const valueDim = data.mapDimension('value'); - const viewRect = layout.getViewRect(seriesModel, api); - const { cx, cy, r, r0 } = layout.getCircleLayout( - seriesModel as unknown as SeriesModel>, - api - ); + const { cx, cy, r, r0, viewRect } = getCircleLayout(seriesModel, api); let startAngle = -seriesModel.get('startAngle') * RADIAN; let endAngle = seriesModel.get('endAngle'); @@ -85,6 +80,10 @@ export default function pieLayout( layoutData.startAngle = startAngle; layoutData.endAngle = endAngle; layoutData.clockwise = clockwise; + layoutData.cx = cx; + layoutData.cy = cy; + layoutData.r = r; + layoutData.r0 = r0; const angleRange = Math.abs(endAngle - startAngle); @@ -94,6 +93,7 @@ export default function pieLayout( let currentAngle = startAngle; + // Requird by `pieLabelLayout`. data.setLayout({ viewRect, r }); data.each(valueDim, function (value: number, idx: number) { @@ -227,4 +227,8 @@ export const getSeriesLayoutData = makeInner<{ startAngle: number endAngle: number clockwise: boolean + cx: number + cy: number + r: number + r0: number }, PieSeriesModel>(); diff --git a/src/chart/sankey/SankeySeries.ts b/src/chart/sankey/SankeySeries.ts index ebaf49e2f6..9409f56df6 100644 --- a/src/chart/sankey/SankeySeries.ts +++ b/src/chart/sankey/SankeySeries.ts @@ -152,6 +152,8 @@ class SankeySeriesModel extends SeriesModel { static readonly type = 'series.sankey'; readonly type = SankeySeriesModel.type; + static layoutMode = 'box' as const; + coordinateSystem: View; levelModels: Model[]; @@ -295,7 +297,9 @@ class SankeySeriesModel extends SeriesModel { // zlevel: 0, z: 2, - coordinateSystem: 'view', + // `coordinateSystem` can be declared as 'matrix', 'calendar', + // which provides box layout container. + coordinateSystemUsage: 'box', left: '5%', top: '5%', @@ -313,6 +317,7 @@ class SankeySeriesModel extends SeriesModel { // true | false | 'move' | 'scale', see module:component/helper/RoamController. roam: false, + roamTrigger: 'global', center: null, zoom: 1, diff --git a/src/chart/sankey/SankeyView.ts b/src/chart/sankey/SankeyView.ts index eb78454852..53c8a98c4b 100644 --- a/src/chart/sankey/SankeyView.ts +++ b/src/chart/sankey/SankeyView.ts @@ -130,7 +130,7 @@ class SankeyView extends ChartView { render(seriesModel: SankeySeriesModel, ecModel: GlobalModel, api: ExtensionAPI) { const sankeyView = this; const graph = seriesModel.getGraph(); - const group = this._mainGroup; + const mainGroup = this._mainGroup; const layoutInfo = seriesModel.layoutInfo; // view width const width = layoutInfo.width; @@ -142,10 +142,13 @@ class SankeyView extends ChartView { this._model = seriesModel; - group.removeAll(); + mainGroup.removeAll(); + + mainGroup.x = layoutInfo.x; + mainGroup.y = layoutInfo.y; this._updateViewCoordSys(seriesModel, api); - roamHelper.updateController(seriesModel, api, group, this._controller, this._controllerHost); + roamHelper.updateController(seriesModel, api, mainGroup, this._controller, this._controllerHost, null); // generate a bezire Curve for each edge graph.eachEdge(function (edge) { @@ -253,7 +256,7 @@ class SankeyView extends ChartView { return style; }); - group.add(curve); + mainGroup.add(curve); edgeData.setItemGraphicEl(edge.dataIndex, curve); @@ -309,7 +312,7 @@ class SankeyView extends ChartView { setStatesStylesFromModel(rect, itemModel); - group.add(rect); + mainGroup.add(rect); nodeData.setItemGraphicEl(node.dataIndex, rect); @@ -353,8 +356,8 @@ class SankeyView extends ChartView { }); if (!this._data && seriesModel.isAnimationEnabled()) { - group.setClipPath(createGridClipShape(group.getBoundingRect(), seriesModel, function () { - group.removeClipPath(); + mainGroup.setClipPath(createGridClipShape(mainGroup.getBoundingRect(), seriesModel, function () { + mainGroup.removeClipPath(); })); } @@ -371,16 +374,20 @@ class SankeyView extends ChartView { const width = layoutInfo.width; const height = layoutInfo.height; - const viewCoordSys = seriesModel.coordinateSystem = new View(); + const viewCoordSys = seriesModel.coordinateSystem = new View(null, {api, ecModel: seriesModel.ecModel}); viewCoordSys.zoomLimit = seriesModel.get('scaleLimit'); viewCoordSys.setBoundingRect(0, 0, width, height); - viewCoordSys.setCenter(seriesModel.get('center'), api); + viewCoordSys.setCenter(seriesModel.get('center')); viewCoordSys.setZoom(seriesModel.get('zoom')); - this._mainGroup.x = layoutInfo.x; - this._mainGroup.y = layoutInfo.y; + this._controllerHost.target.attr({ + x: viewCoordSys.x, + y: viewCoordSys.y, + scaleX: viewCoordSys.scaleX, + scaleY: viewCoordSys.scaleY + }); } } diff --git a/src/chart/sankey/install.ts b/src/chart/sankey/install.ts index b3da093bfb..5314aaedde 100644 --- a/src/chart/sankey/install.ts +++ b/src/chart/sankey/install.ts @@ -25,7 +25,7 @@ import sankeyLayout from './sankeyLayout'; import sankeyVisual from './sankeyVisual'; import { Payload } from '../../util/types'; import GlobalModel from '../../model/Global'; -import { updateCenterAndZoom, RoamPayload } from '../../action/roamHelper'; +import { updateCenterAndZoomInAction, RoamPayload } from '../../component/helper/roamHelper'; import type ExtensionAPI from '../../core/ExtensionAPI'; interface SankeyDragNodePayload extends Payload { @@ -64,12 +64,10 @@ export function install(registers: EChartsExtensionInstallRegisters) { mainType: 'series', subType: 'sankey', query: payload }, function (seriesModel: SankeySeriesModel) { const coordSys = seriesModel.coordinateSystem; - const res = updateCenterAndZoom(coordSys, payload, undefined, api); + const res = updateCenterAndZoomInAction(coordSys, payload, seriesModel.get('scaleLimit')); - seriesModel.setCenter - && seriesModel.setCenter(res.center); - seriesModel.setZoom - && seriesModel.setZoom(res.zoom); + seriesModel.setCenter(res.center); + seriesModel.setZoom(res.zoom); }); }); } diff --git a/src/chart/sankey/sankeyLayout.ts b/src/chart/sankey/sankeyLayout.ts index 410ca7dfbc..8e9c8d22bb 100644 --- a/src/chart/sankey/sankeyLayout.ts +++ b/src/chart/sankey/sankeyLayout.ts @@ -17,7 +17,6 @@ * under the License. */ -import * as layout from '../../util/layout'; import * as zrUtil from 'zrender/src/core/util'; import {groupData} from '../../util/model'; import ExtensionAPI from '../../core/ExtensionAPI'; @@ -25,6 +24,7 @@ import SankeySeriesModel, { SankeySeriesOption, SankeyNodeItemOption } from './S import { GraphNode, GraphEdge } from '../../data/Graph'; import { LayoutOrient } from '../../util/types'; import GlobalModel from '../../model/Global'; +import { createBoxLayoutReference, getLayoutRect } from '../../util/layout'; export default function sankeyLayout(ecModel: GlobalModel, api: ExtensionAPI) { @@ -33,7 +33,8 @@ export default function sankeyLayout(ecModel: GlobalModel, api: ExtensionAPI) { const nodeWidth = seriesModel.get('nodeWidth'); const nodeGap = seriesModel.get('nodeGap'); - const layoutInfo = layout.getViewRect(seriesModel, api); + const refContainer = createBoxLayoutReference(seriesModel, api).refContainer; + const layoutInfo = getLayoutRect(seriesModel.getBoxLayoutParams(), refContainer); seriesModel.layoutInfo = layoutInfo; diff --git a/src/chart/scatter/ScatterSeries.ts b/src/chart/scatter/ScatterSeries.ts index f95b8d6769..9dfe667735 100644 --- a/src/chart/scatter/ScatterSeries.ts +++ b/src/chart/scatter/ScatterSeries.ts @@ -83,7 +83,7 @@ class ScatterSeriesModel extends SeriesModel { static readonly type = 'series.scatter'; type = ScatterSeriesModel.type; - static readonly dependencies = ['grid', 'polar', 'geo', 'singleAxis', 'calendar']; + static readonly dependencies = ['grid', 'polar', 'geo', 'singleAxis', 'calendar', 'matrix']; hasSymbolVisual = true; diff --git a/src/chart/tree/TreeSeries.ts b/src/chart/tree/TreeSeries.ts index 3a8a8da883..c00ce26c9b 100644 --- a/src/chart/tree/TreeSeries.ts +++ b/src/chart/tree/TreeSeries.ts @@ -250,7 +250,10 @@ class TreeSeriesModel extends SeriesModel { static defaultOption: TreeSeriesOption = { // zlevel: 0, z: 2, - coordinateSystem: 'view', + + // `coordinateSystem` can be declared as 'matrix', 'calendar', + // which provides box layout container. + coordinateSystemUsage: 'box', // the position of the whole view left: '12%', @@ -268,6 +271,7 @@ class TreeSeriesModel extends SeriesModel { // true | false | 'move' | 'scale', see module:component/helper/RoamController. roam: false, + roamTrigger: 'global', // Symbol size scale ratio in roam nodeScaleRatio: 0.4, diff --git a/src/chart/tree/TreeView.ts b/src/chart/tree/TreeView.ts index 86110326fe..f7482687b6 100644 --- a/src/chart/tree/TreeView.ts +++ b/src/chart/tree/TreeView.ts @@ -35,7 +35,7 @@ import ExtensionAPI from '../../core/ExtensionAPI'; import { TreeNode } from '../../data/Tree'; import SeriesData from '../../data/SeriesData'; import { setStatesStylesFromModel, setStatesFlag, setDefaultStateProxy, HOVER_STATE_BLUR } from '../../util/states'; -import { AnimationOption, ECElement } from '../../util/types'; +import { AnimationOption, ECElement, NullUndefined } from '../../util/types'; import tokens from '../../visual/tokens'; type TreeSymbol = SymbolClz & { @@ -173,7 +173,7 @@ class TreeView extends ChartView { } this._updateViewCoordSys(seriesModel, api); - this._updateController(seriesModel, ecModel, api); + this._updateController(seriesModel, null, ecModel, api); const oldData = this._data; @@ -252,15 +252,16 @@ class TreeView extends ChartView { max[1] = oldMax ? oldMax[1] : max[1] + 1; } - const viewCoordSys = seriesModel.coordinateSystem = new View(); + const viewCoordSys = seriesModel.coordinateSystem = new View(null, {api, ecModel: seriesModel.ecModel}); viewCoordSys.zoomLimit = seriesModel.get('scaleLimit'); viewCoordSys.setBoundingRect(min[0], min[1], max[0] - min[0], max[1] - min[1]); - viewCoordSys.setCenter(seriesModel.get('center'), api); + viewCoordSys.setCenter(seriesModel.get('center')); viewCoordSys.setZoom(seriesModel.get('zoom')); - // Here we use viewCoordSys just for computing the 'position' and 'scale' of the group + // Here we use viewCoordSys just for computing the 'position' and 'scale' of the group, + // and 'treeRoam' action. this.group.attr({ x: viewCoordSys.x, y: viewCoordSys.y, @@ -274,6 +275,7 @@ class TreeView extends ChartView { _updateController( seriesModel: TreeSeriesModel, + clipRect: graphic.BoundingRect | NullUndefined, ecModel: GlobalModel, api: ExtensionAPI ) { @@ -282,7 +284,8 @@ class TreeView extends ChartView { api, this.group, this._controller, - this._controllerHost + this._controllerHost, + clipRect ); this._controller diff --git a/src/chart/tree/treeAction.ts b/src/chart/tree/treeAction.ts index 358779919c..df737a4480 100644 --- a/src/chart/tree/treeAction.ts +++ b/src/chart/tree/treeAction.ts @@ -17,7 +17,7 @@ * under the License. */ -import {updateCenterAndZoom, RoamPayload} from '../../action/roamHelper'; +import {updateCenterAndZoomInAction, RoamPayload} from '../../component/helper/roamHelper'; import { Payload } from '../../util/types'; import TreeSeriesModel from './TreeSeries'; import GlobalModel from '../../model/Global'; @@ -57,13 +57,10 @@ export function installTreeAction(registers: EChartsExtensionInstallRegisters) { mainType: 'series', subType: 'tree', query: payload }, function (seriesModel: TreeSeriesModel) { const coordSys = seriesModel.coordinateSystem; - const res = updateCenterAndZoom(coordSys, payload, undefined, api); + const res = updateCenterAndZoomInAction(coordSys, payload, seriesModel.get('scaleLimit')); - seriesModel.setCenter - && seriesModel.setCenter(res.center); - - seriesModel.setZoom - && seriesModel.setZoom(res.zoom); + seriesModel.setCenter(res.center); + seriesModel.setZoom(res.zoom); }); }); diff --git a/src/chart/tree/treeLayout.ts b/src/chart/tree/treeLayout.ts index 0f1fe87df5..aa7ffaba36 100644 --- a/src/chart/tree/treeLayout.ts +++ b/src/chart/tree/treeLayout.ts @@ -32,7 +32,7 @@ import { import GlobalModel from '../../model/Global'; import ExtensionAPI from '../../core/ExtensionAPI'; import TreeSeriesModel from './TreeSeries'; -import { getViewRect } from '../../util/layout'; +import { createBoxLayoutReference, getLayoutRect } from '../../util/layout'; export default function treeLayout(ecModel: GlobalModel, api: ExtensionAPI) { ecModel.eachSeriesByType('tree', function (seriesModel: TreeSeriesModel) { @@ -41,7 +41,8 @@ export default function treeLayout(ecModel: GlobalModel, api: ExtensionAPI) { } function commonLayout(seriesModel: TreeSeriesModel, api: ExtensionAPI) { - const layoutInfo = getViewRect(seriesModel, api); + const refContainer = createBoxLayoutReference(seriesModel, api).refContainer; + const layoutInfo = getLayoutRect(seriesModel.getBoxLayoutParams(), refContainer); seriesModel.layoutInfo = layoutInfo; const layout = seriesModel.get('layout'); let width = 0; diff --git a/src/chart/treemap/Breadcrumb.ts b/src/chart/treemap/Breadcrumb.ts index 0875c4a2b3..7c7124ac97 100644 --- a/src/chart/treemap/Breadcrumb.ts +++ b/src/chart/treemap/Breadcrumb.ts @@ -25,7 +25,7 @@ import TreemapSeriesModel, { TreemapSeriesNodeItemOption, TreemapSeriesOption } import ExtensionAPI from '../../core/ExtensionAPI'; import { TreeNode } from '../../data/Tree'; import { curry, defaults } from 'zrender/src/core/util'; -import { ZRElementEvent, BoxLayoutOptionMixin, ECElement } from '../../util/types'; +import { ZRElementEvent, ECElement } from '../../util/types'; import Element from 'zrender/src/Element'; import Model from '../../model/Model'; import { convertOptionIdName } from '../../util/model'; @@ -41,11 +41,6 @@ interface OnSelectCallback { } interface LayoutParam { - pos: BoxLayoutOptionMixin - box: { - width: number, - height: number - } emptyItemWidth: number totalWidth: number renderList: { @@ -87,29 +82,29 @@ class Breadcrumb { const textStyleModel = normalStyleModel.getModel('textStyle'); const emphasisTextStyleModel = emphasisModel.getModel(['itemStyle', 'textStyle']); + const refContainer = layout.createBoxLayoutReference(seriesModel, api).refContainer; + const boxLayoutParams = { + left: model.get('left'), + right: model.get('right'), + top: model.get('top'), + bottom: model.get('bottom') + }; const layoutParam: LayoutParam = { - pos: { - left: model.get('left'), - right: model.get('right'), - top: model.get('top'), - bottom: model.get('bottom') - }, - box: { - width: api.getWidth(), - height: api.getHeight() - }, emptyItemWidth: model.get('emptyItemWidth'), totalWidth: 0, renderList: [] }; - + const availableSize = layout.getLayoutRect( + boxLayoutParams, + refContainer, + ); this._prepare(targetNode, layoutParam, textStyleModel); this._renderContent( - seriesModel, layoutParam, normalStyleModel, + seriesModel, layoutParam, availableSize, normalStyleModel, emphasisModel, textStyleModel, emphasisTextStyleModel, onSelect ); - layout.positionElement(thisGroup, layoutParam.pos, layoutParam.box); + layout.positionElement(thisGroup, boxLayoutParams, refContainer); } /** @@ -139,6 +134,7 @@ class Breadcrumb { _renderContent( seriesModel: TreemapSeriesModel, layoutParam: LayoutParam, + availableSize: {width: number, height: number}, normalStyleModel: BreadcrumbItemStyleModel, emphasisModel: BreadcrumbEmphasisItemStyleModel, textStyleModel: BreadcrumbTextStyleModel, @@ -149,7 +145,6 @@ class Breadcrumb { let lastX = 0; const emptyItemWidth = layoutParam.emptyItemWidth; const height = seriesModel.get(['breadcrumb', 'height']); - const availableSize = layout.getAvailableSize(layoutParam.pos, layoutParam.box); let totalWidth = layoutParam.totalWidth; const renderList = layoutParam.renderList; const emphasisItemStyle = emphasisModel.getModel('itemStyle').getItemStyle(); diff --git a/src/chart/treemap/TreemapSeries.ts b/src/chart/treemap/TreemapSeries.ts index 0c99655c55..9776d40684 100644 --- a/src/chart/treemap/TreemapSeries.ts +++ b/src/chart/treemap/TreemapSeries.ts @@ -245,10 +245,16 @@ class TreemapSeriesModel extends SeriesModel { // Disable progressive rendering progressive: 0, // size: ['80%', '80%'], // deprecated, compatible with ec2. + + // `coordinateSystem` can be declared as 'matrix', 'calendar', + // which provides box layout container. + coordinateSystemUsage: 'box', + left: tokens.size.l, top: tokens.size.xxxl, right: tokens.size.l, bottom: tokens.size.xxxl, + sort: true, clipWindow: 'origin', @@ -266,6 +272,7 @@ class TreemapSeriesModel extends SeriesModel { }, roam: true, + roamTrigger: 'global', nodeClick: 'zoomToNode', animation: true, animationDurationUpdate: 900, diff --git a/src/chart/treemap/TreemapView.ts b/src/chart/treemap/TreemapView.ts index 75aaa128bb..36a5918cd2 100644 --- a/src/chart/treemap/TreemapView.ts +++ b/src/chart/treemap/TreemapView.ts @@ -31,10 +31,7 @@ import DataDiffer from '../../data/DataDiffer'; import * as helper from '../helper/treeHelper'; import Breadcrumb from './Breadcrumb'; import type { RoamControllerHost } from '../../component/helper/roamHelper'; -// eslint-disable-next-line no-duplicate-imports -import type { RoamEventParams } from '../../component/helper/RoamController'; -// eslint-disable-next-line no-duplicate-imports -import RoamController from '../../component/helper/RoamController'; +import RoamController, { RoamEventParams } from '../../component/helper/RoamController'; import BoundingRect, { RectLike } from 'zrender/src/core/BoundingRect'; import * as matrix from 'zrender/src/core/matrix'; import * as animationUtil from '../../util/animation'; @@ -178,7 +175,6 @@ class TreemapView extends ChartView { api: ExtensionAPI, payload: TreemapZoomToNodePayload | TreemapRenderPayload | TreemapMovePayload | TreemapRootToNodePayload ) { - const models = ecModel.findComponents({ mainType: 'series', subType: 'treemap', query: payload }); @@ -493,20 +489,34 @@ class TreemapView extends ChartView { controllerHost = this._controllerHost; } + const seriesModel = this.seriesModel; + // Init controller. if (!controller) { controller = this._controller = new RoamController(api.getZr()); - controller.enable(this.seriesModel.get('roam')); - controllerHost.zoomLimit = this.seriesModel.get('scaleLimit'); - controllerHost.zoom = this.seriesModel.get('zoom'); controller.on('pan', bind(this._onPan, this)); controller.on('zoom', bind(this._onZoom, this)); } - const rect = new BoundingRect(0, 0, api.getWidth(), api.getHeight()); - controller.setPointerChecker(function (e, x, y) { - return rect.contain(x, y); + controller.enable(seriesModel.get('roam'), { + api, + zInfo: {component: seriesModel}, + triggerInfo: { + roamTrigger: seriesModel.get('roamTrigger'), + isInSelf: (e, x, y) => { + const containerGroup = this._containerGroup; + return containerGroup + // Currently only x, y exist in tranform. + ? containerGroup.getBoundingRect().contain(x - containerGroup.x, y - containerGroup.y) + : false; + }, + // isInClip: (e, x, y) => { + // } + }, }); + + controllerHost.zoomLimit = seriesModel.get('scaleLimit'); + controllerHost.zoom = seriesModel.get('zoom'); } private _clearController() { @@ -708,13 +718,12 @@ class TreemapView extends ChartView { } /** - * @public - * @param {number} x Global coord x. - * @param {number} y Global coord y. - * @return {Object} info If not found, return undefined; - * @return {number} info.node Target node. - * @return {number} info.offsetX x refer to target node. - * @return {number} info.offsetY y refer to target node. + * @param x Global coord x. + * @param y Global coord y. + * @return info If not found, return undefined; + * @return info.node Target node. + * @return info.offsetX x refer to target node. + * @return info.offsetY y refer to target node. */ findTarget(x: number, y: number): FoundTargetInfo { let targetInfo; @@ -749,9 +758,6 @@ class TreemapView extends ChartView { } } -/** - * @inner - */ function createStorage(): RenderElementStorage | LastCfgStorage { return { nodeGroup: [], @@ -761,7 +767,6 @@ function createStorage(): RenderElementStorage | LastCfgStorage { } /** - * @inner * @return Return undefined means do not travel further. */ function renderNode( diff --git a/src/chart/treemap/treemapLayout.ts b/src/chart/treemap/treemapLayout.ts index 62872f37b2..4b870b6d1c 100644 --- a/src/chart/treemap/treemapLayout.ts +++ b/src/chart/treemap/treemapLayout.ts @@ -90,26 +90,19 @@ export default { ) { // Layout result in each node: // {x, y, width, height, area, borderWidth} - const ecWidth = api.getWidth(); - const ecHeight = api.getHeight(); const seriesOption = seriesModel.option; - const layoutInfo = layout.getLayoutRect( - seriesModel.getBoxLayoutParams(), - { - width: api.getWidth(), - height: api.getHeight() - } - ); + const refContainer = layout.createBoxLayoutReference(seriesModel, api).refContainer; + const layoutInfo = layout.getLayoutRect(seriesModel.getBoxLayoutParams(), refContainer); const size = seriesOption.size || []; // Compatible with ec2. const containerWidth = parsePercent( retrieveValue(layoutInfo.width, size[0]), - ecWidth + refContainer.width ); const containerHeight = parsePercent( retrieveValue(layoutInfo.height, size[1]), - ecHeight + refContainer.height ); // Fetch payload info. @@ -184,12 +177,12 @@ export default { seriesModel.setLayoutInfo(layoutInfo); - // FIXME - // 现在没有clip功能,暂时取ec高宽。 + // FIXME: narrow down pruning boungding rect. + // Currently ec width/height is used becuases clip is not supported. prunning( treeRoot, // Transform to base element coordinate system. - new BoundingRect(-layoutInfo.x, -layoutInfo.y, ecWidth, ecHeight), + new BoundingRect(-layoutInfo.x, -layoutInfo.y, api.getWidth(), api.getHeight()), viewAbovePath, viewRoot, 0 diff --git a/src/component/axis/axisBreakHelper.ts b/src/component/axis/axisBreakHelper.ts index db8b94a56e..cbf0aba4a7 100644 --- a/src/component/axis/axisBreakHelper.ts +++ b/src/component/axis/axisBreakHelper.ts @@ -27,9 +27,9 @@ import type { PathProps } from 'zrender/src/graphic/Path'; import type SingleAxisView from './SingleAxisView'; import type { AxisBuilderCfg } from './AxisBuilder'; import type { BaseAxisBreakPayload } from './axisAction'; -import type { ComponentModel } from '../../echarts.all'; import type { AxisBaseOption } from '../../coord/axisCommonTypes'; import type { AxisBreakOptionIdentifierInAxis, NullUndefined } from '../../util/types'; +import type ComponentModel from '../../model/Component'; /** * @file The fasade of axis break view and mode. diff --git a/src/component/axisPointer/modelHelper.ts b/src/component/axisPointer/modelHelper.ts index 2b03d20104..a0635b9c68 100644 --- a/src/component/axisPointer/modelHelper.ts +++ b/src/component/axisPointer/modelHelper.ts @@ -293,6 +293,7 @@ function collectSeriesInfo(result: CollectionResult, ecModel: GlobalModel) { const seriesTooltipTrigger = seriesModel.get(['tooltip', 'trigger'], true); const seriesTooltipShow = seriesModel.get(['tooltip', 'show'], true); if (!coordSys + || !coordSys.model // PENDING: radar do not have a model. || seriesTooltipTrigger === 'none' || seriesTooltipTrigger === false || seriesTooltipTrigger === 'item' diff --git a/src/component/calendar/CalendarView.ts b/src/component/calendar/CalendarView.ts index 01a84b2180..0755a4431f 100644 --- a/src/component/calendar/CalendarView.ts +++ b/src/component/calendar/CalendarView.ts @@ -97,7 +97,7 @@ class CalendarView extends ComponentView { i = coordSys.getNextNDay(i, 1).time ) { - const point = coordSys.dataToRect([i], false).tl; + const point = coordSys.dataToCalendarLayout([i], false).tl; // every rect const rect = new graphic.Rect({ @@ -158,7 +158,7 @@ class CalendarView extends ComponentView { function addPoints(date: OptionDataValueDate) { self._firstDayOfMonth.push(coordSys.getDateInfo(date)); - self._firstDayPoints.push(coordSys.dataToRect([date], false).tl); + self._firstDayPoints.push(coordSys.dataToCalendarLayout([date], false).tl); const points = self._getLinePointsOfOneWeek(calendarModel, date, orient); @@ -214,7 +214,7 @@ class CalendarView extends ComponentView { for (let i = 0; i < 7; i++) { const tmpD = coordSys.getNextNDay(parsedDate.time, i); - const point = coordSys.dataToRect([tmpD.time], false); + const point = coordSys.dataToCalendarLayout([tmpD.time], false); points[2 * tmpD.day] = point.tl; points[2 * tmpD.day + 1] = point[orient === 'horizontal' ? 'bl' : 'tr']; @@ -542,7 +542,7 @@ class CalendarView extends ComponentView { for (let i = 0; i < 7; i++) { const tmpD = coordSys.getNextNDay(start, i); - const point = coordSys.dataToRect([tmpD.time], false).center; + const point = coordSys.dataToCalendarLayout([tmpD.time], false).center; let day = i; day = Math.abs((i + firstDayOfWeek) % 7); const weekText = new graphic.Text({ diff --git a/src/component/dataZoom/SliderZoomView.ts b/src/component/dataZoom/SliderZoomView.ts index 7069065601..8f28cf995a 100644 --- a/src/component/dataZoom/SliderZoomView.ts +++ b/src/component/dataZoom/SliderZoomView.ts @@ -224,18 +224,19 @@ class SliderZoomView extends DataZoomView { const showMoveHandle = dataZoomModel.get('brushSelect'); const moveHandleSize = showMoveHandle ? DEFAULT_MOVE_HANDLE_SIZE : 0; + const refContainer = layout.createBoxLayoutReference(dataZoomModel, api).refContainer; + // If some of x/y/width/height are not specified, // auto-adapt according to target grid. const coordRect = this._findCoordRect(); - const ecSize = { width: api.getWidth(), height: api.getHeight() }; const edgeGap = dataZoomModel.get('defaultLocationEdgeGap', true) || 0; // Default align by coordinate system rect. const positionInfo = this._orient === HORIZONTAL ? { // Why using 'right', because right should be used in vertical, // and it is better to be consistent for dealing with position param merge. - right: ecSize.width - coordRect.x - coordRect.width, - top: (ecSize.height - DEFAULT_FILLER_SIZE - edgeGap - moveHandleSize), + right: refContainer.width - coordRect.x - coordRect.width, + top: refContainer.height - DEFAULT_FILLER_SIZE - edgeGap - moveHandleSize, width: coordRect.width, height: DEFAULT_FILLER_SIZE } @@ -259,7 +260,7 @@ class SliderZoomView extends DataZoomView { const layoutRect = layout.getLayoutRect( layoutParams, - ecSize + refContainer ); this._location = { x: layoutRect.x, y: layoutRect.y }; diff --git a/src/component/dataZoom/roams.ts b/src/component/dataZoom/roams.ts index d33e1e287a..38800b8c35 100644 --- a/src/component/dataZoom/roams.ts +++ b/src/component/dataZoom/roams.ts @@ -23,10 +23,10 @@ // pan or zoom, only dispatch one action for those data zoom // components. -import RoamController, { RoamType } from '../../component/helper/RoamController'; +import RoamController, { RoamOption } from '../../component/helper/RoamController'; import * as throttleUtil from '../../util/throttle'; import { makeInner } from '../../util/model'; -import { Dictionary, ZRElementEvent } from '../../util/types'; +import { Dictionary, RoamOptionMixin, ZRElementEvent } from '../../util/types'; import ExtensionAPI from '../../core/ExtensionAPI'; import InsideZoomModel from './InsideZoomModel'; import { each, curry, Curry1, HashMap, createHashMap } from 'zrender/src/core/util'; @@ -179,8 +179,15 @@ function containsPoint( /** * Merge roamController settings when multiple dataZooms share one roamController. */ -function mergeControllerParams(dataZoomInfoMap: HashMap<{ model: InsideZoomModel }>) { - let controlType: RoamType; +function mergeControllerParams( + dataZoomInfoMap: HashMap<{ model: InsideZoomModel }>, + coordSysRecord: CoordSysRecord, + api: ExtensionAPI +): { + controlType: RoamOptionMixin['roam'] + opt: RoamOption +} { + let controlType: RoamOptionMixin['roam']; // DO NOT use reserved word (true, false, undefined) as key literally. Even if encapsulated // as string, it is probably revert to reserved word by compress tool. See #7411. const prefix = 'type_'; @@ -218,7 +225,15 @@ function mergeControllerParams(dataZoomInfoMap: HashMap<{ model: InsideZoomModel zoomOnMouseWheel: true, moveOnMouseMove: true, moveOnMouseWheel: true, - preventDefaultMouseMove: !!preventDefaultMouseMove + preventDefaultMouseMove: !!preventDefaultMouseMove, + api, + zInfo: { + component: coordSysRecord.model, + }, + triggerInfo: { + roamTrigger: null, + isInSelf: coordSysRecord.containsPoint + }, } }; } @@ -280,11 +295,9 @@ export function installDataZoomRoamProcessor(registers: EChartsExtensionInstallR return; } - const controllerParams = mergeControllerParams(dataZoomInfoMap); + const controllerParams = mergeControllerParams(dataZoomInfoMap, coordSysRecord, api); controller.enable(controllerParams.controlType, controllerParams.opt); - controller.setPointerChecker(coordSysRecord.containsPoint); - throttleUtil.createOrUpdate( coordSysRecord, 'dispatchAction', diff --git a/src/component/geo/install.ts b/src/component/geo/install.ts index 3bf9e3e2b9..e77bb60905 100644 --- a/src/component/geo/install.ts +++ b/src/component/geo/install.ts @@ -23,7 +23,7 @@ import geoCreator from '../../coord/geo/geoCreator'; import { ActionInfo } from '../../util/types'; import { each } from 'zrender/src/core/util'; import GlobalModel from '../../model/Global'; -import { updateCenterAndZoom, RoamPayload } from '../../action/roamHelper'; +import { updateCenterAndZoomInAction, RoamPayload } from '../../component/helper/roamHelper'; import MapSeries from '../../chart/map/MapSeries'; import GeoView from './GeoView'; import geoSourceManager from '../../coord/geo/geoSourceManager'; @@ -116,8 +116,22 @@ export function install(registers: EChartsExtensionInstallRegisters) { event: 'geoRoam', update: 'updateTransform' }, function (payload: RoamPayload, ecModel: GlobalModel, api: ExtensionAPI) { - const componentType = payload.componentType || 'series'; - + let componentType = payload.componentType; + if (!componentType) { // backward compat, but `payload.componentType` is deprecated. + if (payload.geoId != null) { + componentType = 'geo'; + } + else if (payload.seriesId != null) { + componentType = 'series'; + } + } + if (!componentType) { + componentType = 'series'; + } + + // FIXME: payload.geoId/payload.seriesId should be required, but historically + // it is not mandatory, causing that all of the geo or series can be queried below, + // which is not reasonable. ecModel.eachComponent( { mainType: componentType, query: payload }, function (componentModel: GeoModel | MapSeries) { @@ -126,8 +140,8 @@ export function install(registers: EChartsExtensionInstallRegisters) { return; } - const res = updateCenterAndZoom( - geo, payload, (componentModel as GeoModel).get('scaleLimit'), api + const res = updateCenterAndZoomInAction( + geo, payload, (componentModel as GeoModel).get('scaleLimit') ); componentModel.setCenter diff --git a/src/component/helper/MapDraw.ts b/src/component/helper/MapDraw.ts index 563f6de8ef..83eaf2efd0 100644 --- a/src/component/helper/MapDraw.ts +++ b/src/component/helper/MapDraw.ts @@ -20,7 +20,6 @@ import * as zrUtil from 'zrender/src/core/util'; import RoamController from './RoamController'; import * as roamHelper from '../../component/helper/roamHelper'; -import {onIrrelevantElement} from '../../component/helper/cursorHelper'; import * as graphic from '../../util/graphic'; import { toggleHoverEmphasis, @@ -33,7 +32,9 @@ import ExtensionAPI from '../../core/ExtensionAPI'; import GeoModel, { GeoCommonOptionMixin, GeoItemStyleOption, RegionOption } from '../../coord/geo/GeoModel'; import MapSeries, { MapDataItemOption } from '../../chart/map/MapSeries'; import GlobalModel from '../../model/Global'; -import { Payload, ECElement, LineStyleOption, InnerFocus, DisplayState } from '../../util/types'; +import { + Payload, ECElement, LineStyleOption, InnerFocus, DisplayState, NullUndefined, RoamOptionMixin +} from '../../util/types'; import GeoView from '../geo/GeoView'; import MapView from '../../chart/map/MapView'; import Geo from '../../coord/geo/Geo'; @@ -142,6 +143,8 @@ class MapDraw { */ private _mouseDownFlag: boolean; + private _transformGroup: graphic.Group; + private _regionsGroup: RegionsGroup; private _regionsGroupByName: zrUtil.HashMap; @@ -158,14 +161,15 @@ class MapDraw { constructor(api: ExtensionAPI) { - const group = new graphic.Group(); + const group = this.group = new graphic.Group(); + const transformGroup = this._transformGroup = new graphic.Group(); + group.add(transformGroup); this.uid = getUID('ec_map_draw'); this._controller = new RoamController(api.getZr()); - this._controllerHost = { target: group }; - this.group = group; + this._controllerHost = { target: transformGroup }; - group.add(this._regionsGroup = new graphic.Group() as RegionsGroup); - group.add(this._svgGroup = new graphic.Group()); + transformGroup.add(this._regionsGroup = new graphic.Group() as RegionsGroup); + transformGroup.add(this._svgGroup = new graphic.Group()); } draw( @@ -190,7 +194,7 @@ class MapDraw { const geo = mapOrGeoModel.coordinateSystem; const regionsGroup = this._regionsGroup; - const group = this.group; + const transformGroup = this._transformGroup; const transformInfo = geo.getTransformInfo(); const transformInfoRaw = transformInfo.raw; @@ -199,15 +203,25 @@ class MapDraw { // No animation when first draw or in action const isFirstDraw = !regionsGroup.childAt(0) || payload; + const clip = (mapOrGeoModel as Model).getShallow('clip', true); + let clipRect: graphic.BoundingRect | NullUndefined; + if (clip) { + clipRect = geo.getViewRect().clone(); + this.group.setClipPath(new graphic.Rect({shape: clipRect.clone()})); + } + else { + this.group.removeClipPath(); + } + if (isFirstDraw) { - group.x = transformInfoRoam.x; - group.y = transformInfoRoam.y; - group.scaleX = transformInfoRoam.scaleX; - group.scaleY = transformInfoRoam.scaleY; - group.dirty(); + transformGroup.x = transformInfoRoam.x; + transformGroup.y = transformInfoRoam.y; + transformGroup.scaleX = transformInfoRoam.scaleX; + transformGroup.scaleY = transformInfoRoam.scaleY; + transformGroup.dirty(); } else { - graphic.updateProps(group, transformInfoRoam, mapOrGeoModel); + graphic.updateProps(transformGroup, transformInfoRoam, mapOrGeoModel); } const isVisualEncodedByVisualMap = data @@ -231,7 +245,7 @@ class MapDraw { this._buildSVG(viewBuildCtx); } - this._updateController(mapOrGeoModel, ecModel, api); + this._updateController(mapOrGeoModel, clipRect, ecModel, api); this._updateMapSelectHandler(mapOrGeoModel, regionsGroup, api, fromView); } @@ -557,19 +571,32 @@ class MapDraw { } private _updateController( - this: MapDraw, mapOrGeoModel: GeoModel | MapSeries, ecModel: GlobalModel, api: ExtensionAPI + this: MapDraw, + mapOrGeoModel: GeoModel | MapSeries, + clipRect: graphic.BoundingRect | NullUndefined, + ecModel: GlobalModel, + api: ExtensionAPI ): void { const geo = mapOrGeoModel.coordinateSystem; const controller = this._controller; const controllerHost = this._controllerHost; - // @ts-ignore FIXME:TS - controllerHost.zoomLimit = mapOrGeoModel.get('scaleLimit'); + controllerHost.zoomLimit = (mapOrGeoModel as Model).get('scaleLimit'); controllerHost.zoom = geo.getZoom(); // roamType is will be set default true if it is null - // @ts-ignore FIXME:TS - controller.enable(mapOrGeoModel.get('roam') || false); + controller.enable( + (mapOrGeoModel as Model).get('roam') || false, + { + api, + zInfo: {component: mapOrGeoModel}, + triggerInfo: { + roamTrigger: (mapOrGeoModel as Model).get('roamTrigger'), + isInSelf: (e, x, y) => geo.containPoint([x, y]), + isInClip: (e, x, y) => !clipRect || clipRect.contain(x, y), + }, + } + ); const mainType = mapOrGeoModel.mainType; function makeActionBase(): Payload { @@ -611,11 +638,6 @@ class MapDraw { })); }, this); - - controller.setPointerChecker(function (e, x, y) { - return geo.containPoint([x, y]) - && !onIrrelevantElement(e, api, mapOrGeoModel); - }); } /** diff --git a/src/component/helper/RoamController.ts b/src/component/helper/RoamController.ts index e316edb32a..4c057412e5 100644 --- a/src/component/helper/RoamController.ts +++ b/src/component/helper/RoamController.ts @@ -21,14 +21,31 @@ import Eventful from 'zrender/src/core/Eventful'; import * as eventTool from 'zrender/src/core/event'; import * as interactionMutex from './interactionMutex'; import { ZRenderType } from 'zrender/src/zrender'; -import { ZRElementEvent, RoamOptionMixin } from '../../util/types'; -import { Bind3, isString, bind, defaults, clone } from 'zrender/src/core/util'; - -// Can be null/undefined or true/false -// or 'pan/move' or 'zoom'/'scale' -export type RoamType = RoamOptionMixin['roam']; +import { ZRElementEvent, RoamOptionMixin, NullUndefined } from '../../util/types'; +import { Bind3, isString, bind, defaults, extend, retrieve2 } from 'zrender/src/core/util'; +import { makeInner } from '../../util/model'; +import { retrieveZInfo } from '../../util/graphic'; +import type Component from '../../model/Component'; +import ExtensionAPI from '../../core/ExtensionAPI'; +import { onIrrelevantElement } from './cursorHelper'; +import Displayable from 'zrender/src/graphic/Displayable'; + + +export interface RoamOption { + zInfo: { + // mandatory to provide z and zlevel and pointer checker criteria. + component: Component + z2?: number + } + triggerInfo: { + roamTrigger: RoamOptionMixin['roamTrigger'] | NullUndefined + // At present in all scenarios it can be supported. + isInSelf: RoamPointerChecker + // Required if clipping is supported + isInClip?: RoamPointerChecker + } + api: ExtensionAPI -interface RoamOption { zoomOnMouseWheel?: boolean | 'ctrl' | 'shift' | 'alt' moveOnMouseMove?: boolean | 'ctrl' | 'shift' | 'alt' moveOnMouseWheel?: boolean | 'ctrl' | 'shift' | 'alt' @@ -37,6 +54,14 @@ interface RoamOption { */ preventDefaultMouseMove?: boolean } +type RoamSetting = Omit, 'zInfo'> & { + zInfoParsed: { + component: Component + z: number + zlevel: number + z2: number + } +}; type RoamEventType = keyof RoamEventParams; @@ -72,13 +97,26 @@ export type RoamEventDefinition = { [key in keyof RoamEventParams]: (params: RoamEventParams[key]) => void | undefined }; +type RoamPointerChecker = (e: ZRElementEvent, x: number, y: number) => boolean; + +/** + * An manager of zoom and pan(darg) hehavior. + * But it is not responsible for updating the view, since view updates vary and can + * not be handled in a uniform way. + * + * Note: regarding view updates: + * - Transformabe views typically use `coord/View` (e.g., geo and series.graph roaming). + * Some commonly used view update logic has been organized into `roamHelper.ts`. + * - Non-transformable views handle updates themselves, possibly involving re-layout, + * (e.g., treemap). + * - Some scenarios do not require transformation (e.g., dataZoom roaming for cartesian, + * brush component). + */ class RoamController extends Eventful { - pointerChecker: (e: ZRElementEvent, x: number, y: number) => boolean; - private _zr: ZRenderType; - private _opt: Required; + private _opt: Required; private _dragging: boolean; @@ -88,7 +126,11 @@ class RoamController extends Eventful { private _y: number; - readonly enable: (this: this, controlType?: RoamType, opt?: RoamOption) => void; + private _controlType: RoamOptionMixin['roam']; + + private _enabled: boolean; + + readonly enable: (this: this, controlType: RoamOptionMixin['roam'], opt: RoamOption) => void; readonly disable: () => void; @@ -106,44 +148,65 @@ class RoamController extends Eventful { const pinchHandler = bind(this._pinchHandler, this); /** - * Notice: only enable needed types. For example, if 'zoom' - * is not needed, 'zoom' should not be enabled, otherwise - * default mousewheel behaviour (scroll page) will be disabled. + * Notice: + * - only enable needed types. For example, if 'zoom' + * is not needed, 'zoom' should not be enabled, otherwise + * default mousewheel behaviour (scroll page) will be disabled. + * - This method is idempotent. */ - this.enable = function (controlType, opt) { - - // Disable previous first - this.disable(); - - this._opt = defaults(clone(opt) || {}, { + this.enable = function (controlType, rawOpt) { + const zInfo = rawOpt.zInfo; + const {z, zlevel} = retrieveZInfo(zInfo.component); + const zInfoParsed = { + component: zInfo.component, + z, + zlevel, + // By default roam controller is the lowest z2 comparing to other elememts in a component. + z2: retrieve2(zInfo.z2, -Infinity), + }; + const triggerInfo = extend({}, rawOpt.triggerInfo); + + this._opt = defaults(extend({}, rawOpt), { zoomOnMouseWheel: true, moveOnMouseMove: true, // By default, wheel do not trigger move. moveOnMouseWheel: false, - preventDefaultMouseMove: true + preventDefaultMouseMove: true, + zInfoParsed, + triggerInfo, }); if (controlType == null) { controlType = true; } - if (controlType === true || (controlType === 'move' || controlType === 'pan')) { - zr.on('mousedown', mousedownHandler); - zr.on('mousemove', mousemoveHandler); - zr.on('mouseup', mouseupHandler); - } - if (controlType === true || (controlType === 'scale' || controlType === 'zoom')) { - zr.on('mousewheel', mousewheelHandler); - zr.on('pinch', pinchHandler); + // A handy optimization for repeatedly calling `enable` during roaming. + // Assert `disable` is only affected by `controlType`. + if (!this._enabled || this._controlType !== controlType) { + this._enabled = true; + + // Disable previous first + this.disable(); + + if (controlType === true || (controlType === 'move' || controlType === 'pan')) { + addRoamZrListener(zr, 'mousedown', mousedownHandler, zInfoParsed); + addRoamZrListener(zr, 'mousemove', mousemoveHandler, zInfoParsed); + addRoamZrListener(zr, 'mouseup', mouseupHandler, zInfoParsed); + } + if (controlType === true || (controlType === 'scale' || controlType === 'zoom')) { + addRoamZrListener(zr, 'mousewheel', mousewheelHandler, zInfoParsed); + addRoamZrListener(zr, 'pinch', pinchHandler, zInfoParsed); + } } }; this.disable = function () { - zr.off('mousedown', mousedownHandler); - zr.off('mousemove', mousemoveHandler); - zr.off('mouseup', mouseupHandler); - zr.off('mousewheel', mousewheelHandler); - zr.off('pinch', pinchHandler); + this._enabled = false; + removeRoamZrListener(zr, 'mousedown', mousedownHandler); + removeRoamZrListener(zr, 'mousemove', mousemoveHandler); + removeRoamZrListener(zr, 'mouseup', mouseupHandler); + removeRoamZrListener(zr, 'mousewheel', mousewheelHandler); + removeRoamZrListener(zr, 'pinch', pinchHandler); }; } @@ -155,8 +218,50 @@ class RoamController extends Eventful { return this._pinching; } - setPointerChecker(pointerChecker: RoamController['pointerChecker']) { - this.pointerChecker = pointerChecker; + _checkPointer(e: ZRElementEvent, x: number, y: number): boolean { + const opt = this._opt; + + const zInfoParsed = opt.zInfoParsed; + if (onIrrelevantElement(e, opt.api, zInfoParsed.component)) { + return false; + }; + + const triggerInfo = opt.triggerInfo; + const roamTrigger = triggerInfo.roamTrigger; + let inArea = false; + if (roamTrigger === 'global') { + inArea = true; + } + if (!inArea) { + inArea = triggerInfo.isInSelf(e, x, y); + } + if (inArea && triggerInfo.isInClip && !triggerInfo.isInClip(e, x, y)) { + inArea = false; + } + return inArea; + } + + private _decideCursorStyle( + e: ZRElementEvent, + x: number, + y: number, + forReverse: boolean, + ): string | NullUndefined { + // If this cursor style decision is not strictly consistent with zrender, + // it's fine - zr will set the cursor on the next mousemove. + + // This `grab` cursor style should take the lowest precedence. If the hovring element already + // have a cursor, zrender will set it to be non-'default' before entering this handler. + // (note, e.target is never silent, e.topTarget can be silent be irrelevant.) + const target = e.target; + if (!target && this._checkPointer(e, x, y)) { + // To indicate users that this area is draggable, otherwise users probably cannot kwown + // that when hovering out of the shape but still inside the bounding rect. + return 'grab'; + } + if (forReverse) { + return target && (target as Displayable).cursor || 'default'; + } } dispose() { @@ -164,7 +269,9 @@ class RoamController extends Eventful { } private _mousedownHandler(e: ZRElementEvent) { - if (eventTool.isMiddleOrRightButtonOnMouseUpDown(e)) { + if (eventTool.isMiddleOrRightButtonOnMouseUpDown(e) + || eventConsumed(e) + ) { return; } @@ -180,9 +287,9 @@ class RoamController extends Eventful { const x = e.offsetX; const y = e.offsetY; - // Only check on mosedown, but not mousemove. + // To determine dragging start, only by checking on mosedown, but not mousemove. // Mouse can be out of target when mouse moving. - if (this.pointerChecker && this.pointerChecker(e, x, y)) { + if (this._checkPointer(e, x, y)) { this._x = x; this._y = y; this._dragging = true; @@ -190,10 +297,10 @@ class RoamController extends Eventful { } private _mousemoveHandler(e: ZRElementEvent) { - if (!this._dragging - || !isAvailableBehavior('moveOnMouseMove', e, this._opt) - || e.gestureEvent === 'pinch' - || interactionMutex.isTaken(this._zr, 'globalPan') + const zr = this._zr; + if (e.gestureEvent === 'pinch' + || interactionMutex.isTaken(zr, 'globalPan') + || eventConsumed(e) ) { return; } @@ -201,6 +308,18 @@ class RoamController extends Eventful { const x = e.offsetX; const y = e.offsetY; + if (!this._dragging + || !isAvailableBehavior('moveOnMouseMove', e, this._opt) + ) { + const cursorStyle = this._decideCursorStyle(e, x, y, false); + if (cursorStyle) { + zr.setCursorStyle(cursorStyle); + } + return; + } + + zr.setCursorStyle('grabbing'); + const oldX = this._x; const oldY = this._y; @@ -210,7 +329,10 @@ class RoamController extends Eventful { this._x = x; this._y = y; - this._opt.preventDefaultMouseMove && eventTool.stop(e.event); + if (this._opt.preventDefaultMouseMove) { + eventTool.stop(e.event); + } + (e as RoamControllerZREventExtend).__ecRoamConsumed = true; trigger(this, 'pan', 'moveOnMouseMove', e, { dx: dx, dy: dy, oldX: oldX, oldY: oldY, newX: x, newY: y, isAvailableBehavior: null @@ -218,12 +340,25 @@ class RoamController extends Eventful { } private _mouseupHandler(e: ZRElementEvent) { + if (eventConsumed(e)) { + return; + } + const zr = this._zr; if (!eventTool.isMiddleOrRightButtonOnMouseUpDown(e)) { this._dragging = false; + + const cursorStyle = this._decideCursorStyle(e, e.offsetX, e.offsetY, true); + if (cursorStyle) { + zr.setCursorStyle(cursorStyle); + } } } private _mousewheelHandler(e: ZRElementEvent) { + if (eventConsumed(e)) { + return; + } + const shouldZoom = isAvailableBehavior('zoomOnMouseWheel', e, this._opt); const shouldMove = isAvailableBehavior('moveOnMouseWheel', e, this._opt); const wheelDelta = e.wheelDelta; @@ -252,7 +387,7 @@ class RoamController extends Eventful { // wheelDelta of mouse wheel is bigger than touch pad. const factor = absWheelDeltaDelta > 3 ? 1.4 : absWheelDeltaDelta > 1 ? 1.2 : 1.1; const scale = wheelDelta > 0 ? factor : 1 / factor; - checkPointerAndTrigger(this, 'zoom', 'zoomOnMouseWheel', e, { + this._checkTriggerMoveZoom(this, 'zoom', 'zoomOnMouseWheel', e, { scale: scale, originX: originX, originY: originY, isAvailableBehavior: null }); } @@ -262,43 +397,155 @@ class RoamController extends Eventful { const absDelta = Math.abs(wheelDelta); // wheelDelta of mouse wheel is bigger than touch pad. const scrollDelta = (wheelDelta > 0 ? 1 : -1) * (absDelta > 3 ? 0.4 : absDelta > 1 ? 0.15 : 0.05); - checkPointerAndTrigger(this, 'scrollMove', 'moveOnMouseWheel', e, { + this._checkTriggerMoveZoom(this, 'scrollMove', 'moveOnMouseWheel', e, { scrollDelta: scrollDelta, originX: originX, originY: originY, isAvailableBehavior: null }); } } private _pinchHandler(e: ZRElementEvent) { - if (interactionMutex.isTaken(this._zr, 'globalPan')) { + if (interactionMutex.isTaken(this._zr, 'globalPan') + || eventConsumed(e) + ) { return; } const scale = e.pinchScale > 1 ? 1.1 : 1 / 1.1; - checkPointerAndTrigger(this, 'zoom', null, e, { + this._checkTriggerMoveZoom(this, 'zoom', null, e, { scale: scale, originX: e.pinchX, originY: e.pinchY, isAvailableBehavior: null }); } + + private _checkTriggerMoveZoom( + controller: RoamController, + eventName: T, + behaviorToCheck: RoamBehavior, + e: ZRElementEvent, + contollerEvent: RoamEventParams[T] + ) { + if (controller._checkPointer(e, contollerEvent.originX, contollerEvent.originY)) { + // When mouse is out of roamController rect, + // default befavoius should not be be disabled, otherwise + // page sliding is disabled, contrary to expectation. + eventTool.stop(e.event); + (e as RoamControllerZREventExtend).__ecRoamConsumed = true; + + trigger(controller, eventName, behaviorToCheck, e, contollerEvent); + } + } } +type RoamControllerZREventExtend = ZRElementEvent & { + __ecRoamConsumed: boolean +}; -function checkPointerAndTrigger( - controller: RoamController, - eventName: T, - behaviorToCheck: RoamBehavior, - e: ZRElementEvent, - contollerEvent: RoamEventParams[T] -) { - if (controller.pointerChecker - && controller.pointerChecker(e, contollerEvent.originX, contollerEvent.originY) - ) { - // When mouse is out of roamController rect, - // default befavoius should not be be disabled, otherwise - // page sliding is disabled, contrary to expectation. - eventTool.stop(e.event); +function eventConsumed(e: ZRElementEvent): boolean { + return (e as RoamControllerZREventExtend).__ecRoamConsumed; +} + + +type RoamControllerZREventListener = (e: ZRElementEvent) => void; +type RoamControllerZREventType = 'mousedown' | 'mousemove' | 'mouseup' | 'mousewheel' | 'pinch'; +type RoamControllerListenerItem = {listener: RoamControllerZREventListener} & Pick; + +const innerZrStore = makeInner<{ + roam: Partial> + uniform: Partial> +}, ZRenderType>(); - trigger(controller, eventName, behaviorToCheck, e, contollerEvent); +function ensureZrStore(zr: ZRenderType) { + const store = innerZrStore(zr); + store.roam = store.roam || {}; + store.uniform = store.uniform || {}; + return store; +} + +/** + * Listeners are sorted by z2/z/zlevel in descending order. + * This decides the precedence between different roam controllers if they are overlapped. + * + * [MEMO]: It's not easy to perfectly reconcile the conflicts caused by overlap. + * - Consider cases: + * - Multiple roam controllers overlapped. + * - Usually only the topmost can trigger roam. + * - Roam controllers overlap with other zr elements: + * - zr elements are relevant or irrelevent to the host of the roam controller. e.g., axis split line + * or series elements is relevant to a cartesian and should trigger roam. + * - zr elements is above or below the roam controller host, which affects the precedence of interaction. + * - zr elements may not silent only for triggering tooltip by hovering, which is available to roam; + * or may not silent for click, where roam is not preferable. + * - Approach - `addRoamZrListener+pointerChecker+onIrrelevantElement` (currently used): + * - Resolve the precedence between different roam controllers + * - But cannot prevent the handling on other zr elements that under the roam controller in z-order. + * - Approach - "use an invisible zr elements to receive the zr events to trigger roam": + * - More complicated in impl. + * - May cause bad cases where zr event cannot be receive due to other non-silient zr elements covering it. + */ +function addRoamZrListener( + zr: ZRenderType, + eventType: RoamControllerZREventType, + listener: RoamControllerZREventListener, + zInfoParsed: RoamSetting['zInfoParsed'] +): void { + const store = ensureZrStore(zr); + const roam = store.roam; + const listenerList = roam[eventType] = roam[eventType] || []; + let idx = 0; + for (; idx < listenerList.length; idx++) { + const currZInfo = listenerList[idx].zInfoParsed; + if ( + ((currZInfo.zlevel - zInfoParsed.zlevel) + || (currZInfo.z - zInfoParsed.z) + || (currZInfo.z2 - zInfoParsed.z2) + // If all equals, the latter added one has a higher precedence. + ) <= 0 + ) { + break; + } + } + listenerList.splice(idx, 0, {listener, zInfoParsed}); + ensureUniformListener(zr, eventType); +} + +function removeRoamZrListener( + zr: ZRenderType, eventType: RoamControllerZREventType, listener: RoamControllerZREventListener +): void { + const store = ensureZrStore(zr); + const listenerList = store.roam[eventType] || []; + for (let idx = 0; idx < listenerList.length; idx++) { + if (listenerList[idx].listener === listener) { + listenerList.splice(idx, 1); + if (!listenerList.length) { + removeUniformListener(zr, eventType); + } + return; + } } } +function ensureUniformListener(zr: ZRenderType, eventType: RoamControllerZREventType): void { + const store = ensureZrStore(zr); + if (!store.uniform[eventType]) { + zr.on(eventType, store.uniform[eventType] = function (event) { + const listenerList = store.roam[eventType]; + if (listenerList) { + for (let i = 0; i < listenerList.length; i++) { + listenerList[i].listener(event); + } + } + }); + } +} + +function removeUniformListener(zr: ZRenderType, eventType: RoamControllerZREventType): void { + const store = ensureZrStore(zr); + const uniform = store.uniform; + if (uniform[eventType]) { + zr.off(eventType, uniform[eventType]); + uniform[eventType] = null; + } +} + + function trigger( controller: RoamController, eventName: T, diff --git a/src/component/helper/cursorHelper.ts b/src/component/helper/cursorHelper.ts index dd23501dbc..e6810e4df4 100644 --- a/src/component/helper/cursorHelper.ts +++ b/src/component/helper/cursorHelper.ts @@ -20,23 +20,56 @@ import { ElementEvent } from 'zrender/src/Element'; import ExtensionAPI from '../../core/ExtensionAPI'; -import SeriesModel from '../../model/Series'; -import { CoordinateSystem } from '../../coord/CoordinateSystem'; +import { CoordinateSystemHostModel } from '../../coord/CoordinateSystem'; +import type Component from '../../model/Component'; +import { retrieveZInfo } from '../../util/graphic'; const IRRELEVANT_EXCLUDES = {'axisPointer': 1, 'tooltip': 1, 'brush': 1}; /** - * Avoid that: mouse click on a elements that is over geo or graph, - * but roam is triggered. + * Used on roam/brush triggering determination. + * This is to avoid that: mouse clicking on an elements that is over geo or graph, + * but roam is triggered unexpectedly. */ export function onIrrelevantElement( - e: ElementEvent, api: ExtensionAPI, targetCoordSysModel: CoordinateSystem['model'] + e: ElementEvent, + api: ExtensionAPI, + targetComponent: Component ): boolean { - const model = api.getComponentByElement(e.topTarget); - // If model is axisModel, it works only if it is injected with coordinateSystem. - const coordSys = model && (model as SeriesModel).coordinateSystem; - return model - && model !== targetCoordSysModel - && !IRRELEVANT_EXCLUDES.hasOwnProperty(model.mainType) - && (coordSys && coordSys.model !== targetCoordSysModel); + const eventElComponent = api.getComponentByElement(e.topTarget); + + if (!eventElComponent + || eventElComponent === targetComponent + || IRRELEVANT_EXCLUDES.hasOwnProperty(eventElComponent.mainType) + ) { + return false; + } + + // At present the `true` return is conservative. That is, the caller, such as a RoamController, + // is more likely to get a `false` return and start a roam behavior, becuase even if the + // `model.coordinateSystem` does not exist, the `e.topTarget` may be also a relevant element, + // such as axis split line/area or series elements, where roam should be available. Otherwise, + // if a dataZoom-served cartesian is full of series elements, the dataZoom-roaming can hardly + // be triggered. + const eventElCoordSys = (eventElComponent as CoordinateSystemHostModel).coordinateSystem; + // If eventElComponent is axisModel, it works only if it is injected with coordinateSystem. + if (!eventElCoordSys || eventElCoordSys.model === targetComponent) { + return false; + } + + // e.g., if a cartesian is covered by a graph, the graph has a higher presedence in roam. + // A potential bad case is that RoamController does not prevent the cartesian from handling zr + // event, such as click and hovering, but it's fine so far. + // Aslo be conservative, if equals, return false. + const eventElCmptZInfo = retrieveZInfo(eventElComponent); + const targetCmptZInfo = retrieveZInfo(targetComponent); + if (( + (eventElCmptZInfo.zlevel - targetCmptZInfo.zlevel) + || (eventElCmptZInfo.z - targetCmptZInfo.z) + ) <= 0 + ) { + return false; + } + + return true; } diff --git a/src/component/helper/interactionMutex.ts b/src/component/helper/interactionMutex.ts index 149e6d7f77..85695e749c 100644 --- a/src/component/helper/interactionMutex.ts +++ b/src/component/helper/interactionMutex.ts @@ -17,19 +17,30 @@ * under the License. */ -// @ts-nocheck +import { ZRenderType } from 'zrender/src/zrender'; import * as echarts from '../../core/echarts'; import { noop } from 'zrender/src/core/util'; +import { makeInner } from '../../util/model'; -const ATTR = '\0_ec_interaction_mutex'; +type InteractionMutexResource = { + globalPan: string +}; +const inner = makeInner(); -export function take(zr, resourceKey, userKey) { - const store = getStore(zr); - store[resourceKey] = userKey; +export function take( + zr: ZRenderType, + resourceKey: keyof InteractionMutexResource, + userKey: InteractionMutexResource[keyof InteractionMutexResource] +) { + inner(zr)[resourceKey] = userKey; } -export function release(zr, resourceKey, userKey) { - const store = getStore(zr); +export function release( + zr: ZRenderType, + resourceKey: keyof InteractionMutexResource, + userKey: InteractionMutexResource[keyof InteractionMutexResource] +) { + const store = inner(zr); const uKey = store[resourceKey]; if (uKey === userKey) { @@ -37,12 +48,11 @@ export function release(zr, resourceKey, userKey) { } } -export function isTaken(zr, resourceKey) { - return !!getStore(zr)[resourceKey]; -} - -function getStore(zr) { - return zr[ATTR] || (zr[ATTR] = {}); +export function isTaken( + zr: ZRenderType, + resourceKey: keyof InteractionMutexResource, +) { + return !!inner(zr)[resourceKey]; } /** diff --git a/src/component/helper/listComponent.ts b/src/component/helper/listComponent.ts index 39d063bc48..9faabbb619 100644 --- a/src/component/helper/listComponent.ts +++ b/src/component/helper/listComponent.ts @@ -17,57 +17,27 @@ * under the License. */ -// @ts-nocheck - -import { - getLayoutRect, - box as layoutBox, - positionElement -} from '../../util/layout'; import * as formatUtil from '../../util/format'; import * as graphic from '../../util/graphic'; +import { RectLike } from 'zrender/src/core/BoundingRect'; +import { ItemStyleOption, ZRColor } from '../../util/types'; +import Model from '../../model/Model'; -/** - * Layout list like component. - * It will box layout each items in group of component and then position the whole group in the viewport - * @param {module:zrender/group/Group} group - * @param {module:echarts/model/Component} componentModel - * @param {module:echarts/ExtensionAPI} - */ -export function layout(group, componentModel, api) { - const boxLayoutParams = componentModel.getBoxLayoutParams(); - const padding = componentModel.get('padding'); - const viewportSize = {width: api.getWidth(), height: api.getHeight()}; - - const rect = getLayoutRect( - boxLayoutParams, - viewportSize, - padding - ); - layoutBox( - componentModel.get('orient'), - group, - componentModel.get('itemGap'), - rect.width, - rect.height - ); - - positionElement( - group, - boxLayoutParams, - viewportSize, - padding - ); -} +interface BackgroundRelatedOption { + backgroundColor?: ZRColor + borderRadius?: number | number[] + padding?: number | number[] + itemStyle?: Omit +}; -export function makeBackground(rect, componentModel) { +export function makeBackground(rect: RectLike, componentModel: Model): graphic.Rect { const padding = formatUtil.normalizeCssArray( componentModel.get('padding') ); const style = componentModel.getItemStyle(['color', 'opacity']); style.fill = componentModel.get('backgroundColor'); - rect = new graphic.Rect({ + const bgRect = new graphic.Rect({ shape: { x: rect.x - padding[3], y: rect.y - padding[0], @@ -84,5 +54,5 @@ export function makeBackground(rect, componentModel) { // and background rect when setting like `left: 0`, `top: 0`. // graphic.subPixelOptimizeRect(rect); - return rect; + return bgRect; } diff --git a/src/component/helper/roamHelper.ts b/src/component/helper/roamHelper.ts index 8f87a9e001..f19a4e5d30 100644 --- a/src/component/helper/roamHelper.ts +++ b/src/component/helper/roamHelper.ts @@ -18,23 +18,34 @@ */ import Element from 'zrender/src/Element'; -import { SeriesModel } from '../../echarts.all'; +import type SeriesModel from '../../model/Series'; import ExtensionAPI from '../../core/ExtensionAPI'; import Group from 'zrender/src/graphic/Group'; import RoamController from './RoamController'; import type { SeriesOption } from '../../export/option'; import type View from '../../coord/View'; -import type { RoamOptionMixin } from '../../util/types'; -import { onIrrelevantElement } from './cursorHelper'; +import type { NullUndefined, RoamOptionMixin, Payload } from '../../util/types'; +import { BoundingRect } from '../../util/graphic'; +import type Geo from '../../coord/geo/Geo'; +import Transformable from 'zrender/src/core/Transformable'; + +export interface ZoomLimit { + max?: number; + min?: number; +} export interface RoamControllerHost { + // Its transform x/y/scaleX/scaleY will be modified when roaming. target: Element; zoom?: number; - zoomLimit?: {min?: number, max?: number}; + zoomLimit?: ZoomLimit; } /** - * For geo and graph. + * [CAVEAT] `updateViewOnPan` and `updateViewOnZoom` modifies the group transform directly, + * but the 'center' and 'zoom' in echarts option and 'View' coordinate system are not updated yet, + * which must be performed later in 'xxxRoam' action by calling `updateCenterAndZoom`. + * @see {updateCenterAndZoomInAction} */ export function updateViewOnPan(controllerHost: RoamControllerHost, dx: number, dy: number) { const target = controllerHost.target; @@ -43,53 +54,52 @@ export function updateViewOnPan(controllerHost: RoamControllerHost, dx: number, target.dirty(); } -/** - * For geo and graph. - */ export function updateViewOnZoom(controllerHost: RoamControllerHost, zoomDelta: number, zoomX: number, zoomY: number) { const target = controllerHost.target; const zoomLimit = controllerHost.zoomLimit; let newZoom = controllerHost.zoom = controllerHost.zoom || 1; newZoom *= zoomDelta; - if (zoomLimit) { - const zoomMin = zoomLimit.min || 0; - const zoomMax = zoomLimit.max || Infinity; - newZoom = Math.max( - Math.min(zoomMax, newZoom), - zoomMin - ); - } + + newZoom = clampByZoomLimit(newZoom, zoomLimit); + const zoomScale = newZoom / controllerHost.zoom; controllerHost.zoom = newZoom; - // Keep the mouse center when scaling - target.x -= (zoomX - target.x) * (zoomScale - 1); - target.y -= (zoomY - target.y) * (zoomScale - 1); - target.scaleX *= zoomScale; - target.scaleY *= zoomScale; - + zoomTransformableByOrigin(target, zoomX, zoomY, zoomScale); target.dirty(); } +/** + * A abstraction for some similar impl in roaming. + */ export function updateController( seriesModel: SeriesModel, api: ExtensionAPI, - group: Group, + pointerCheckerEl: Group, controller: RoamController, controllerHost: RoamControllerHost, + clipRect: BoundingRect | NullUndefined, ) { - controller.setPointerChecker(function (e, x, y) { - const rect = group.getBoundingRect(); - rect.applyTransform(group.transform); - return rect.contain(x, y) - && !onIrrelevantElement(e, api, seriesModel); + const tmpRect = new BoundingRect(0, 0, 0, 0); + controller.enable(seriesModel.get('roam'), { + api, + zInfo: {component: seriesModel}, + triggerInfo: { + roamTrigger: seriesModel.get('roamTrigger'), + isInSelf: function (e, x, y) { + tmpRect.copy(pointerCheckerEl.getBoundingRect()); + tmpRect.applyTransform(pointerCheckerEl.getComputedTransform()); + return tmpRect.contain(x, y); + }, + isInClip: function (e, x, y) { + return !clipRect || clipRect.contain(x, y); + } + } }); - - controller.enable(seriesModel.get('roam')); controllerHost.zoomLimit = seriesModel.get('scaleLimit'); const coordinate = seriesModel.coordinateSystem; controllerHost.zoom = coordinate ? (coordinate as View).getZoom() : 1; - const type = seriesModel.type + 'Roam'; + const type = seriesModel.subType + 'Roam'; controller .off('pan') @@ -104,6 +114,11 @@ export function updateController( }); }) .on('zoom', (e) => { + /** + * FIXME: should do nothing except `api.dispatchAction` here, the other logic + * should be performed in the action handler and `updateTransform`; otherwise, + * they are inconsistent if user triggers this action explicitly. + */ updateViewOnZoom(controllerHost, e.scale, e.originX, e.originY); api.dispatchAction({ seriesId: seriesModel.id, @@ -116,3 +131,85 @@ export function updateController( api.updateLabelLayout(); }); } + +export interface RoamPayload extends Payload { + dx: number + dy: number + // This is a delta zoom, not an absolute zoom. + zoom: number + originX: number + originY: number +} + +function getCenterCoord(view: View, point: number[]) { + // Use projected coord as center because it's linear. + return (view as Geo).pointToProjected + ? (view as Geo).pointToProjected(point) + : view.pointToData(point); +} + +/** + * Should be called only in action handler. + * @see {updateViewOnPan|updateViewOnZoom} + */ +export function updateCenterAndZoomInAction( + view: View, + payload: RoamPayload, + zoomLimit?: ZoomLimit +) { + const previousZoom = view.getZoom(); + const center = view.getCenter(); + let deltaZoom = payload.zoom; + + const point = (view as Geo).projectedToPoint + ? (view as Geo).projectedToPoint(center) + : view.dataToPoint(center); + + if (payload.dx != null && payload.dy != null) { + point[0] -= payload.dx; + point[1] -= payload.dy; + + view.setCenter(getCenterCoord(view, point)); + } + if (deltaZoom != null) { + deltaZoom = clampByZoomLimit(previousZoom * deltaZoom, zoomLimit) / previousZoom; + + zoomTransformableByOrigin(view, payload.originX, payload.originY, deltaZoom); + view.updateTransform(); + + // [NOTICE] Tricky: `getCetnerCoord` uses `this.invTransform` modified by the `updateTransform` above. + view.setCenter(getCenterCoord(view, point)); + view.setZoom(deltaZoom * previousZoom); + } + + return { + center: view.getCenter(), + zoom: view.getZoom() + }; +} + + +function zoomTransformableByOrigin( + target: Transformable, + originX: number, + originY: number, + deltaZoom: number, // positive number, 1 means no zooming. +): void { + // Keep the mouse center when scaling. + target.x -= (originX - target.x) * (deltaZoom - 1); + target.y -= (originY - target.y) * (deltaZoom - 1); + target.scaleX *= deltaZoom; + target.scaleY *= deltaZoom; +} + +export function clampByZoomLimit(zoom: number, zoomLimit: ZoomLimit | NullUndefined): number { + if (zoomLimit) { + const zoomMin = zoomLimit.min || 0; + const zoomMax = zoomLimit.max || Infinity; + zoom = Math.max( + Math.min(zoomMax, zoom), + zoomMin + ); + } + return zoom; +} diff --git a/src/component/helper/thumbnailBridge.ts b/src/component/helper/thumbnailBridge.ts new file mode 100644 index 0000000000..610e1464d5 --- /dev/null +++ b/src/component/helper/thumbnailBridge.ts @@ -0,0 +1,91 @@ +/* +* 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 type Group from 'zrender/src/graphic/Group'; +import type ComponentModel from '../../model/Component'; +import { makeInner } from '../../util/model'; +import { NullUndefined, RoamOptionMixin } from '../../util/types'; +import ExtensionAPI from '../../core/ExtensionAPI'; +import BoundingRect from 'zrender/src/core/BoundingRect'; +import type View from '../../coord/View'; +/** + * @caveat Do not import other `src/component/thumbnail/*` files. + * This file should be decoupled from them for sake of the size consideration. + */ + +/** + * FIXME: This is a temporary implmentation. May need refactor to decouple + * the direct call from series.graph to thumbnail. + */ + +const inner = makeInner<{ + bridge: ThumbnailBridge +}, ComponentModel>(); + +export function getThumbnailBridge( + model: ComponentModel +): ThumbnailBridge | NullUndefined { + if (model) { + return inner(model).bridge; + } +} + +export function injectThumbnailBridge( + model: ComponentModel, + thumbnailBridge: ThumbnailBridge | NullUndefined +): void { + if (model) { + inner(model).bridge = thumbnailBridge; + } +} + +/** + * This is the transform from the rendered target elements (e.g., the graph elements, the geo map elements) + * in their local unit (e.g., geo in longitude-latitude) to screen coord. + * Typically it is `View['transform']` if `coord/View` is used. + */ +export type ThumbnailTargetTransformRawToViewport = View['transform']; + +export interface ThumbnailBridge { + /** + * Must be called in `ChartView['render']`/`ComponentView['render']` + */ + reset(api: ExtensionAPI): void; + + /** + * Trigger content rendering. + * Some series, such graph force layout, will update elements asynchronously, + * therefore rendering and register are separated. + */ + renderContent(opt: { + roamType: RoamOptionMixin['roam']; + // `viewportRect`: + // - If clip is suppored, this should be the clip rect. + // - Otherwise (can pass NullUndefined), this should be the canvas rect. + viewportRect: BoundingRect; + group: Group; + targetTrans: ThumbnailTargetTransformRawToViewport; + api: ExtensionAPI; + }): void; + + updateWindow( + targetTrans: ThumbnailTargetTransformRawToViewport, + api: ExtensionAPI + ): void; +}; diff --git a/src/component/legend/LegendView.ts b/src/component/legend/LegendView.ts index f32e3c2b6d..2dc195fcae 100644 --- a/src/component/legend/LegendView.ts +++ b/src/component/legend/LegendView.ts @@ -43,7 +43,8 @@ import { CommonTooltipOption, ColorString, SeriesOption, - SymbolOptionMixin + SymbolOptionMixin, + ItemStyleOption } from '../../util/types'; import Model from '../../model/Model'; import {LineStyleProps} from '../../model/mixin/lineStyle'; @@ -134,11 +135,11 @@ class LegendView extends ComponentView { this.renderInner(itemAlign, legendModel, ecModel, api, selector, orient, selectorPosition); // Perform layout. + const refContainer = layoutUtil.createBoxLayoutReference(legendModel, api).refContainer; const positionInfo = legendModel.getBoxLayoutParams(); - const viewportSize = {width: api.getWidth(), height: api.getHeight()}; const padding = legendModel.get('padding'); - const maxSize = layoutUtil.getLayoutRect(positionInfo, viewportSize, padding); + const maxSize = layoutUtil.getLayoutRect(positionInfo, refContainer, padding); const mainRect = this.layoutInner(legendModel, itemAlign, maxSize, isFirstRender, selector, selectorPosition); @@ -148,7 +149,7 @@ class LegendView extends ComponentView { width: mainRect.width, height: mainRect.height }, positionInfo), - viewportSize, + refContainer, padding ); this.group.x = layoutRect.x - mainRect.x; @@ -157,7 +158,11 @@ class LegendView extends ComponentView { // Render background after group is layout. this.group.add( - this._backgroundEl = makeBackground(mainRect, legendModel) + this._backgroundEl = makeBackground( + mainRect, + // FXIME: most itemStyle options does not work in background because inherit is not handled yet. + legendModel as Model & {itemStyle: ItemStyleOption}> + ) ); } diff --git a/src/component/matrix.ts b/src/component/matrix.ts new file mode 100644 index 0000000000..2c2752d3ef --- /dev/null +++ b/src/component/matrix.ts @@ -0,0 +1,23 @@ +/* +* 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 { use } from '../extension'; +import { install } from './matrix/install'; + +use(install); diff --git a/src/component/matrix/MatrixView.ts b/src/component/matrix/MatrixView.ts new file mode 100644 index 0000000000..32b10d1bb9 --- /dev/null +++ b/src/component/matrix/MatrixView.ts @@ -0,0 +1,400 @@ +/* +* 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 MatrixModel, { MatrixBaseCellOption, MatrixCellStyleOption, MatrixOption } from '../../coord/matrix/MatrixModel'; +import ComponentView from '../../view/Component'; +import { MatrixCellLayoutInfo, MatrixDim, MatrixXYLocator } from '../../coord/matrix/MatrixDim'; +import Model from '../../model/Model'; +import { NullUndefined } from '../../util/types'; +import BoundingRect, { RectLike } from 'zrender/src/core/BoundingRect'; +import * as vectorUtil from 'zrender/src/core/vector'; +import { RectShape } from 'zrender/src/graphic/shape/Rect'; +import { ItemStyleProps } from '../../model/mixin/itemStyle'; +import { LineStyleProps } from '../../model/mixin/lineStyle'; +import { LineShape } from 'zrender/src/graphic/shape/Line'; +import { subPixelOptimize } from 'zrender/src/graphic/helper/subPixelOptimize'; +import { Group, Text, Rect, Line, XY, setTooltipConfig, expandOrShrinkRect } from '../../util/graphic'; +import { ListIterator } from '../../util/model'; +import { clone, retrieve2 } from 'zrender/src/core/util'; +import { invert } from 'zrender/src/core/matrix'; +import { MatrixBodyCorner, MatrixBodyOrCornerKind } from '../../coord/matrix/MatrixBodyCorner'; +import { setLabelStyle } from '../../label/labelStyle'; + +const round = Math.round; + +// When special border style is defined on cell, it +// should be over all of the other borders. +type Z2CellDefault = {normal: number, special: number}; +const Z2_BACKGROUND = 0; +const Z2_OUTER_BORDER = 99; +const Z2_BODY_CORNER_CELL_DEFAULT: Z2CellDefault = {normal: 25, special: 100}; +const Z2_DIMENSION_CELL_DEFAULT: Z2CellDefault = {normal: 50, special: 125}; + +class MatrixView extends ComponentView { + + static type = 'matrix'; + type = MatrixView.type; + + render(matrixModel: MatrixModel) { + + this.group.removeAll(); + + const group = this.group; + const coordSys = matrixModel.coordinateSystem; + const rect = coordSys.getRect(); + const xDimModel = matrixModel.getDimensionModel('x'); + const yDimModel = matrixModel.getDimensionModel('y'); + const xDim = xDimModel.dim; + const yDim = yDimModel.dim; + + // PENDING: + // reuse the existing text and rect elements for performance? + + renderDimensionCells( + group, + matrixModel + ); + + createBodyAndCorner( + group, + matrixModel, + xDim, + yDim + ); + + const borderZ2Option = matrixModel.getShallow('borderZ2', true); + const outerBorderZ2 = retrieve2(borderZ2Option, Z2_OUTER_BORDER); + const dividerLineZ2 = outerBorderZ2 - 1; + + // Outer border and overall background. Use separate elements because of z-order: + // The overall background should appear below any other elements. + // But in most cases, the outer border and the divider line should be above the normal cell borders - + // especially when cell borders have different colors. But users may highlight some specific cells by + // overstirking their border, in which case it should be above the outer border. + const bgStyle = matrixModel.getModel('backgroundStyle').getItemStyle( + ['borderWidth'] + ); + bgStyle.lineWidth = 0; + const borderStyle = matrixModel.getModel('backgroundStyle').getItemStyle( + ['color', 'decal', 'shadowColor', 'shadowBlur', 'shadowOffsetX', 'shadowOffsetY'] + ); + borderStyle.fill = 'none'; + const bgRect = createMatrixRect(rect.clone(), bgStyle, Z2_BACKGROUND); + const borderRect = createMatrixRect(rect.clone(), borderStyle, outerBorderZ2); + bgRect.silent = true; + borderRect.silent = true; + group.add(bgRect); + group.add(borderRect); + + // Header split line. + const xDimCell0 = xDim.getUnitLayoutInfo(0, 0); + const yDimCell0 = yDim.getUnitLayoutInfo(1, 0); + if (xDimCell0 && yDimCell0) { + if (xDim.shouldShow()) { + group.add(createMatrixLine( + { + x1: rect.x, + y1: yDimCell0.xy, + x2: rect.x + rect.width, + y2: yDimCell0.xy, + }, + xDimModel.getModel('dividerLineStyle').getLineStyle(), + dividerLineZ2, + )); + } + if (yDim.shouldShow()) { + group.add(createMatrixLine( + { + x1: xDimCell0.xy, + y1: rect.y, + x2: xDimCell0.xy, + y2: rect.y + rect.height, + }, + yDimModel.getModel('dividerLineStyle').getLineStyle(), + dividerLineZ2, + )); + } + } + } +} + +function renderDimensionCells(group: Group, matrixModel: MatrixModel) { + + renderOnDimension(0); + renderOnDimension(1); + + function renderOnDimension(dimIdx: 0 | 1) { + const thisDimModel = matrixModel.getDimensionModel(XY[dimIdx]); + const thisDim = thisDimModel.dim; + + if (!thisDim.shouldShow()) { + return; + } + + const thisDimBgStyleModel = thisDimModel.getModel('itemStyle'); + const thisDimLabelModel = thisDimModel.getModel('label'); + const tooltipOption = matrixModel.getShallow('tooltip', true); + const xyLocator: MatrixXYLocator[] = []; + + for (const it = thisDim.resetCellIterator(); it.next();) { + const dimCell = it.item; + const shape = {} as RectLike; + BoundingRect.copy(shape, dimCell.rect); + + vectorUtil.set(xyLocator, dimCell.id.x, dimCell.id.y); + + createMatrixCell( + xyLocator, + matrixModel, + group, + dimCell.option, + thisDimBgStyleModel, + thisDimLabelModel, + thisDimModel, + shape, + dimCell.option.value, + Z2_DIMENSION_CELL_DEFAULT, + tooltipOption + ); + } + } +} + +function createBodyAndCorner( + group: Group, + matrixModel: MatrixModel, + xDim: MatrixDim, + yDim: MatrixDim +): void { + + createBodyOrCornerCells('body', matrixModel.getBody(), xDim, yDim); + if (xDim.shouldShow() && yDim.shouldShow()) { + createBodyOrCornerCells('corner', matrixModel.getCorner(), yDim, xDim); + } + + function createBodyOrCornerCells( + bodyCornerOptionRoot: TBodyOrCornerKind, + bodyOrCorner: MatrixBodyCorner, + dimForCoordX: MatrixDim, // Can be `matrix.y` (transposed) for corners. + dimForCoordY: MatrixDim, // Can be `matrix.x` (trnasposed) for corners. + ): void { + // Prevent inheriting from ancestor. + const parentCellModel = new Model(matrixModel.getShallow(bodyCornerOptionRoot, true)); + const parentItemStyleModel = parentCellModel.getModel('itemStyle'); + const parentLabelModel = parentCellModel.getModel('label'); + + const itx = new ListIterator(); + const ity = new ListIterator(); + const xyLocator: number[] = []; + const tooltipOption = matrixModel.getShallow('tooltip', true); + + for (dimForCoordY.resetLayoutIterator(ity, 1); ity.next();) { + for (dimForCoordX.resetLayoutIterator(itx, 0); itx.next();) { + const xLayout = itx.item; + const yLayout = ity.item; + + vectorUtil.set(xyLocator, xLayout.id.x, yLayout.id.y); + const bodyCornerCell = bodyOrCorner.getCell(xyLocator); + + // If in span of an other body or corner cell, never render it. + if (bodyCornerCell && bodyCornerCell.inSpanOf && bodyCornerCell.inSpanOf !== bodyCornerCell) { + continue; + } + + const shape = {} as RectLike; + if (bodyCornerCell && bodyCornerCell.span) { + BoundingRect.copy(shape, bodyCornerCell.spanRect); + } + else { + xLayout.dim.getLayout(shape, 0, xyLocator[0]); + yLayout.dim.getLayout(shape, 1, xyLocator[1]); + } + + const bodyCornerCellOption = bodyCornerCell ? bodyCornerCell.option : null; + + createMatrixCell( + xyLocator, + matrixModel, + group, + bodyCornerCellOption, + parentItemStyleModel, + parentLabelModel, + parentCellModel, + shape, + bodyCornerCellOption ? bodyCornerCellOption.value : null, + Z2_BODY_CORNER_CELL_DEFAULT, + tooltipOption + ); + } + } + } // End of createBodyOrCornerCells +} + +function createMatrixCell( + xyLocator: MatrixXYLocator[], + matrixModel: MatrixModel, + group: Group, + cellOption: MatrixBaseCellOption | NullUndefined, + parentItemStyleModel: Model, + parentLabelModel: Model, + parentCellModel: Model, + shape: RectLike, + textValue: unknown, + zrCellDefault: Z2CellDefault, + tooltipOption: MatrixOption['tooltip'], +): void { + // Do not use getModel for handy performance optimization. + _tmpCellItemStyleModel.option = cellOption ? cellOption.itemStyle : null; + _tmpCellItemStyleModel.parentModel = parentItemStyleModel; + _tmpCellModel.option = cellOption; + _tmpCellModel.parentModel = parentCellModel; + + // Use different z2 becuase special border may be defined in itemStyle. + const z2 = retrieve2( + _tmpCellModel.getShallow('z2'), + (cellOption && cellOption.itemStyle) ? zrCellDefault.special : zrCellDefault.normal + ); + const tooltipOptionShow = tooltipOption && tooltipOption.show; + + const cellRect = createMatrixRect(shape, _tmpCellItemStyleModel.getItemStyle(), z2); + group.add(cellRect); + + const cursorOption = _tmpCellModel.get('cursor'); + if (cursorOption != null) { + cellRect.attr('cursor', cursorOption); + } + let cellText: Text | NullUndefined; + + if (textValue != null) { + const text = textValue + ''; + _tmpCellLabelModel.option = cellOption ? cellOption.label : null; + _tmpCellLabelModel.parentModel = parentLabelModel; + + setLabelStyle( + cellRect, + // Currently do not support other states (`emphasis`, `select`, `blur`) + {normal: _tmpCellLabelModel}, + { + defaultText: text, + autoOverflowArea: true, + // By default based on boundingRect. But boundingRect contains borderWidth, + // and borderWidth is half outside the cell. Thus specific `layoutRect` explicitly. + layoutRect: clone(cellRect.shape) + }, + ); + cellText = cellRect.getTextContent(); + if (cellText) { + cellText.z2 = z2 + 1; + + const style = cellText.style; + if (style && (style.overflow && style.overflow !== 'none' && style.lineOverflow)) { + // `overflow: 'break'/'breakAll'/'truncate'` does not guarantee prevention of overflow + // when space is insufficient. Use a `clipPath` in such case. + const clipShape = {} as RectLike; + BoundingRect.copy(clipShape, shape); + // `lineWidth` is half outside half inside the bounding rect. + expandOrShrinkRect(clipShape, (cellRect.style?.lineWidth || 0) / 2, true, true); + cellRect.updateInnerText(); + cellText.getLocalTransform(_tmpInnerTextTrans); + invert(_tmpInnerTextTrans, _tmpInnerTextTrans); + BoundingRect.applyTransform(clipShape, clipShape, _tmpInnerTextTrans); + cellText.setClipPath(new Rect({shape: clipShape})); + } + } + + setTooltipConfig({ // At least for text overflow. + el: cellRect, + componentModel: matrixModel, + itemName: text, + itemTooltipOption: tooltipOption, + formatterParamsExtra: { + xyLocator: xyLocator.slice() + } + }); + } + + // Set silent + if (cellText) { + let labelSilent = _tmpCellLabelModel.get('silent'); + // auto, tooltip of text cells need silient: false, but non-text cells + // do not need a special cursor in most cases. + if (labelSilent == null) { + labelSilent = !tooltipOptionShow; + } + cellText.silent = labelSilent; + cellText.ignoreHostSilent = true; + } + let rectSilent = _tmpCellModel.get('silent'); + if (rectSilent == null) { + rectSilent = ( + // If no background color in cell, set `rect.silent: false` will cause that only + // the border response to mouse hovering, which is probably weird. + !cellRect.style || cellRect.style.fill === 'none' || !cellRect.style.fill + ); + } + cellRect.silent = rectSilent; +} +const _tmpCellModel = new Model(); +const _tmpCellItemStyleModel = new Model(); +const _tmpCellLabelModel = new Model(); +const _tmpInnerTextTrans: number[] = []; + +// FIXME: move all of the subpixel process to Matrix.ts resize, otherwise the result of +// `dataToLayout` is not consistent with this rendering, and the caller (like heatmap) can +// not precisely align with the matrix border. +function createMatrixRect( + shape: RectShape, style: ItemStyleProps, z2: number +): Rect { + // Currently `subPixelOptimizeRect` can not be used here because it will break rect alignment. + // Optimize line and rect with the same direction. + const lineWidth = style.lineWidth; + if (lineWidth) { + const x2Original = shape.x + shape.width; + const y2Original = shape.y + shape.height; + shape.x = subPixelOptimize(shape.x, lineWidth, true); + shape.y = subPixelOptimize(shape.y, lineWidth, true); + shape.width = subPixelOptimize(x2Original, lineWidth, true) - shape.x; + shape.height = subPixelOptimize(y2Original, lineWidth, true) - shape.y; + } + return new Rect({ + shape, + style: style, + z2, + }); +} + +function createMatrixLine(shape: Omit, style: LineStyleProps, z2: number): Line { + const lineWidth = style.lineWidth; + if (lineWidth) { + if (round(shape.x1 * 2) === round(shape.x2 * 2)) { + shape.x1 = shape.x2 = subPixelOptimize(shape.x1, lineWidth, true); + } + if (round(shape.y1 * 2) === round(shape.y2 * 2)) { + shape.y1 = shape.y2 = subPixelOptimize(shape.y1, lineWidth, true); + } + } + return new Line({ + shape, + style, + silent: true, + z2, + }); +} + +export default MatrixView; diff --git a/src/component/matrix/install.ts b/src/component/matrix/install.ts new file mode 100644 index 0000000000..86a0ccc85b --- /dev/null +++ b/src/component/matrix/install.ts @@ -0,0 +1,29 @@ +/* +* 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 { EChartsExtensionInstallRegisters } from '../../extension'; +import MatrixModel from '../../coord/matrix/MatrixModel'; +import MatrixView from './MatrixView'; +import Matrix from '../../coord/matrix/Matrix'; + +export function install(registers: EChartsExtensionInstallRegisters) { + registers.registerComponentModel(MatrixModel); + registers.registerComponentView(MatrixView); + registers.registerCoordinateSystem('matrix', Matrix); +} diff --git a/src/component/thumbnail.ts b/src/component/thumbnail.ts new file mode 100644 index 0000000000..1370d941f5 --- /dev/null +++ b/src/component/thumbnail.ts @@ -0,0 +1,25 @@ +/* +* 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 { use } from '../extension'; +import { install } from './thumbnail/install'; + +use(install); diff --git a/src/component/thumbnail/ThumbnailBridgeImpl.ts b/src/component/thumbnail/ThumbnailBridgeImpl.ts new file mode 100644 index 0000000000..9b33cec8c5 --- /dev/null +++ b/src/component/thumbnail/ThumbnailBridgeImpl.ts @@ -0,0 +1,93 @@ +/* +* 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 { calcZ2Range, Group } from '../../util/graphic'; +import { RoamOptionMixin } from '../../util/types'; +import { ThumbnailBridge, ThumbnailTargetTransformRawToViewport } from '../helper/thumbnailBridge'; +import ExtensionAPI from '../../core/ExtensionAPI'; +import type { ThumbnailModel } from './ThumbnailModel'; +import type { ThumbnailView } from './ThumbnailView'; +import BoundingRect from 'zrender/src/core/BoundingRect'; + +export interface ThumbnailBridgeRendered { + roamType: RoamOptionMixin['roam']; + group: Group; + targetTrans: ThumbnailTargetTransformRawToViewport; + z2Range: {min: number, max: number}; + viewportRect: BoundingRect; + // Use version becuase: + // - `renderContent` may be called asynchronously by graph force layout. + // - The order of `updateContent` and `ComponentView['render']` is not guaranteed. + renderVersion: number; +} + +/** + * [CAVEAT]: the call order of `ThumbnailView['render']` and other + * `ChartView['render']/ComponentView['render']` is not guaranteed. + */ +export class ThumbnailBridgeImpl implements ThumbnailBridge { + + private _thumbnailModel: ThumbnailModel; + private _renderVersion: number; + + constructor(thumbnailModel: ThumbnailModel) { + this._thumbnailModel = thumbnailModel; + } + + reset(api: ExtensionAPI) { + this._renderVersion = api.getMainProcessVersion(); + } + + renderContent(opt: { + roamType: RoamOptionMixin['roam']; + viewportRect: BoundingRect; + group: Group; + targetTrans: ThumbnailTargetTransformRawToViewport; + api: ExtensionAPI; + }): void { + const thumbnailView = opt.api.getViewOfComponentModel(this._thumbnailModel) as ThumbnailView; + if (!thumbnailView) { + return; + } + opt.group.silent = true; + thumbnailView.renderContent({ + group: opt.group, + targetTrans: opt.targetTrans, + z2Range: calcZ2Range(opt.group), + roamType: opt.roamType, + viewportRect: opt.viewportRect, + renderVersion: this._renderVersion, + }); + } + + updateWindow( + targetTrans: ThumbnailTargetTransformRawToViewport, + api: ExtensionAPI + ): void { + const thumbnailView = api.getViewOfComponentModel(this._thumbnailModel) as ThumbnailView; + if (!thumbnailView) { + return; + } + thumbnailView.updateWindow({ + targetTrans, + renderVersion: this._renderVersion, + }); + } + +} diff --git a/src/component/thumbnail/ThumbnailModel.ts b/src/component/thumbnail/ThumbnailModel.ts new file mode 100644 index 0000000000..7f480dc5ac --- /dev/null +++ b/src/component/thumbnail/ThumbnailModel.ts @@ -0,0 +1,161 @@ +/* +* 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 ComponentModel from '../../model/Component'; +import { error } from '../../util/log'; +import { + BorderOptionMixin, BoxLayoutOptionMixin, ComponentOption, ItemStyleOption, NullUndefined, +} from '../../util/types'; +import tokens from '../../visual/tokens'; +import { + injectThumbnailBridge +} from '../helper/thumbnailBridge'; +import { ThumbnailBridgeImpl } from './ThumbnailBridgeImpl'; + +/** + * [NOTE]: thumbnail is implemented as a component, rather than internal data strucutrue, + * due to the possibility of serveing geo and related series with a single thumbnail, + * and enable to apply some common layout feature, such as matrix coord sys. + */ + +// TODO: currently only graph supports thumbnail. +// May need some refactor if serving new components in future. + +export interface ThumbnailOption extends ComponentOption, BoxLayoutOptionMixin, BorderOptionMixin { + mainType?: 'thumbnail' + + show?: boolean + + itemStyle?: ItemStyleOption + windowStyle?: ItemStyleOption + + seriesIndex?: number | number[] + seriesId?: string | string[] +} + +export interface ThumbnailZ2Setting { + background: number; + window: number; +} + +export class ThumbnailModel extends ComponentModel { + static type = 'thumbnail' as const; + type = ThumbnailModel.type; + + static layoutMode = 'box' as const; + + preventAutoZ = true; + + // All the supported components should be added here. + static dependencies = ['series', 'geo']; + + static defaultOption: ThumbnailOption = { + show: true, + right: 1, + bottom: 1, + height: '25%', + width: '25%', + + itemStyle: { + // Use echarts option.backgorundColor by default. + borderColor: tokens.color.border, + borderWidth: 2 + }, + + windowStyle: { + borderWidth: 1, + color: tokens.color.neutral30, + borderColor: tokens.color.neutral40, + opacity: 0.3 + }, + + z: 10, + + }; + + // Never remove after created. + private _birdge: ThumbnailBridgeImpl; + + private _target: { + baseMapProvider: ComponentModel | NullUndefined + // May extend. + } | NullUndefined; + + optionUpdated(newCptOption: ThumbnailOption, isInit: boolean): void { + this._updateBridge(); + } + + private _updateBridge() { + const bridge = this._birdge = this._birdge || new ThumbnailBridgeImpl(this); + + // Clear all, in case of option changed. + this._target = null; + this.ecModel.eachSeries(series => { + injectThumbnailBridge(series, null); + }); + + if (this.shouldShow()) { + const target = this.getTarget(); + // If a component is targeted by more than one thumbnails, simply only the last one works. + injectThumbnailBridge(target.baseMapProvider, bridge); + } + } + + shouldShow() { + return this.getShallow('show', true); + } + + getBridge(): ThumbnailBridgeImpl { + return this._birdge; + } + + getTarget(): { + baseMapProvider: ComponentModel | NullUndefined + // May extend. + } { + if (this._target) { + return this._target; + } + + // Find by `seriesId`/`seriesIndex`. + let series = this.getReferringComponents('series', { + useDefault: false, enableAll: false, enableNone: false + }).models[0]; + if (series) { + if (series.subType !== 'graph') { + series = null; + if (__DEV__) { + error(`series.${series.subType} is not supported in thumbnail.`, true); + } + } + } + else { + // If no xxxId and xxxIndex specified, find the first series.graph. If other components, + // such as geo, is supported in future, the default stretagy may be extended. + series = this.ecModel.queryComponents({mainType: 'series', subType: 'graph'})[0]; + } + + this._target = { + baseMapProvider: series + }; + + return this._target; + } + +} diff --git a/src/component/thumbnail/ThumbnailView.ts b/src/component/thumbnail/ThumbnailView.ts new file mode 100644 index 0000000000..617f6dfda1 --- /dev/null +++ b/src/component/thumbnail/ThumbnailView.ts @@ -0,0 +1,322 @@ +/* +* 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 ExtensionAPI from '../../core/ExtensionAPI'; +import GlobalModel from '../../model/Global'; +import ComponentView from '../../view/Component'; +import { ThumbnailModel } from './ThumbnailModel'; +import { NullUndefined, RoamOptionMixin } from '../../util/types'; +import BoundingRect from 'zrender/src/core/BoundingRect'; +import * as matrix from 'zrender/src/core/matrix'; +import RoamController, { RoamEventParams } from '../helper/RoamController'; +import tokens from '../../visual/tokens'; +import { createBoxLayoutReference, getBoxLayoutParams, getLayoutRect } from '../../util/layout'; +import { expandOrShrinkRect, Rect, Group, traverseUpdateZ, retrieveZInfo } from '../../util/graphic'; +import { RectShape } from 'zrender/src/graphic/shape/Rect'; +import { applyTransform } from 'zrender/src/core/vector'; +import View from '../../coord/View'; +import { bind, defaults, extend } from 'zrender/src/core/util'; +import type ComponentModel from '../../model/Component'; +import { RoamPayload } from '../helper/roamHelper'; +import { ThumbnailBridgeRendered } from './ThumbnailBridgeImpl'; + + +export class ThumbnailView extends ComponentView { + + static type = 'thumbnail' as const; + type = ThumbnailView.type; + + private _api: ExtensionAPI; + private _model: ThumbnailModel; + private _bgRect: Rect; + private _windowRect: Rect; + private _contentRect: BoundingRect; + private _targetGroup: Group; + private _transThisToTarget: matrix.MatrixArray; + private _roamController: RoamController; + private _coordSys: View; + private _bridgeRendered: ThumbnailBridgeRendered | NullUndefined; + + // The version of rendering result. + // `render`/`updateContent`/`updateWindow` can be called separatedly and the order + // is not guaranteed. Use version to ensure the consistency. + private _renderVersion: number; + + render(thumbnailModel: ThumbnailModel, ecModel: GlobalModel, api: ExtensionAPI): void { + this._api = api; + this._model = thumbnailModel; + if (!this._coordSys) { + this._coordSys = new View(); + } + + if (!this._isEnabled()) { + this._clear(); + return; + } + + this._renderVersion = api.getMainProcessVersion(); + const group = this.group; + + group.removeAll(); + + const itemStyleModel = thumbnailModel.getModel('itemStyle'); + const itemStyle = itemStyleModel.getItemStyle(); + if (itemStyle.fill == null) { + itemStyle.fill = ecModel.get('backgroundColor') || tokens.color.neutral00; + } + + const refContainer = createBoxLayoutReference(thumbnailModel, api).refContainer; + const boxRect = getLayoutRect( + getBoxLayoutParams(thumbnailModel, true), + refContainer + ); + const boxBorderWidth = itemStyle.lineWidth || 0; + const contentRect = this._contentRect = expandOrShrinkRect( + boxRect.clone(), boxBorderWidth / 2, true, true + ); + + const contentGroup = new Group(); + group.add(contentGroup); + contentGroup.setClipPath(new Rect({shape: contentRect.plain()})); + + const targetGroup = this._targetGroup = new Group(); + contentGroup.add(targetGroup); + + // Draw border and background and shadow of thumbnail box. + const borderShape: RectShape = boxRect.plain(); + borderShape.r = itemStyleModel.getShallow('borderRadius', true) as (number | number[]); + group.add(this._bgRect = new Rect({ + style: itemStyle, + shape: borderShape, + silent: false, // Prevent from hovering on the lower elements. + cursor: 'grab', + })); + + const windowStyleModel = thumbnailModel.getModel('windowStyle'); + const windowR = windowStyleModel.getShallow('borderRadius', true) as (number | number[]); + contentGroup.add(this._windowRect = new Rect({ + shape: {x: 0, y: 0, width: 0, height: 0, r: windowR}, + style: windowStyleModel.getItemStyle(), + silent: false, // Prevent from hovering on the lower elements. + cursor: 'grab', + })); + + this._dealRenderContent(); + this._dealUpdateWindow(); + + updateZ(thumbnailModel, this); + } + + /** + * Can be called asynchronously directly. + * This method should be idempotent. + */ + renderContent(bridgeRendered: ThumbnailBridgeRendered): void { + this._bridgeRendered = bridgeRendered; + if (this._isEnabled()) { + this._dealRenderContent(); + this._dealUpdateWindow(); + updateZ(this._model, this); + } + } + + private _dealRenderContent(): void { + const bridgeRendered = this._bridgeRendered; + if (!bridgeRendered || bridgeRendered.renderVersion !== this._renderVersion) { + return; + } + + const targetGroup = this._targetGroup; + const coordSys = this._coordSys; + const contentRect = this._contentRect; + + targetGroup.removeAll(); + + if (!bridgeRendered) { + return; + } + + const bridgeGroup = bridgeRendered.group; + const bridgeRect = bridgeGroup.getBoundingRect(); + + targetGroup.add(bridgeGroup); + + this._bgRect.z2 = bridgeRendered.z2Range.min - 10; + + coordSys.setBoundingRect(bridgeRect.x, bridgeRect.y, bridgeRect.width, bridgeRect.height); + // Use `getLayoutRect` is just to find an approperiate rect in thumbnail. + const viewRect = getLayoutRect( + { + left: 'center', + top: 'center', + aspect: bridgeRect.width / bridgeRect.height + }, + contentRect + ); + coordSys.setViewRect(viewRect.x, viewRect.y, viewRect.width, viewRect.height); + bridgeGroup.attr(coordSys.getTransformInfo().raw); + + this._windowRect.z2 = bridgeRendered.z2Range.max + 10; + + this._resetRoamController(bridgeRendered.roamType); + } + + /** + * Can be called from action handler directly. + * This method should be idempotent. + */ + updateWindow(param: Pick): void { + const bridgeRendered = this._bridgeRendered; + if (bridgeRendered && bridgeRendered.renderVersion === param.renderVersion) { + bridgeRendered.targetTrans = param.targetTrans; + } + if (this._isEnabled()) { + this._dealUpdateWindow(); + } + } + + private _dealUpdateWindow(): void { + const bridgeRendered = this._bridgeRendered; + if (!bridgeRendered || bridgeRendered.renderVersion !== this._renderVersion) { + return; + } + + const invTargetTrans = matrix.invert([], bridgeRendered.targetTrans); + const transTargetToThis = matrix.mul([], this._coordSys.transform, invTargetTrans); + this._transThisToTarget = matrix.invert([], transTargetToThis); + + let viewportRect = bridgeRendered.viewportRect; + if (!viewportRect) { + viewportRect = new BoundingRect(0, 0, this._api.getWidth(), this._api.getHeight()); + } + else { + viewportRect = viewportRect.clone(); + } + viewportRect.applyTransform(transTargetToThis); + const windowRect = this._windowRect; + const r = windowRect.shape.r; + windowRect.setShape(defaults({r}, viewportRect)); + } + + private _resetRoamController( + roamType: RoamOptionMixin['roam'], + ): void { + const api = this._api; + + let roamController = this._roamController; + if (!roamController) { + roamController = this._roamController = new RoamController(api.getZr()); + } + + if (!roamType || !this._isEnabled()) { + roamController.disable(); + return; + } + + roamController.enable(roamType, { + api: api, + zInfo: {component: this._model}, + triggerInfo: { + roamTrigger: null, + isInSelf: (e, x, y) => this._contentRect.contain(x, y) + } + }); + roamController + .off('pan') + .off('zoom') + .on('pan', bind(this._onPan, this)) + .on('zoom', bind(this._onZoom, this)); + } + + private _onPan(event: RoamEventParams['pan']): void { + const trans = this._transThisToTarget; + if (!this._isEnabled() || !trans) { + return; + } + const oldOffset = applyTransform([], [event.oldX, event.oldY], trans); + const newOffset = applyTransform([], [event.oldX - event.dx, event.oldY - event.dy], trans); + + this._api.dispatchAction(makeRoamPayload(this._model.getTarget().baseMapProvider, { + dx: newOffset[0] - oldOffset[0], + dy: newOffset[1] - oldOffset[1], + })); + } + + private _onZoom(event: RoamEventParams['zoom']): void { + const trans = this._transThisToTarget; + if (!this._isEnabled() || !trans) { + return; + } + const offset = applyTransform([], [event.originX, event.originY], trans); + + this._api.dispatchAction(makeRoamPayload(this._model.getTarget().baseMapProvider, { + zoom: 1 / event.scale, + originX: offset[0], + originY: offset[1], + })); + } + + /** + * This method is also responsible for check enable in asynchronous situation, + * e.g., in event listeners that is supposed to be outdated but not be removed. + */ + private _isEnabled(): boolean { + const thumbnailModel = this._model; + if (!thumbnailModel || !thumbnailModel.shouldShow()) { + return false; + } + const baseMapProvider = thumbnailModel.getTarget().baseMapProvider; + if (!baseMapProvider) { + return false; + } + return true; + } + + private _clear(): void { + this.group.removeAll(); + this._bridgeRendered = null; + if (this._roamController) { + this._roamController.disable(); + } + } + + remove() { + this._clear(); + } + + dispose() { + this._clear(); + } + +} + +function makeRoamPayload(baseMapProvider: ComponentModel, params: Partial): RoamPayload { + const type = baseMapProvider.mainType === 'series' + ? `${baseMapProvider.subType}Roam` // e.g. 'graphRoam' + : `${baseMapProvider.mainType}Roam`; // e.g., 'geoRoam' + const payload = {type} as RoamPayload; + payload[`${baseMapProvider.mainType}Id`] = baseMapProvider.id; + extend(payload, params); + return payload; +} + +function updateZ(thumbnailModel: ThumbnailModel, thumbnailView: ThumbnailView): void { + const zInfo = retrieveZInfo(thumbnailModel); + traverseUpdateZ(thumbnailView.group, zInfo.z, zInfo.zlevel); +} diff --git a/src/component/thumbnail/install.ts b/src/component/thumbnail/install.ts new file mode 100644 index 0000000000..4cacdda222 --- /dev/null +++ b/src/component/thumbnail/install.ts @@ -0,0 +1,27 @@ +/* +* 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 { EChartsExtensionInstallRegisters } from '../../extension'; +import { ThumbnailModel } from './ThumbnailModel'; +import { ThumbnailView } from './ThumbnailView'; + +export function install(registers: EChartsExtensionInstallRegisters) { + registers.registerComponentModel(ThumbnailModel); + registers.registerComponentView(ThumbnailView); +} diff --git a/src/component/timeline.ts b/src/component/timeline.ts index d42d780593..bc264e1706 100644 --- a/src/component/timeline.ts +++ b/src/component/timeline.ts @@ -24,4 +24,4 @@ import { use } from '../extension'; import { install } from './timeline/install'; -use(install); \ No newline at end of file +use(install); diff --git a/src/component/timeline/SliderTimelineView.ts b/src/component/timeline/SliderTimelineView.ts index f993e57138..0a547abd75 100644 --- a/src/component/timeline/SliderTimelineView.ts +++ b/src/component/timeline/SliderTimelineView.ts @@ -756,10 +756,7 @@ function createScaleByModel(model: SliderTimelineModel, axisType?: string): Scal function getViewRect(model: SliderTimelineModel, api: ExtensionAPI) { return layout.getLayoutRect( model.getBoxLayoutParams(), - { - width: api.getWidth(), - height: api.getHeight() - }, + layout.createBoxLayoutReference(model, api).refContainer, model.get('padding') ); } diff --git a/src/component/title/install.ts b/src/component/title/install.ts index d5fdf9344d..d127ad0464 100644 --- a/src/component/title/install.ts +++ b/src/component/title/install.ts @@ -21,7 +21,7 @@ import * as zrUtil from 'zrender/src/core/util'; import * as graphic from '../../util/graphic'; import {getECData} from '../../util/innerStore'; import {createTextStyle} from '../../label/labelStyle'; -import {getLayoutRect} from '../../util/layout'; +import {createBoxLayoutReference, getLayoutRect} from '../../util/layout'; import ComponentModel from '../../model/Component'; import { ComponentOption, @@ -134,7 +134,6 @@ class TitleModel extends ComponentModel { }; } - // View class TitleView extends ComponentView { @@ -213,11 +212,10 @@ class TitleView extends ComponentView { const layoutOption = titleModel.getBoxLayoutParams(); layoutOption.width = groupRect.width; layoutOption.height = groupRect.height; + + const layoutRef = createBoxLayoutReference(titleModel, api); const layoutRect = getLayoutRect( - layoutOption, { - width: api.getWidth(), - height: api.getHeight() - }, titleModel.get('padding') + layoutOption, layoutRef.refContainer, titleModel.get('padding') ); // Adjust text align based on position if (!textAlign) { diff --git a/src/component/toolbox/ToolboxView.ts b/src/component/toolbox/ToolboxView.ts index 9c751218d5..d3f96d1b40 100644 --- a/src/component/toolbox/ToolboxView.ts +++ b/src/component/toolbox/ToolboxView.ts @@ -40,6 +40,7 @@ import { getUID } from '../../util/component'; import Displayable from 'zrender/src/graphic/Displayable'; import ZRText from 'zrender/src/graphic/Text'; import { getFont } from '../../label/labelStyle'; +import { box, createBoxLayoutReference, getLayoutRect, positionElement } from '../../util/layout'; import tokens from '../../visual/tokens'; type IconPath = ToolboxFeatureModel['iconPaths'][string]; @@ -293,7 +294,28 @@ class ToolboxView extends ComponentView { }); } - listComponentHelper.layout(group, toolboxModel, api); + const refContainer = createBoxLayoutReference(toolboxModel, api).refContainer; + const boxLayoutParams = toolboxModel.getBoxLayoutParams(); + const padding = toolboxModel.get('padding'); + const viewRect = getLayoutRect( + boxLayoutParams, + refContainer, + padding + ); + box( + toolboxModel.get('orient'), + group, + toolboxModel.get('itemGap'), + viewRect.width, + viewRect.height + ); + positionElement( + group, + boxLayoutParams, + refContainer, + padding + ); + // Render background after group is layout // FIXME group.add(listComponentHelper.makeBackground(group.getBoundingRect(), toolboxModel)); diff --git a/src/component/visualMap/VisualMapModel.ts b/src/component/visualMap/VisualMapModel.ts index 62821439bc..9139d54e8c 100644 --- a/src/component/visualMap/VisualMapModel.ts +++ b/src/component/visualMap/VisualMapModel.ts @@ -32,7 +32,8 @@ import { BorderOptionMixin, OptionDataValue, BuiltinVisualProperty, - DimensionIndex + DimensionIndex, + OptionId } from '../../util/types'; import ComponentModel from '../../model/Component'; import Model from '../../model/Model'; @@ -68,12 +69,13 @@ export interface VisualMapOption /** * 'all' or null/undefined: all series. * A number or an array of number: the specified series. - * set min: 0, max: 200, only for campatible with ec2. - * In fact min max should not have default value. */ seriesIndex?: 'all' | number[] | number + seriesId?: OptionId | OptionId[] /** + * set min: 0, max: 200, only for campatible with ec2. + * In fact min max should not have default value. * min value, must specified if pieces is not specified. */ min?: number @@ -238,23 +240,30 @@ class VisualMapModel extends Com } /** - * @protected - * @return {Array.} An array of series indices. + * @return An array of series indices. */ - getTargetSeriesIndices() { - const optionSeriesIndex = this.option.seriesIndex; - let seriesIndices: number[] = []; - - if (optionSeriesIndex == null || optionSeriesIndex === 'all') { - this.ecModel.eachSeries(function (seriesModel, index) { - seriesIndices.push(index); - }); - } - else { - seriesIndices = modelUtil.normalizeToArray(optionSeriesIndex); + protected getTargetSeriesIndices(): number[] { + const optionSeriesId = this.option.seriesId; + let optionSeriesIndex = this.option.seriesIndex; + if (optionSeriesIndex == null && optionSeriesId == null) { + optionSeriesIndex = 'all'; } - return seriesIndices; + const seriesModels = modelUtil.queryReferringComponents( + this.ecModel, + 'series', + { + index: optionSeriesIndex, + id: optionSeriesId, + }, + { + useDefault: false, + enableAll: true, + enableNone: false, + } + ).models; + + return zrUtil.map(seriesModels, seriesModel => seriesModel.componentIndex); } /** @@ -606,7 +615,7 @@ class VisualMapModel extends Com // zlevel: 0, z: 4, - seriesIndex: 'all', + // seriesIndex: 'all', min: 0, max: 200, diff --git a/src/component/visualMap/VisualMapView.ts b/src/component/visualMap/VisualMapView.ts index 1c24317071..76cf41f0fe 100644 --- a/src/component/visualMap/VisualMapView.ts +++ b/src/component/visualMap/VisualMapView.ts @@ -154,10 +154,11 @@ class VisualMapView extends ComponentView { const model = this.visualMapModel; const api = this.api; + const refContainer = layout.createBoxLayoutReference(model, api).refContainer; layout.positionElement( group, model.getBoxLayoutParams(), - {width: api.getWidth(), height: api.getHeight()} + refContainer ); } diff --git a/src/coord/CoordinateSystem.ts b/src/coord/CoordinateSystem.ts index ea17e8a9aa..0a43b51a98 100644 --- a/src/coord/CoordinateSystem.ts +++ b/src/coord/CoordinateSystem.ts @@ -20,7 +20,10 @@ import GlobalModel from '../model/Global'; import {ParsedModelFinder} from '../util/model'; import ExtensionAPI from '../core/ExtensionAPI'; -import { DimensionDefinitionLoose, ScaleDataValue, DimensionName } from '../util/types'; +import { + DimensionDefinitionLoose, ScaleDataValue, DimensionName, NullUndefined, CoordinateSystemDataLayout, + CoordinateSystemDataCoord +} from '../util/types'; import Axis from './Axis'; import { BoundingRect } from '../util/graphic'; import { MatrixArray } from 'zrender/src/core/matrix'; @@ -45,6 +48,8 @@ export interface CoordinateSystemCreator { /** * The instance get from `CoordinateSystemManger` is `CoordinateSystemMaster`. + * Consider a typical case: `grid` is a `CoordinateSystemMaster`, and it contains + * one or multiple `cartesian2d`s, which are `CoordinateSystem`s. */ export interface CoordinateSystemMaster { @@ -55,23 +60,45 @@ export interface CoordinateSystemMaster { model?: ComponentModel; + // Injected if required. + boxCoordinateSystem?: CoordinateSystem; + update?: (ecModel: GlobalModel, api: ExtensionAPI) => void; // This methods is also responsible for determining whether this // coordinate system is applicable to the given `finder`. // Each coordinate system will be tried, until one returns non- // null/undefined value. + // Aslo support + // const resultNumber = convertToPixel({someAxis: 0}, number); convertToPixel?( - ecModel: GlobalModel, finder: ParsedModelFinder, value: ScaleDataValue | ScaleDataValue[] - ): number | number[]; + ecModel: GlobalModel, + finder: ParsedModelFinder, + value: Parameters[0], + opt?: unknown + ): ReturnType | number | NullUndefined; + + // This methods is also responsible for determining whether this + // coordinate system is applicable to the given `finder`. + // Each coordinate system will be tried, until one returns non- + // null/undefined value. + convertToLayout?( + ecModel: GlobalModel, + finder: ParsedModelFinder, + value: Parameters[0], + opt?: unknown + ): ReturnType | NullUndefined; // This methods is also responsible for determining whether this // coordinate system is applicable to the given `finder`. // Each coordinate system will be tried, until one returns non- // null/undefined value. convertFromPixel?( - ecModel: GlobalModel, finder: ParsedModelFinder, pixelValue: number | number[] - ): number | number[]; + ecModel: GlobalModel, + finder: ParsedModelFinder, + pixelValue: Parameters[0], + opt?: unknown + ): ReturnType | NullUndefined; // @param point Point in global pixel coordinate system. // The signature of this method should be the same as `CoordinateSystemExecutive` @@ -113,25 +140,46 @@ export interface CoordinateSystem { /** * @param data * @param reserved Defined by the coordinate system itself - * @param out - * @return {Array.} point Point in global pixel coordinate system. + * @param out Fill it if passing, and return. For performance optimization. + * @return Point in global pixel coordinate system. + * An invalid returned point should be represented by `[NaN, NaN]`, + * rather than `null/undefined`. */ dataToPoint( - data: ScaleDataValue | ScaleDataValue[], - reserved?: any, + data: CoordinateSystemDataCoord, + opt?: unknown, out?: number[] ): number[]; + /** + * @param data See the meaning in `dataToPoint`. + * @param reserved Defined by the coordinate system itself + * @param out Fill it if passing, and return. For performance optimization. Vary by different coord sys. + * @return Layout in global pixel coordinate system. + * An invalid returned rect should be represented by `{x: NaN, y: NaN, width: NaN, height: NaN}`, + * Never return `null/undefined`. + */ + dataToLayout?( + data: CoordinateSystemDataCoord, + opt?: unknown, + out?: CoordinateSystemDataLayout + ): CoordinateSystemDataLayout; + /** * Some coord sys (like Parallel) might do not have `pointToData`, * or the meaning of this kind of features is not clear yet. * @param point point Point in global pixel coordinate system. - * @param clamp Clamp range + * @param out Fill it if passing, and return. For performance optimization. * @return data + * An invalid returned data should be represented by `[NaN, NaN]` or `NaN`, + * rather than `null/undefined`, which represents not-applicable in `convertFromPixel`. + * Return `OrdinalNumber` in ordianal (category axis) case. + * Return timestamp in time axis. */ pointToData?( point: number[], - clamp?: boolean + opt?: unknown, + out?: number | number[] ): number | number[]; // @param point Point in global pixel coordinate system. diff --git a/src/coord/View.ts b/src/coord/View.ts index 60fbb0318f..14935ac3d5 100644 --- a/src/coord/View.ts +++ b/src/coord/View.ts @@ -30,7 +30,10 @@ import { CoordinateSystemMaster, CoordinateSystem } from './CoordinateSystem'; import GlobalModel from '../model/Global'; import { ParsedModelFinder, ParsedModelFinderKnown } from '../util/model'; import { parsePercent } from '../util/number'; -import type ExtensionAPI from '../core/ExtensionAPI'; +import { NullUndefined, RoamOptionMixin } from '../util/types'; +import { clone } from 'zrender/src/core/util'; +import ExtensionAPI from '../core/ExtensionAPI'; +import { clampByZoomLimit, ZoomLimit } from '../component/helper/roamHelper'; const v2ApplyTransform = vector.applyTransform; @@ -45,10 +48,7 @@ class View extends Transformable implements CoordinateSystemMaster, CoordinateSy readonly name: string; - zoomLimit: { - max?: number; - min?: number; - }; + zoomLimit: ZoomLimit; /** * Represents the transform brought by roam/zoom. @@ -65,17 +65,20 @@ class View extends Transformable implements CoordinateSystemMaster, CoordinateSy /** * This is a user specified point on the source, which will be * located to the center of the `View['_viewRect']`. - * The unit this the same as `View['_rect']`. + * The unit and the origin of this point is the same as that of `[View['_rect']`. */ private _center: number[]; + private _centerOption: RoamOptionMixin['center']; + private _zoom: number; /** * The rect of the source, where the measure is used by "data" and "center". * Has nothing to do with roam/zoom. * The unit is defined by the source. For example, - * for geo source the unit is lat/lng, - * for SVG source the unit is the same as the width/height defined in SVG. + * - for geo source the unit is 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 uiit is px. */ private _rect: BoundingRect; /** @@ -84,23 +87,35 @@ class View extends Transformable implements CoordinateSystemMaster, CoordinateSy */ private _viewRect: BoundingRect; - constructor(name?: string) { + private _opt: {ecModel: GlobalModel, api: ExtensionAPI} | NullUndefined; + + constructor( + name?: string, + opt?: { + // Only for backward compat. + ecModel: GlobalModel, + api: ExtensionAPI + } + ) { super(); this.name = name; + this._opt = opt; } setBoundingRect(x: number, y: number, width: number, height: number): BoundingRect { this._rect = new BoundingRect(x, y, width, height); + this._updateCenterAndZoom(); return this._rect; } - /** - * @return {module:zrender/core/BoundingRect} - */ getBoundingRect(): BoundingRect { return this._rect; } + /** + * If no need to transform `View['_rect']` to `View['_viewRect']`, the calling of + * `setViewRect` can be omitted. + */ setViewRect(x: number, y: number, width: number, height: number): void { this._transformTo(x, y, width, height); this._viewRect = new BoundingRect(x, y, width, height); @@ -126,33 +141,34 @@ class View extends Transformable implements CoordinateSystemMaster, CoordinateSy } /** - * Set center of view + * [NOTICE] + * The definition of this center has always been irrelevant to some other series center like + * 'series-pie.center' - this center is a point on the same coord sys as `View['_rect'].x/y`, + * rather than canvas viewport, and the unit is not necessarily pixel (e.g., in geo case). + * @see {View['_center']} for details. */ - setCenter(centerCoord: (number | string)[], api: ExtensionAPI): void { - if (!centerCoord) { - return; + setCenter( + centerCoord: RoamOptionMixin['center'] + ): void { + // #16904 introcuded 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 `View['_rect']` is not calculated based on the current canvas rect. Therefore the percentage + // value is changed to based on `View['_rect'].width/height` since v6. Under this definition, users + // can use '0%' to map the top-left of `View['_rect']` to the center of `View['_viewRect']`. + const opt = this._opt; + if (opt && opt.api && opt.ecModel && opt.ecModel.getShallow('legacyViewCoordSysCenterBase') && centerCoord) { + centerCoord = [ + parsePercent(centerCoord[0], opt.api.getWidth()), + parsePercent(centerCoord[1], opt.api.getWidth()) + ]; } - this._center = [ - parsePercent(centerCoord[0], api.getWidth()), - parsePercent(centerCoord[1], api.getHeight()) - ]; + + this._centerOption = clone(centerCoord); this._updateCenterAndZoom(); } setZoom(zoom: number): void { - zoom = zoom || 1; - - const zoomLimit = this.zoomLimit; - if (zoomLimit) { - if (zoomLimit.max != null) { - zoom = Math.min(zoomLimit.max, zoom); - } - if (zoomLimit.min != null) { - zoom = Math.max(zoomLimit.min, zoom); - } - } - this._zoom = zoom; - + this._zoom = clampByZoomLimit(zoom || 1, this.zoomLimit); this._updateCenterAndZoom(); } @@ -181,9 +197,19 @@ class View extends Transformable implements CoordinateSystemMaster, CoordinateSy } /** - * Remove roam + * Ensure this method is idempotent, since it should be called when + * every relevant prop (e.g. _centerOption/_zoom/_rect/_viewRect) changed. */ private _updateCenterAndZoom(): void { + const centerOption = this._centerOption; + const rect = this._rect; + if (centerOption && rect) { + this._center = [ + parsePercent(centerOption[0], rect.width, rect.x), + parsePercent(centerOption[1], rect.height, rect.y) + ]; + } + // Must update after view transform updated const rawTransformMatrix = this._rawTransformable.getLocalTransform(); const roamTransform = this._roamTransformable; @@ -282,19 +308,24 @@ class View extends Transformable implements CoordinateSystemMaster, CoordinateSy /** * Convert a (x, y) point to (lon, lat) data */ - pointToData(point: number[]): number[] { + pointToData(point: number[], reserved?: unknown, out?: number[]): number[] { + out = out || []; const invTransform = this.invTransform; return invTransform - ? v2ApplyTransform([], point, invTransform) - : [point[0], point[1]]; + ? v2ApplyTransform(out, point, invTransform) + : (out[0] = point[0], out[1] = point[1], out); } - convertToPixel(ecModel: GlobalModel, finder: ParsedModelFinder, value: number[]): number[] { + 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[] { + convertFromPixel( + ecModel: GlobalModel, finder: ParsedModelFinder, pixel: number[] + ): number[] { const coordSys = getCoordSys(finder); return coordSys === this ? coordSys.pointToData(pixel) : null; } diff --git a/src/coord/axisAlignTicks.ts b/src/coord/axisAlignTicks.ts index 504b3b7475..3ccdddbee4 100644 --- a/src/coord/axisAlignTicks.ts +++ b/src/coord/axisAlignTicks.ts @@ -131,8 +131,9 @@ export function alignScaleTicks( if (ticks[1] && (!isValueNice(interval) || getPrecisionSafe(ticks[1].value) > getPrecisionSafe(interval))) { warn( - // eslint-disable-next-line - `The ticks may be not readable when set min: ${axisModel.get('min')}, max: ${axisModel.get('max')} and alignTicks: true` + `The ticks may be not readable when set min: ${axisModel.get('min')}, max: ${axisModel.get('max')}` + + ` and alignTicks: true. (${axisModel.axis?.dim}AxisIndex: ${axisModel.componentIndex})`, + true ); } } diff --git a/src/coord/calendar/Calendar.ts b/src/coord/calendar/Calendar.ts index 3a0909a002..381a9400e6 100644 --- a/src/coord/calendar/Calendar.ts +++ b/src/coord/calendar/Calendar.ts @@ -28,12 +28,14 @@ import { LayoutOrient, ScaleDataValue, OptionDataValueDate, - SeriesOption, - SeriesOnCalendarOptionMixin + CoordinateSystemDataLayout, } from '../../util/types'; import { ParsedModelFinder, ParsedModelFinderKnown } from '../../util/model'; -import { CoordinateSystem, CoordinateSystemMaster } from '../CoordinateSystem'; -import SeriesModel from '../../model/Series'; +import { + CoordinateSystem, CoordinateSystemMaster, +} from '../CoordinateSystem'; +import { expandOrShrinkRect } from '../../util/graphic'; +import { injectCoordSysByOption, simpleCoordSysInjectionProvider } from '../../core/CoordinateSystem'; // (24*60*60*1000) const PROXIMATE_ONE_DAY = 86400000; @@ -81,8 +83,7 @@ export interface CalendarParsedDateInfo { date: Date } -export interface CalendarCellRect { - contentShape: RectLike +interface CalendarCellRect { center: number[] tl: number[] tr: number[] @@ -119,6 +120,7 @@ class Calendar implements CoordinateSystem, CoordinateSystemMaster { constructor(calendarModel: CalendarModel, ecModel: GlobalModel, api: ExtensionAPI) { this._model = calendarModel; + this._update(ecModel, api); } // Required in createListFromData getDimensionsInfo = Calendar.getDimensionsInfo; @@ -203,7 +205,7 @@ class Calendar implements CoordinateSystem, CoordinateSystemMaster { return this.getDateInfo(date); } - update(ecModel: GlobalModel, api: ExtensionAPI) { + private _update(ecModel: GlobalModel, api: ExtensionAPI) { this._firstDayOfWeek = +this._model.getModel('dayLabel').get('firstDay'); this._orient = this._model.get('orient'); @@ -250,7 +252,12 @@ class Calendar implements CoordinateSystem, CoordinateSystemMaster { */ // TODO Clamp of calendar is not same with cartesian coordinate systems. // It will return NaN if data exceeds. - dataToPoint(data: OptionDataValueDate | OptionDataValueDate[], clamp?: boolean) { + dataToPoint( + data: OptionDataValueDate | OptionDataValueDate[], + clamp?: boolean, + out?: number[] + ): number[] { + out = out || []; zrUtil.isArray(data) && (data = data[0]); clamp == null && (clamp = true); @@ -263,24 +270,22 @@ class Calendar implements CoordinateSystem, CoordinateSystemMaster { dayInfo.time >= range.start.time && dayInfo.time < range.end.time + PROXIMATE_ONE_DAY )) { - return [NaN, NaN]; + out[0] = out[1] = NaN; + return out; } const week = dayInfo.day; const nthWeek = this._getRangeInfo([range.start.time, date]).nthWeek; if (this._orient === 'vertical') { - return [ - this._rect.x + week * this._sw + this._sw / 2, - this._rect.y + nthWeek * this._sh + this._sh / 2 - ]; - + out[0] = this._rect.x + week * this._sw + this._sw / 2; + out[1] = this._rect.y + nthWeek * this._sh + this._sh / 2; } - - return [ - this._rect.x + nthWeek * this._sw + this._sw / 2, - this._rect.y + week * this._sh + this._sh / 2 - ]; + else { + out[0] = this._rect.x + nthWeek * this._sw + this._sw / 2; + out[1] = this._rect.y + week * this._sh + this._sh / 2; + } + return out; } @@ -294,42 +299,53 @@ class Calendar implements CoordinateSystem, CoordinateSystemMaster { return date && date.time; } + dataToLayout( + data: OptionDataValueDate | OptionDataValueDate[], + clamp?: boolean, + out?: CoordinateSystemDataLayout + ): CoordinateSystemDataLayout { + out = out || {} as CoordinateSystemDataLayout; + const rect = out.rect = out.rect || {} as RectLike; + const contentRect = out.contentRect = out.contentRect || {} as RectLike; + const point = this.dataToPoint(data, clamp); + + rect.x = point[0] - (this._sw) / 2; + rect.y = point[1] - (this._sh) / 2; + rect.width = this._sw; + rect.height = this._sh; + + BoundingRect.copy(contentRect, rect); + expandOrShrinkRect(contentRect, this._lineWidth / 2, true, true); + + return out; + } + /** * Convert a time date item to (x, y) four point. */ - dataToRect(data: OptionDataValueDate | OptionDataValueDate[], clamp?: boolean): CalendarCellRect { + dataToCalendarLayout( + data: OptionDataValueDate | OptionDataValueDate[], + clamp?: boolean, + ): CalendarCellRect { const point = this.dataToPoint(data, clamp); - return { - contentShape: { - x: point[0] - (this._sw - this._lineWidth) / 2, - y: point[1] - (this._sh - this._lineWidth) / 2, - width: this._sw - this._lineWidth, - height: this._sh - this._lineWidth - }, - center: point, - tl: [ point[0] - this._sw / 2, point[1] - this._sh / 2 ], - tr: [ point[0] + this._sw / 2, point[1] - this._sh / 2 ], - br: [ point[0] + this._sw / 2, point[1] + this._sh / 2 ], - bl: [ point[0] - this._sw / 2, point[1] + this._sh / 2 - ] - + ], }; } @@ -351,11 +367,20 @@ class Calendar implements CoordinateSystem, CoordinateSystemMaster { return this._getDateByWeeksAndDay(nthX, nthY - 1, range); } - convertToPixel(ecModel: GlobalModel, finder: ParsedModelFinder, value: ScaleDataValue | ScaleDataValue[]) { + convertToPixel( + ecModel: GlobalModel, finder: ParsedModelFinder, value: ScaleDataValue | ScaleDataValue[] + ) { const coordSys = getCoordSys(finder); return coordSys === this ? coordSys.dataToPoint(value) : null; } + convertToLayout( + ecModel: GlobalModel, finder: ParsedModelFinder, value: ScaleDataValue | ScaleDataValue[] + ) { + const coordSys = getCoordSys(finder); + return coordSys === this ? coordSys.dataToLayout(value) : null; + } + convertFromPixel(ecModel: GlobalModel, finder: ParsedModelFinder, pixel: number[]) { const coordSys = getCoordSys(finder); return coordSys === this ? coordSys.pointToData(pixel) : null; @@ -525,11 +550,13 @@ class Calendar implements CoordinateSystem, CoordinateSystemMaster { calendarModel.coordinateSystem = calendar; }); - ecModel.eachSeries(function (calendarSeries: SeriesModel) { - if (calendarSeries.get('coordinateSystem') === 'calendar') { - // Inject coordinate system - calendarSeries.coordinateSystem = calendarList[calendarSeries.get('calendarIndex') || 0]; - } + // Inject coordinate system + ecModel.eachComponent((mainType, componentModel) => { + injectCoordSysByOption({ + targetModel: componentModel, + coordSysType: 'calendar', + coordSysProvider: simpleCoordSysInjectionProvider, + }); }); return calendarList; } diff --git a/src/coord/calendar/CalendarModel.ts b/src/coord/calendar/CalendarModel.ts index 57b878e63b..82a838191d 100644 --- a/src/coord/calendar/CalendarModel.ts +++ b/src/coord/calendar/CalendarModel.ts @@ -36,6 +36,7 @@ import { } from '../../util/types'; import GlobalModel from '../../model/Global'; import Model from '../../model/Model'; +import { CoordinateSystemHostModel } from '../CoordinateSystem'; import tokens from '../../visual/tokens'; export interface CalendarMonthLabelFormatterCallbackParams { @@ -153,12 +154,14 @@ export interface CalendarOption extends ComponentOption, BoxLayoutOptionMixin { } } -class CalendarModel extends ComponentModel { +class CalendarModel extends ComponentModel implements CoordinateSystemHostModel { static type = 'calendar'; type = CalendarModel.type; coordinateSystem: Calendar; + static layoutMode = 'box' as const; + /** * @override */ @@ -186,7 +189,9 @@ class CalendarModel extends ComponentModel { static defaultOption: CalendarOption = { // zlevel: 0, - z: 2, + // As a most basic coord sys, `z` should be lower than + // other series and coord sys, such as, grid. + z: -50, left: 80, top: 60, diff --git a/src/coord/calendar/prepareCustom.ts b/src/coord/calendar/prepareCustom.ts index abd1436a9d..b17903dc48 100644 --- a/src/coord/calendar/prepareCustom.ts +++ b/src/coord/calendar/prepareCustom.ts @@ -17,7 +17,7 @@ * under the License. */ -import Calendar from './Calendar'; +import type Calendar from './Calendar'; import { OptionDataValueDate } from '../../util/types'; export default function calendarPrepareCustom(coordSys: Calendar) { @@ -41,8 +41,17 @@ export default function calendarPrepareCustom(coordSys: Calendar) { } }, api: { - coord: function (data: OptionDataValueDate, clamp?: boolean) { + coord: function ( + data: Parameters[0], + clamp?: Parameters[1] + ): ReturnType { return coordSys.dataToPoint(data, clamp); + }, + layout: function ( + data: Parameters[0], + clamp?: Parameters[1] + ): ReturnType { + return coordSys.dataToLayout(data, clamp); } } }; diff --git a/src/coord/cartesian/Cartesian2D.ts b/src/coord/cartesian/Cartesian2D.ts index a17694d398..bf833ece6a 100644 --- a/src/coord/cartesian/Cartesian2D.ts +++ b/src/coord/cartesian/Cartesian2D.ts @@ -160,8 +160,8 @@ class Cartesian2D extends Cartesian implements CoordinateSystem { return out; } - pointToData(point: number[], clamp?: boolean): number[] { - const out: number[] = []; + pointToData(point: number[], clamp?: boolean, out?: number[]): number[] { + out = out || []; if (this._invTransform) { return applyTransform(out, point, this._invTransform); } diff --git a/src/coord/cartesian/Grid.ts b/src/coord/cartesian/Grid.ts index 2c10825aa4..6583bb3ca1 100644 --- a/src/coord/cartesian/Grid.ts +++ b/src/coord/cartesian/Grid.ts @@ -24,7 +24,7 @@ */ import {isObject, each, indexOf, retrieve3, keys, map} from 'zrender/src/core/util'; -import {getLayoutRect, LayoutRect} from '../../util/layout'; +import {createBoxLayoutReference, getLayoutRect, LayoutRect} from '../../util/layout'; import { createScaleByModel, ifAxisCrossZero, @@ -50,13 +50,14 @@ import {CoordinateSystemMaster} from '../CoordinateSystem'; import { ScaleDataValue } from '../../util/types'; import SeriesData from '../../data/SeriesData'; import OrdinalScale from '../../scale/Ordinal'; -import { isCartesian2DSeries, findAxisModels } from './cartesianAxisHelper'; +import { findAxisModels, isCartesian2DInjectedAsDataCoordSys } from './cartesianAxisHelper'; import { CategoryAxisBaseOption, NumericAxisBaseOptionCommon } from '../axisCommonTypes'; import { AxisBaseModel } from '../AxisBaseModel'; import { isIntervalOrLogScale } from '../../scale/helper'; import { alignScaleTicks } from '../axisAlignTicks'; import IntervalScale from '../../scale/Interval'; import LogScale from '../../scale/Log'; +import { injectCoordSysByOption } from '../../core/CoordinateSystem'; import { BoundingRect } from 'zrender'; @@ -174,16 +175,12 @@ class Grid implements CoordinateSystemMaster { */ resize(gridModel: GridModel, api: ExtensionAPI, ignoreContainLabel?: boolean): void { - const boxLayoutParams = gridModel.getBoxLayoutParams(); const isContainLabel = !ignoreContainLabel && gridModel.get('containLabel'); - const gridRect = getLayoutRect( - boxLayoutParams, { - width: api.getWidth(), - height: api.getHeight() - }); - - this._rect = gridRect; + const layoutRef = createBoxLayoutReference(gridModel, api); + const gridRect = this._rect = getLayoutRect(gridModel.getBoxLayoutParams(), layoutRef.refContainer); + // PENDING: whether to support that if the input `coord` is out of the base coord sys, + // do not render anything. At present, the behavior is undefined. const axesList = this._axesList; @@ -491,7 +488,10 @@ class Grid implements CoordinateSystemMaster { }); ecModel.eachSeries(function (seriesModel) { - if (isCartesian2DSeries(seriesModel)) { + // If pie (or other similar series) use cartesian2d, the unionExtent logic below is + // wrong, therefore skip it temporarily. See also in `defaultAxisExtentFromData.ts`. + // TODO: support union extent in this case. + if (isCartesian2DInjectedAsDataCoordSys(seriesModel)) { const axesModelMap = findAxisModels(seriesModel); const xAxisModel = axesModelMap.xAxisModel; const yAxisModel = axesModelMap.yAxisModel; @@ -558,36 +558,40 @@ class Grid implements CoordinateSystemMaster { // Inject the coordinateSystems into seriesModel ecModel.eachSeries(function (seriesModel) { - if (!isCartesian2DSeries(seriesModel)) { - return; - } - - const axesModelMap = findAxisModels(seriesModel); - const xAxisModel = axesModelMap.xAxisModel; - const yAxisModel = axesModelMap.yAxisModel; + injectCoordSysByOption({ + targetModel: seriesModel, + coordSysType: 'cartesian2d', + coordSysProvider: coordSysProvider + }); - const gridModel = xAxisModel.getCoordSysModel(); + function coordSysProvider() { + const axesModelMap = findAxisModels(seriesModel); + const xAxisModel = axesModelMap.xAxisModel; + const yAxisModel = axesModelMap.yAxisModel; - if (__DEV__) { - if (!gridModel) { - throw new Error( - 'Grid "' + retrieve3( - xAxisModel.get('gridIndex'), - xAxisModel.get('gridId'), - 0 - ) + '" not found' - ); - } - if (xAxisModel.getCoordSysModel() !== yAxisModel.getCoordSysModel()) { - throw new Error('xAxis and yAxis must use the same grid'); + const gridModel = xAxisModel.getCoordSysModel(); + + if (__DEV__) { + if (!gridModel) { + throw new Error( + 'Grid "' + retrieve3( + xAxisModel.get('gridIndex'), + xAxisModel.get('gridId'), + 0 + ) + '" not found' + ); + } + if (xAxisModel.getCoordSysModel() !== yAxisModel.getCoordSysModel()) { + throw new Error('xAxis and yAxis must use the same grid'); + } } - } - const grid = gridModel.coordinateSystem as Grid; + const grid = gridModel.coordinateSystem as Grid; - seriesModel.coordinateSystem = grid.getCartesian( - xAxisModel.componentIndex, yAxisModel.componentIndex - ); + return grid.getCartesian( + xAxisModel.componentIndex, yAxisModel.componentIndex + ); + } }); return grids; diff --git a/src/coord/cartesian/GridModel.ts b/src/coord/cartesian/GridModel.ts index cb3b7e92d3..e7cad526e0 100644 --- a/src/coord/cartesian/GridModel.ts +++ b/src/coord/cartesian/GridModel.ts @@ -19,12 +19,16 @@ import ComponentModel from '../../model/Component'; -import { ComponentOption, BoxLayoutOptionMixin, ZRColor, ShadowOptionMixin } from '../../util/types'; +import { + ComponentOption, BoxLayoutOptionMixin, ZRColor, ShadowOptionMixin, +} from '../../util/types'; import Grid from './Grid'; import { CoordinateSystemHostModel } from '../CoordinateSystem'; import tokens from '../../visual/tokens'; -export interface GridOption extends ComponentOption, BoxLayoutOptionMixin, ShadowOptionMixin { +export interface GridOption + extends ComponentOption, BoxLayoutOptionMixin, ShadowOptionMixin { + mainType?: 'grid'; show?: boolean; diff --git a/src/coord/cartesian/cartesianAxisHelper.ts b/src/coord/cartesian/cartesianAxisHelper.ts index 6f977bf2a9..9c746aecb1 100644 --- a/src/coord/cartesian/cartesianAxisHelper.ts +++ b/src/coord/cartesian/cartesianAxisHelper.ts @@ -98,10 +98,21 @@ export function layout( return layout; } -export function isCartesian2DSeries(seriesModel: SeriesModel): boolean { +export function isCartesian2DDeclaredSeries(seriesModel: SeriesModel): boolean { return seriesModel.get('coordinateSystem') === 'cartesian2d'; } +/** + * Note: If pie (or other similar series) use cartesian2d, here + * option `seriesModel.get('coordinateSystem') === 'cartesian2d'` + * and `seriesModel.coordinateSystem !== cartesian2dCoordSysInstance` + * and `seriesModel.boxCoordinateSystem === cartesian2dCoordSysInstance`, + * the logic below is probably wrong, therefore skip it temporarily. + */ +export function isCartesian2DInjectedAsDataCoordSys(seriesModel: SeriesModel): boolean { + return seriesModel.coordinateSystem && seriesModel.coordinateSystem.type === 'cartesian2d'; +} + export function findAxisModels(seriesModel: SeriesModel): { xAxisModel: CartesianAxisModel; yAxisModel: CartesianAxisModel; diff --git a/src/coord/cartesian/defaultAxisExtentFromData.ts b/src/coord/cartesian/defaultAxisExtentFromData.ts index 736eafee91..ca4bdcbacf 100644 --- a/src/coord/cartesian/defaultAxisExtentFromData.ts +++ b/src/coord/cartesian/defaultAxisExtentFromData.ts @@ -20,7 +20,9 @@ import * as echarts from '../../core/echarts'; import { createHashMap, each, HashMap, hasOwn, keys, map } from 'zrender/src/core/util'; import SeriesModel from '../../model/Series'; -import { isCartesian2DSeries, findAxisModels } from './cartesianAxisHelper'; +import { + isCartesian2DDeclaredSeries, findAxisModels, isCartesian2DInjectedAsDataCoordSys +} from './cartesianAxisHelper'; import { getDataDimensionsOnAxis, unionAxisExtentFromData } from '../axisHelper'; import { AxisBaseModel } from '../AxisBaseModel'; import Axis from '../Axis'; @@ -50,7 +52,7 @@ echarts.registerProcessor(echarts.PRIORITY.PROCESSOR.FILTER + 10, { getTargetSeries: function (ecModel) { const seriesModelMap = createHashMap(); ecModel.eachSeries(function (seriesModel: SeriesModel) { - isCartesian2DSeries(seriesModel) && seriesModelMap.set(seriesModel.uid, seriesModel); + isCartesian2DDeclaredSeries(seriesModel) && seriesModelMap.set(seriesModel.uid, seriesModel); }); return seriesModelMap; }, @@ -71,7 +73,12 @@ function prepareDataExtentOnAxis( seriesRecords: SeriesRecord[] ): void { ecModel.eachSeries(function (seriesModel: SeriesModel) { - if (!isCartesian2DSeries(seriesModel)) { + // If pie (or other similar series) use cartesian2d, the logic below is + // probably wrong, therefore skip it temporarily. + // TODO: support union extent in this case. + // e.g. make a fake seriesData by series.coord/series.center, and it can be + // performed by data processing (such as, filter), and applied here. + if (!isCartesian2DInjectedAsDataCoordSys(seriesModel)) { return; } diff --git a/src/coord/geo/Geo.ts b/src/coord/geo/Geo.ts index 130d959163..62db6efca6 100644 --- a/src/coord/geo/Geo.ts +++ b/src/coord/geo/Geo.ts @@ -25,9 +25,10 @@ import { GeoJSONRegion, Region } from './Region'; import { GeoProjection, GeoResource, NameMap } from './geoTypes'; import GlobalModel from '../../model/Global'; import { ParsedModelFinder, ParsedModelFinderKnown, SINGLE_REFERRING } from '../../util/model'; -import GeoModel from './GeoModel'; +import type GeoModel from './GeoModel'; import { resizeGeoType } from './geoCreator'; import { warn } from '../../util/log'; +import type ExtensionAPI from '../../core/ExtensionAPI'; const GEO_DEFAULT_PARAMS: { [type in GeoResource['type']]: { @@ -79,9 +80,11 @@ class Geo extends View { nameMap?: NameMap; nameProperty?: string; aspectScale?: number; + api: ExtensionAPI; + ecModel: GlobalModel; } ) { - super(name); + super(name, {api: opt.api, ecModel: opt.ecModel}); this.map = map; @@ -215,32 +218,38 @@ class Geo extends View { } } - pointToData(point: number[]) { + pointToData(point: number[], reserved?: unknown, out?: number[]) { const projection = this.projection; if (projection) { // projection may return null point. point = projection.unproject(point); } - return point && this.pointToProjected(point); + // FIXME: if no `point`, should return [NaN, NaN], rather than undefined. + // null/undefined has special meaning in `convertFromPixel`. + return point && this.pointToProjected(point, out); } /** * Point to projected data. Same with pointToData when projection is used. */ - pointToProjected(point: number[]) { - return super.pointToData(point); + pointToProjected(point: number[], out?: number[]) { + return super.pointToData(point, 0, out); } projectedToPoint(projected: number[], noRoam?: boolean, out?: number[]) { return super.dataToPoint(projected, noRoam, out); } - convertToPixel(ecModel: GlobalModel, finder: ParsedModelFinder, value: number[]): number[] { + 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[] { + convertFromPixel( + ecModel: GlobalModel, finder: ParsedModelFinder, pixel: number[] + ): number[] { const coordSys = getCoordSys(finder); return coordSys === this ? coordSys.pointToData(pixel) : null; } diff --git a/src/coord/geo/GeoModel.ts b/src/coord/geo/GeoModel.ts index 215c47e208..72f0252bd6 100644 --- a/src/coord/geo/GeoModel.ts +++ b/src/coord/geo/GeoModel.ts @@ -36,7 +36,8 @@ import { StatesOptionMixin, Dictionary, CommonTooltipOption, - StatesMixinBase + StatesMixinBase, + PreserveAspectMixin } from '../../util/types'; import { GeoProjection, NameMap } from './geoTypes'; import GlobalModel from '../../model/Global'; @@ -79,8 +80,7 @@ export interface GeoTooltipFormatterParams { $vars: ['name'] } - -export interface GeoCommonOptionMixin extends RoamOptionMixin { +export interface GeoCommonOptionMixin extends RoamOptionMixin, PreserveAspectMixin { // Map name map: string; @@ -96,6 +96,10 @@ export interface GeoCommonOptionMixin extends RoamOptionMixin { // Like: `40` or `'50%'`. layoutSize?: number | string; + // Whether to clip by the viewRect (as a viewport, decided by + // `BoxLayoutOptionMixin` (or `layoutCenter`/`layoutSize`) and `PreserveAspectMixin`) + clip?: boolean + // Define left-top, right-bottom lng/lat coords to control view // For example, [ [180, 90], [-180, -90] ] // higher priority than center and zoom diff --git a/src/coord/geo/geoCreator.ts b/src/coord/geo/geoCreator.ts index 4247d394ae..d41f72b545 100644 --- a/src/coord/geo/geoCreator.ts +++ b/src/coord/geo/geoCreator.ts @@ -27,13 +27,13 @@ import MapSeries, { MapSeriesOption } from '../../chart/map/MapSeries'; import ExtensionAPI from '../../core/ExtensionAPI'; import { CoordinateSystemCreator } from '../CoordinateSystem'; import { NameMap } from './geoTypes'; -import { SeriesOption, SeriesOnGeoOptionMixin } from '../../util/types'; import { Dictionary } from 'zrender/src/core/types'; import type Model from '../../model/Model'; import type GlobalModel from '../../model/Global'; -import type SeriesModel from '../../model/Series'; import type ComponentModel from '../../model/Component'; import * as vector from 'zrender/src/core/vector'; +import { injectCoordSysByOption } from '../../core/CoordinateSystem'; +import { SINGLE_REFERRING } from '../../util/model'; import type { GeoJSONRegion } from './Region'; export type resizeGeoType = typeof resizeGeo; @@ -96,8 +96,9 @@ function resizeGeo(this: Geo, geoModel: ComponentModel[0]; - boxLayoutOption.aspect = aspect; - viewRect = layout.getLayoutRect(boxLayoutOption, { - width: viewWidth, - height: viewHeight - }); + viewRect = layout.getLayoutRect(boxLayoutOption, refContainer); + viewRect = layout.applyPreserveAspect(geoModel, viewRect, aspect); } this.setViewRect(viewRect.x, viewRect.y, viewRect.width, viewRect.height); - this.setCenter(geoModel.get('center'), api); + this.setCenter(geoModel.get('center')); this.setZoom(geoModel.get('zoom')); } @@ -184,7 +182,9 @@ class GeoCreator implements CoordinateSystemCreator { const mapName = geoModel.get('map'); const geo = new Geo(mapName + idx, mapName, zrUtil.extend({ - nameMap: geoModel.get('nameMap') + nameMap: geoModel.get('nameMap'), + api, + ecModel, }, getCommonGeoProperties(geoModel))); geo.zoomLimit = geoModel.get('scaleLimit'); @@ -202,13 +202,19 @@ class GeoCreator implements CoordinateSystemCreator { }); ecModel.eachSeries(function (seriesModel) { - const coordSys = seriesModel.get('coordinateSystem'); - if (coordSys === 'geo') { - const geoIndex = ( - seriesModel as SeriesModel - ).get('geoIndex') || 0; - seriesModel.coordinateSystem = geoList[geoIndex]; - } + injectCoordSysByOption({ + targetModel: seriesModel, + coordSysType: 'geo', + coordSysProvider() { + const geoModel = seriesModel.subType === 'map' + ? (seriesModel as MapSeries).getHostGeoModel() + : seriesModel.getReferringComponents( + 'geo', SINGLE_REFERRING + ).models[0] as GeoModel; + return geoModel && geoModel.coordinateSystem; + }, + allowNotFound: true, + }); }); // If has map series @@ -228,7 +234,9 @@ class GeoCreator implements CoordinateSystemCreator { }); const geo = new Geo(mapType, mapType, zrUtil.extend({ - nameMap: zrUtil.mergeAll(nameMapList) + nameMap: zrUtil.mergeAll(nameMapList), + api, + ecModel, }, getCommonGeoProperties(mapSeries[0]))); geo.zoomLimit = zrUtil.retrieve.apply(null, zrUtil.map(mapSeries, function (singleMapSeries) { diff --git a/src/coord/matrix/Matrix.ts b/src/coord/matrix/Matrix.ts new file mode 100644 index 0000000000..b974403e3a --- /dev/null +++ b/src/coord/matrix/Matrix.ts @@ -0,0 +1,630 @@ +/* +* 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 { RectLike } from 'zrender/src/core/BoundingRect'; +import type { CoordinateSystemDataLayout, NullUndefined, OrdinalNumber } from '../../util/types'; +import { + CoordinateSystem, CoordinateSystemMaster +} from '../CoordinateSystem'; +import GlobalModel from '../../model/Global'; +import ExtensionAPI from '../../core/ExtensionAPI'; +import MatrixModel, { + MatrixCoordRangeOption, + MatrixDimensionCellOption, MatrixDimensionLevelOption, MatrixDimensionModel +} from './MatrixModel'; +import { LayoutRect, getLayoutRect } from '../../util/layout'; +import { ListIterator, ParsedModelFinder, ParsedModelFinderKnown } from '../../util/model'; +import { eqNaN, isArray, retrieve2 } from 'zrender/src/core/util'; +import Point from 'zrender/src/core/Point'; +import { WH, XY } from '../../util/graphic'; +import Model from '../../model/Model'; +import { + MatrixCellLayoutInfo, MatrixCellLayoutInfoType, + MatrixDimensionCell, MatrixDimPair, MatrixXYLocator +} from './MatrixDim'; +import { mathMax, mathMin, parsePositionSizeOption } from '../../util/number'; +import { + createNaNRectLike, + MatrixClampOption, + parseCoordRangeOption, + resetXYLocatorRange, + xyLocatorRangeToRectOneDim +} from './matrixCoordHelper'; +import { MatrixBodyCorner, MatrixBodyOrCornerKind } from './MatrixBodyCorner'; +import { error } from '../../util/log'; +import { injectCoordSysByOption, simpleCoordSysInjectionProvider } from '../../core/CoordinateSystem'; + + +class Matrix implements CoordinateSystem, CoordinateSystemMaster { + + static readonly dimensions = ['x', 'y', 'value']; + /** + * @see fetchers in `model/referHelper.ts`, + * which is used to parse data in ordinal way. + * In most series only 'x' and 'y' is required, + * but some series, such as heatmap, can specify value. + */ + static getDimensionsInfo() { + return [ + {name: 'x', type: 'ordinal' as const}, + {name: 'y', type: 'ordinal' as const}, + {name: 'value'}, + ]; + } + + readonly dimensions = Matrix.dimensions; + readonly type = 'matrix'; + + private _model: MatrixModel; + private _dimModels: { + x: MatrixDimensionModel; + y: MatrixDimensionModel; + }; + private _dims: MatrixDimPair; + + private _rect: LayoutRect; + + static create(ecModel: GlobalModel, api: ExtensionAPI) { + const matrixList: Matrix[] = []; + + ecModel.eachComponent('matrix', function (matrixModel: MatrixModel) { + const matrix = new Matrix(matrixModel, ecModel, api); + matrixList.push(matrix); + matrixModel.coordinateSystem = matrix; + }); + + // Inject coordinate system + // PENDING: optimize to not to travel all components? + // (collect relevant components in ecModel only when model update?) + ecModel.eachComponent((mainType, componentModel) => { + injectCoordSysByOption({ + targetModel: componentModel, + coordSysType: 'matrix', + coordSysProvider: simpleCoordSysInjectionProvider, + }); + }); + + return matrixList; + } + + constructor(matrixModel: MatrixModel, ecModel: GlobalModel, api: ExtensionAPI) { + this._model = matrixModel; + const models = this._dimModels = { + x: matrixModel.getDimensionModel('x'), + y: matrixModel.getDimensionModel('y'), + }; + this._dims = { + x: models.x.dim, + y: models.y.dim, + }; + + this._resize(matrixModel, api); + } + + getRect(): LayoutRect { + return this._rect; + } + + private _resize(matrixModel: MatrixModel, api: ExtensionAPI) { + const dims = this._dims; + const dimModels = this._dimModels; + + const rect = this._rect = getLayoutRect(matrixModel.getBoxLayoutParams(), { + width: api.getWidth(), + height: api.getHeight(), + }); + + layOutUnitsOnDimension(dimModels, dims, rect, 0); + layOutUnitsOnDimension(dimModels, dims, rect, 1); + + layOutDimCellsRestInfoByUnit(0, dims); + layOutDimCellsRestInfoByUnit(1, dims); + + layOutBodyCornerCellMerge(this._model.getBody(), dims); + layOutBodyCornerCellMerge(this._model.getCorner(), dims); + } + + /** + * @implement + * - The input is allowed to be `[NaN/null/undefined, xxx]`/`[xxx, NaN/null/undefined]`; + * the return is `[NaN, xxxresult]`/`[xxxresult, NaN]` or clamped boundary value if + * `clamp` passed. This is for the usage that only get coord on single x or y. + * - Alwasy return an numeric array, but never be null/undefined. + * If it can not be located or invalid, return `[NaN, NaN]`. + */ + dataToPoint( + data: MatrixCoordRangeOption[], + opt?: Parameters[1], + out?: number[] + ): number[] { + out = out || []; + + this.dataToLayout(data, opt, _dtpOutDataToLayout); + + out[0] = _dtpOutDataToLayout.rect.x + _dtpOutDataToLayout.rect.width / 2; + out[1] = _dtpOutDataToLayout.rect.y + _dtpOutDataToLayout.rect.height / 2; + return out; + } + + /** + * @implement + * - The input is allowed to be `[NaN/null/undefined, xxx]`/`[xxx, NaN/null/undefined]`; + * the return is `{x: NaN, width: NaN, y: xxxresulty, height: xxxresulth}`/ + * `{y: NaN, height: NaN, x: xxxresultx, width: xxxresultw}` or clamped boundary value + * if `clamp` passed. This is for the usage that only get coord on single x or y. + * - The returned `out.rect` and `out.matrixXYLocatorRange` is always an object or an 2d-array, + * but never be null/undefined. If it cannot be located or invalid, `NaN` is in their + * corresponding number props. + * - Do not provide `out.contentRect`, because it's allowed to input non-leaf dimension x/y or + * a range of x/y, which determines a rect covering multiple cells (even not merged), in which + * case the padding and borderWidth can not be determined to make a contentRect. Therefore only + * return `out.rect` in any case for consistency. The caller is responsible for adding space to + * avoid covering cell borders, if necessary. + */ + dataToLayout( + data: MatrixCoordRangeOption[], + opt?: { + // No clamp by default, considering the possibility of supporting dataZoom (overflow/scroll). + clamp?: MatrixClampOption | NullUndefined; + // Expand if cell merging is encountered. + // - `false`: If intersecting with a rect of merged cells, expand the result to cover it. + // This is the default option, becuase `series.data` do not support the format + // `MatrixCoordRangeOption` (e.g., `[[3,5], [5,8]]`), thus merged cells can only + // be located by single cell locators (e.g., `[3, 5]`). + // - `true`: regardless of cell merging, even if the resulting rect spans accorss the merged cells. + ignoreMergeCells?: boolean; + }, + out?: CoordinateSystemDataLayout + ): CoordinateSystemDataLayout { + const dims = this._dims; + + out = out || {} as CoordinateSystemDataLayout; + const outRect = out.rect = out.rect || {} as RectLike; + outRect.x = outRect.y = outRect.width = outRect.height = NaN; + const outLocRange = out.matrixXYLocatorRange = resetXYLocatorRange(out.matrixXYLocatorRange); + + if (!isArray(data)) { + if (__DEV__) { + error('Input data must be an array in `convertToLayout`, `convertToPixel`'); + } + return out; + } + + parseCoordRangeOption( + outLocRange, + null, + data, + dims, + retrieve2(opt && opt.clamp, MatrixClampOption.none) + ); + + if (!opt || !opt.ignoreMergeCells) { + if (!opt || opt.clamp !== MatrixClampOption.corner) { + this._model.getBody().expandRangeByCellMerge(outLocRange); + } + if (!opt || opt.clamp !== MatrixClampOption.body) { + this._model.getCorner().expandRangeByCellMerge(outLocRange); + } + } + + xyLocatorRangeToRectOneDim(outRect, outLocRange, dims, 0); + xyLocatorRangeToRectOneDim(outRect, outLocRange, dims, 1); + + return out; + } + + /** + * The returned locator pair can be the input of `dataToPoint` or `dataToLayout`. + * + * If point[0] is out of the matrix rect, + * the out[0] is NaN; + * else if it is on the right of top-left corner of body, + * the out[0] is the oridinal number (>= 0). + * else + * out[0] is the locator for corner or header (<= 0). + * + * The same rule goes for point[1] and out[1]. + * + * But point[0] and point[1] are calculated separately, i.e., + * the reuslt can be `[1, NaN]` or `[NaN, 1]` if only one dimension is out of boundary. + * + * @implement + */ + pointToData( + point: number[], + opt?: { + clamp?: MatrixClampOption | NullUndefined + }, + out?: MatrixXYLocator[] + ): MatrixXYLocator[] { + const dims = this._dims; + pointToDataOneDimPrepareCtx(_tmpCtxPointToData, 0, dims, point, opt && opt.clamp); + pointToDataOneDimPrepareCtx(_tmpCtxPointToData, 1, dims, point, opt && opt.clamp); + + out = out || []; + out[0] = out[1] = NaN; + + if (_tmpCtxPointToData.y === CtxPointToDataAreaType.inCorner + && _tmpCtxPointToData.x === CtxPointToDataAreaType.inBody + ) { + pointToDataOnlyHeaderFillOut(_tmpCtxPointToData, out, 0, dims); + } + else if (_tmpCtxPointToData.x === CtxPointToDataAreaType.inCorner + && _tmpCtxPointToData.y === CtxPointToDataAreaType.inBody + ) { + pointToDataOnlyHeaderFillOut(_tmpCtxPointToData, out, 1, dims); + } + else { + pointToDataBodyCornerFillOut(_tmpCtxPointToData, out, 0, dims); + pointToDataBodyCornerFillOut(_tmpCtxPointToData, out, 1, dims); + } + + return out; + } + + convertToPixel( + ecModel: GlobalModel, + finder: ParsedModelFinder, + value: Parameters[0], + opt?: Parameters[1], + ): ReturnType | NullUndefined { + const coordSys = getCoordSys(finder); + return coordSys === this ? coordSys.dataToPoint(value, opt) : undefined; + } + + convertToLayout( + ecModel: GlobalModel, + finder: ParsedModelFinder, + value: Parameters[0], + opt?: Parameters[1], + ): ReturnType | NullUndefined { + const coordSys = getCoordSys(finder); + return coordSys === this ? coordSys.dataToLayout(value, opt) : undefined; + } + + convertFromPixel( + ecModel: GlobalModel, + finder: ParsedModelFinder, + pixel: Parameters[0], + opt?: Parameters[1], + ): ReturnType | NullUndefined { + const coordSys = getCoordSys(finder); + return coordSys === this ? coordSys.pointToData(pixel, opt) : undefined; + } + + containPoint( + point: number[] + ): boolean { + return this._rect.contain(point[0], point[1]); + } + +} + +const _dtpOutDataToLayout = {rect: createNaNRectLike()}; +const _ptdLevelIt = new ListIterator(); +const _ptdDimCellIt = new ListIterator(); + + +function layOutUnitsOnDimension( + dimModels: Matrix['_dimModels'], + dims: MatrixDimPair, + matrixRect: RectLike, + dimIdx: number +): void { + const otherDimIdx = 1 - dimIdx; + const thisDim = dims[XY[dimIdx]]; + const otherDim = dims[XY[otherDimIdx]]; + // Notice: If matrix.x/y.show is false, still lay out, to ensure the + // consistent return of `dataToLayout`. + const otherDimShow = otherDim.shouldShow(); + + // Reset + for (const it = thisDim.resetCellIterator(); it.next();) { + it.item.wh = it.item.xy = NaN; + } + for (const it = otherDim.resetLayoutIterator(null, dimIdx); it.next();) { + it.item.wh = it.item.xy = NaN; + } + + // Set specified size from option. + let restSize = matrixRect[WH[dimIdx]]; + let restCellsCount = thisDim.getLocatorCount(dimIdx) + otherDim.getLocatorCount(dimIdx); + const tmpLevelModel = new Model(); + for (const it = otherDim.resetLevelIterator(); it.next();) { + // Consider `matrix.x.levelSize` and `matrix.x.levels[i].levelSize`. + tmpLevelModel.option = it.item.option; + tmpLevelModel.parentModel = dimModels[XY[otherDimIdx]]; + layOutSpecified(it.item, otherDimShow ? tmpLevelModel.get('levelSize') : 0); + } + const tmpCellModel = new Model(); + for (const it = thisDim.resetCellIterator(); it.next();) { + // Only leaf support size specification, to avoid unnecessary complexity. + if (it.item.type === MatrixCellLayoutInfoType.leaf) { + tmpCellModel.option = it.item.option; + tmpCellModel.parentModel = undefined; + layOutSpecified(it.item, tmpCellModel.get('size')); + } + } + function layOutSpecified(item: MatrixCellLayoutInfo, sizeOption: unknown): void { + const size = parseSizeOption(sizeOption, dimIdx, matrixRect); + if (!eqNaN(size)) { + item.wh = confineSize(size, restSize); + restSize = confineSize(restSize - item.wh); + restCellsCount--; + } + } + + // Set all sizes and positions to levels and leaf cells of which size is unspecified. + // Contents lay out based on matrix, rather than inverse; therefore do not support + // calculating size based on content, but allocate equally. + const computedCellWH = restCellsCount ? (restSize / restCellsCount) : 0; + // If all size specified, but some space remain (may also caused by matrix.x/y.show: false) + // do not align to the big most edge. + const notAlignToBigmost = !restCellsCount && restSize >= 1; // `1` for cumulative precision error. + let currXY = matrixRect[XY[dimIdx]]; + const maxLocator = thisDim.getLocatorCount(dimIdx) - 1; + const it = new ListIterator(); + + // Lay out levels of the perpendicular dim. + for (otherDim.resetLayoutIterator(it, dimIdx); it.next();) { + layOutUnspecified(it.item); + } + for (thisDim.resetLayoutIterator(it, dimIdx); it.next();) { + layOutUnspecified(it.item); + } + function layOutUnspecified(item: MatrixCellLayoutInfo) { + if (eqNaN(item.wh)) { + item.wh = computedCellWH; + } + item.xy = currXY; + if (item.id[XY[dimIdx]] === maxLocator && !notAlignToBigmost) { + // Align to the rightmost border, consider cumulative precision error. + item.wh = matrixRect[XY[dimIdx]] + matrixRect[WH[dimIdx]] - item.xy; + } + currXY += item.wh; + } +} + +function layOutDimCellsRestInfoByUnit(dimIdx: number, dims: MatrixDimPair): void { + // Finally save layout info based on the unit leaves and levels. + for (const it = dims[XY[dimIdx]].resetCellIterator(); it.next();) { + const dimCell = it.item; + layOutRectOneDimBasedOnUnit(dimCell.rect, dimIdx, dimCell.id, dimCell.span, dims); + // Consider level varitation on tree leaves, should extend the size to touch matrix body + // to avoid weird appearance. + layOutRectOneDimBasedOnUnit(dimCell.rect, 1 - dimIdx, dimCell.id, dimCell.span, dims); + + if (dimCell.type === MatrixCellLayoutInfoType.nonLeaf) { + // `xy` and `wh` need to be saved in non-leaf since it supports locating by non-leaf + // in `dataToPoint` or `dataToLayout`. + dimCell.xy = dimCell.rect[XY[dimIdx]]; + dimCell.wh = dimCell.rect[WH[dimIdx]]; + } + } +} + +function layOutBodyCornerCellMerge(bodyOrCorner: MatrixBodyCorner, dims: MatrixDimPair) { + bodyOrCorner.travelExistingCells(cell => { + const computedSpan = cell.span; + if (computedSpan) { + const layoutRect = cell.spanRect; + const id = cell.id; + layOutRectOneDimBasedOnUnit(layoutRect, 0, id, computedSpan, dims); + layOutRectOneDimBasedOnUnit(layoutRect, 1, id, computedSpan, dims); + } + }); +} + +// Save to rect for rendering. +function layOutRectOneDimBasedOnUnit( + outRect: RectLike, dimIdx: number, id: Point, span: Point, dims: MatrixDimPair +): void { + outRect[WH[dimIdx]] = 0; + const locator = id[XY[dimIdx]]; + const dim = locator < 0 ? dims[XY[1 - dimIdx]] : dims[XY[dimIdx]]; + const layoutUnit = dim.getUnitLayoutInfo(dimIdx, id[XY[dimIdx]]); + outRect[XY[dimIdx]] = layoutUnit.xy; + outRect[WH[dimIdx]] = layoutUnit.wh; + + if (span[XY[dimIdx]] > 1) { + const layoutUnit2 = dim.getUnitLayoutInfo(dimIdx, id[XY[dimIdx]] + span[XY[dimIdx]] - 1); + // Be careful the cumulative error - cell must be aligned. + outRect[WH[dimIdx]] = layoutUnit2.xy + layoutUnit2.wh - layoutUnit.xy; + } +} + +/** + * Return NaN if not defined or invalid. + */ +function parseSizeOption( + sizeOption: unknown, + dimIdx: number, + matrixRect: RectLike, +): number { + const sizeNum = parsePositionSizeOption(sizeOption, matrixRect[WH[dimIdx]]); + return confineSize(sizeNum, matrixRect[WH[dimIdx]]); +} + +function confineSize( + sizeNum: number, + sizeLimit?: number, +): number { + return Math.max(Math.min(sizeNum, retrieve2(sizeLimit, Infinity)), 0); +} + +function getCoordSys(finder: ParsedModelFinderKnown): Matrix { + const matrixModel = finder.matrixModel as MatrixModel; + const seriesModel = finder.seriesModel; + + const coordSys = matrixModel + ? matrixModel.coordinateSystem + : seriesModel + ? seriesModel.coordinateSystem + : null; + + return coordSys as Matrix; +} + +const CtxPointToDataAreaType = {inBody: 1, inCorner: 2, outside: 3}; +type CtxPointToDataAreaType = (typeof CtxPointToDataAreaType)[keyof typeof CtxPointToDataAreaType]; +type CtxPointToData = { + x: CtxPointToDataAreaType | NullUndefined; + y: CtxPointToDataAreaType | NullUndefined; + point: number[]; // If clamp required, this point is clamped after prepared. +}; +// For handy performance optimization in pointToData. +const _tmpCtxPointToData: CtxPointToData = {x: null, y: null, point: []}; + +function pointToDataOneDimPrepareCtx( + ctx: CtxPointToData, + dimIdx: number, + dims: MatrixDimPair, + point: number[], + clamp: MatrixClampOption | NullUndefined +) { + const thisDim = dims[XY[dimIdx]]; + const otherDim = dims[XY[1 - dimIdx]]; + + // Notice: considered cases: `matrix.x/y.show: false`, `matrix.x/y.data` is empty. + // In this cases the `layout.xy` is on the edge and `layout.wh` is `0`; they still can be + // use to calculate clampping. + + const bodyMaxUnit = thisDim.getUnitLayoutInfo(dimIdx, thisDim.getLocatorCount(dimIdx) - 1); + const body0Unit = thisDim.getUnitLayoutInfo(dimIdx, 0); + const cornerMinUnit = otherDim.getUnitLayoutInfo(dimIdx, -otherDim.getLocatorCount(dimIdx)); + const cornerMinus1Unit = otherDim.shouldShow() ? otherDim.getUnitLayoutInfo(dimIdx, -1) : null; + + let coord = ctx.point[dimIdx] = point[dimIdx]; // Transfer the oridinal coord. + + if (!body0Unit && !cornerMinus1Unit) { + ctx[XY[dimIdx]] = CtxPointToDataAreaType.outside; + return; + } + + if (clamp === MatrixClampOption.body) { + if (body0Unit) { + ctx[XY[dimIdx]] = CtxPointToDataAreaType.inBody; + coord = mathMin(bodyMaxUnit.xy + bodyMaxUnit.wh, mathMax(body0Unit.xy, coord)); + ctx.point[dimIdx] = coord; + } + else { + // If clamp to body, the result must not be in header. + ctx[XY[dimIdx]] = CtxPointToDataAreaType.outside; + } + return; + } + else if (clamp === MatrixClampOption.corner) { + if (cornerMinus1Unit) { + ctx[XY[dimIdx]] = CtxPointToDataAreaType.inCorner; + coord = mathMin(cornerMinus1Unit.xy + cornerMinus1Unit.wh, mathMax(cornerMinUnit.xy, coord)); + ctx.point[dimIdx] = coord; + } + else { + // If clamp to corner, the result must not be in body. + ctx[XY[dimIdx]] = CtxPointToDataAreaType.outside; + } + return; + } + + const pxLoc0 = body0Unit ? body0Unit.xy : cornerMinus1Unit ? cornerMinus1Unit.xy + cornerMinus1Unit.wh : NaN; + const pxMin = cornerMinUnit ? cornerMinUnit.xy : pxLoc0; + const pxMax = bodyMaxUnit ? bodyMaxUnit.xy + bodyMaxUnit.wh : pxLoc0; + + if (coord < pxMin) { + if (!clamp) { + // Quick pass for later calc, since mouse event on any place will enter this method if use `pointToData`. + ctx[XY[dimIdx]] = CtxPointToDataAreaType.outside; + return; + } + coord = pxMin; + } + else if (coord > pxMax) { + if (!clamp) { + ctx[XY[dimIdx]] = CtxPointToDataAreaType.outside; + return; + } + coord = pxMax; + } + ctx.point[dimIdx] = coord; // Save the updated coord. + + ctx[XY[dimIdx]] = pxLoc0 <= coord && coord <= pxMax ? CtxPointToDataAreaType.inBody + : pxMin <= coord && coord <= pxLoc0 ? CtxPointToDataAreaType.inCorner + : CtxPointToDataAreaType.outside; + + // Every props in ctx must be set in every branch of this method. +} + +// Assume partialOut has been set to NaN outside. +// This method may fill out[0] and out[1] in one call. +function pointToDataOnlyHeaderFillOut( + ctx: CtxPointToData, + partialOut: (OrdinalNumber | MatrixXYLocator)[], + dimIdx: number, + dims: MatrixDimPair, +): void { + const otherDimIdx = 1 - dimIdx; + + if (ctx[XY[dimIdx]] === CtxPointToDataAreaType.outside) { + return; + } + for (dims[XY[dimIdx]].resetCellIterator(_ptdDimCellIt); _ptdDimCellIt.next();) { + const cell = _ptdDimCellIt.item; + if (isCoordInRect(ctx.point[dimIdx], cell.rect, dimIdx) + && isCoordInRect(ctx.point[otherDimIdx], cell.rect, otherDimIdx) + ) { + // non-leaves are also allowed to be located. + // If the point is in x or y dimension cell area, should check both x and y coord to + // determine a cell; in this way a non-leaf cell can be determined. + partialOut[dimIdx] = cell.ordinal; + partialOut[otherDimIdx] = cell.id[XY[otherDimIdx]]; + return; + } + } +} + +// Assume partialOut has been set to NaN outside. +// This method may fill out[0] and out[1] in one call. +function pointToDataBodyCornerFillOut( + ctx: CtxPointToData, + partialOut: (OrdinalNumber | MatrixXYLocator)[], + dimIdx: number, + dims: MatrixDimPair, +): void { + if (ctx[XY[dimIdx]] === CtxPointToDataAreaType.outside) { + return; + } + const dim = ctx[XY[dimIdx]] === CtxPointToDataAreaType.inCorner + ? dims[XY[1 - dimIdx]] : dims[XY[dimIdx]]; + for (dim.resetLayoutIterator(_ptdLevelIt, dimIdx); _ptdLevelIt.next();) { + if (isCoordInLayoutInfo(ctx.point[dimIdx], _ptdLevelIt.item)) { + partialOut[dimIdx] = _ptdLevelIt.item.id[XY[dimIdx]]; + return; + } + } +} + +function isCoordInLayoutInfo(coord: number, cell: MatrixCellLayoutInfo): boolean { + return cell.xy <= coord && coord <= cell.xy + cell.wh; +} +function isCoordInRect(coord: number, rect: RectLike, dimIdx: number): boolean { + return rect[XY[dimIdx]] <= coord && coord <= rect[XY[dimIdx]] + rect[WH[dimIdx]]; +} + + +export default Matrix; diff --git a/src/coord/matrix/MatrixBodyCorner.ts b/src/coord/matrix/MatrixBodyCorner.ts new file mode 100644 index 0000000000..e0e3be8ac6 --- /dev/null +++ b/src/coord/matrix/MatrixBodyCorner.ts @@ -0,0 +1,291 @@ +/* +* 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 { HashMap, createHashMap, each, extend, isArray, isObject } from 'zrender/src/core/util'; +import { NullUndefined } from '../../util/types'; +import type { MatrixXYLocator, MatrixDimPair, MatrixXYLocatorRange } from './MatrixDim'; +import { error } from '../../util/log'; +import Point from 'zrender/src/core/Point'; +import { RectLike } from 'zrender/src/core/BoundingRect'; +import { MatrixBodyCornerCellOption, MatrixBodyOption, MatrixCornerOption } from './MatrixModel'; +import { + resolveXYLocatorRangeByCellMerge, + MatrixClampOption, + parseCoordRangeOption, + fillIdSpanFromLocatorRange, + createNaNRectLike, + isXYLocatorRangeInvalidOnDim, + resetXYLocatorRange, + cloneXYLocatorRange, +} from './matrixCoordHelper'; +import Model from '../../model/Model'; + + +/** + * Key: @see `makeCellMapKey` + */ +type MatrixModelBodyCornerCellMap = HashMap; + +export type MatrixBodyOrCornerKind = 'body' | 'corner'; +type MatrixBodyOrCornerOption = + ('body' extends TKind ? MatrixBodyOption : MatrixCornerOption); + +export interface MatrixBodyCornerCell { + // Represents col/row, serves as both id and locator. + // Actually its `x` is `xDimCell.id.x`; its `y` is `yDimCell.id.y` + id: Point; + // raw option in `matrix.body/corner.data[i]`. + option: MatrixBodyCornerCellOption | NullUndefined; + // `matrix.body/corner.data[i].coord` can locate a rect of cells (say, area). + // `inSpanOf` refers to the top-left cell, which represents that area. + // The top-left cell has `inSpanOf` refering to itself. + inSpanOf: MatrixBodyCornerCell | NullUndefined; + // If existing, it indicates cell merging, and this cell is the top-left cell + // of the merging area. + cellMergeOwner: boolean; + // Exist only if `cellMergeOwner: true`. + // In this case, it enusres that x > 1 and y > 1 and never out of boundary; + // othewise it is null/undefined. + span: Point | NullUndefined; + // Exist only if `cellMergeOwner: true`. + // Convey the same info with `id`+`span`, but be used in different calculation. + locatorRange: MatrixXYLocatorRange | NullUndefined; + // Exist only if `cellMergeOwner: true`. + spanRect: RectLike | NullUndefined; +} + +/** + * Lifetime: the same with `MatrixModel`, but different from `coord/Matrix`. + */ +export class MatrixBodyCorner { + + /** + * Be sparse, item exists only if needed. + */ + private _cellMap: MatrixModelBodyCornerCellMap | NullUndefined; + private _cellMergeOwnerList: MatrixBodyCornerCell[]; + + private _model: Model>; + private _dims: MatrixDimPair; + private _kind: TKind; + + constructor( + kind: TKind, + bodyOrCornerModel: Model>, + dims: MatrixDimPair + ) { + this._model = bodyOrCornerModel; + this._dims = dims; + this._kind = kind; + this._cellMergeOwnerList = []; + } + + /** + * Can not be called before series models initialization finished, since the ordinalMeta may + * use collect the values from `series.data` in series initialization. + */ + private _ensureCellMap(): MatrixModelBodyCornerCellMap { + const self = this; + + let _cellMap = self._cellMap; + if (!_cellMap) { + _cellMap = self._cellMap = createHashMap(); + fillCellMap(); + } + return _cellMap; + + function fillCellMap(): void { + type TmpParsed = { + id: Point; + span: Point; + locatorRange: MatrixXYLocatorRange; + option: MatrixBodyCornerCellOption; + cellMergeOwner: boolean; + }; + const parsedList: TmpParsed[] = []; + + let cellOptionList = self._model.getShallow('data'); + if (cellOptionList && !isArray(cellOptionList)) { + if (__DEV__) { + error(`matrix.${cellOptionList}.data must be an array if specified.`); + } + cellOptionList = null; + } + each(cellOptionList, (option, idx) => { + if (!isObject(option) || !isArray(option.coord)) { + if (__DEV__) { + error(`Illegal matrix.${self._kind}.data[${idx}], must be a {coord: [...], ...}`); + } + return; + } + + const locatorRange = resetXYLocatorRange([]); + let reasonArr: string[] | NullUndefined = null; + if (__DEV__) { + reasonArr = []; + } + parseCoordRangeOption( + locatorRange, reasonArr, option.coord, self._dims, MatrixClampOption[self._kind] + ); + if (isXYLocatorRangeInvalidOnDim(locatorRange, 0) || isXYLocatorRangeInvalidOnDim(locatorRange, 1)) { + if (__DEV__) { + error(`Can not determine cells by option matrix.${self._kind}.data[${idx}]: ` + + `${reasonArr.join(' ')}` + ); + } + return; + } + + const cellMergeOwner = option && option.mergeCells; + const parsed: TmpParsed = {id: new Point(), span: new Point(), locatorRange, option, cellMergeOwner}; + fillIdSpanFromLocatorRange(parsed, locatorRange); + + // The order of the `parsedList` determines the precedence of the styles, if there + // are overlaps between ranges specified in different items. Preserve the original + // order of `matrix.body/corner/data` to make it predictable for users. + parsedList.push(parsed); + }); + + // Resolve cell merging intersection - union to a larger rect. + const mergedMarkList: boolean[] = []; + for (let parsedIdx = 0; parsedIdx < parsedList.length; parsedIdx++) { + const parsed = parsedList[parsedIdx]; + if (!parsed.cellMergeOwner) { + continue; + } + const locatorRange = parsed.locatorRange; + resolveXYLocatorRangeByCellMerge(locatorRange, mergedMarkList, parsedList, parsedIdx); + for (let idx = 0; idx < parsedIdx; idx++) { + if (mergedMarkList[idx]) { + parsedList[idx].cellMergeOwner = false; + } + } + if (locatorRange[0][0] !== parsed.id.x || locatorRange[1][0] !== parsed.id.y) { + // The top-left cell of the unioned locatorRange is not this cell any more. + parsed.cellMergeOwner = false; + // Reconcile: simply use the last style and value option if multiple styles involved + // in a merged area, since there might be no commonly used merge strategy. + const newOption = extend({} as MatrixBodyCornerCellOption, parsed.option); + newOption.coord = null; + const newParsed: TmpParsed = { + id: new Point(), + span: new Point(), + locatorRange, + option: newOption, + cellMergeOwner: true + }; + fillIdSpanFromLocatorRange(newParsed, locatorRange); + parsedList.push(newParsed); + } + } + + // Assign options to cells. + each(parsedList, parsed => { + const topLeftCell = ensureBodyOrCornerCell(parsed.id.x, parsed.id.y); + if (parsed.cellMergeOwner) { + topLeftCell.cellMergeOwner = true; + topLeftCell.span = parsed.span; + topLeftCell.locatorRange = parsed.locatorRange; + topLeftCell.spanRect = createNaNRectLike(); + self._cellMergeOwnerList.push(topLeftCell); + } + if (!parsed.cellMergeOwner && !parsed.option) { + return; + } + for (let yidx = 0; yidx < parsed.span.y; yidx++) { + for (let xidx = 0; xidx < parsed.span.x; xidx++) { + const cell = ensureBodyOrCornerCell(parsed.id.x + xidx, parsed.id.y + yidx); + // If multiple style options are defined on a cell, the later ones takes precedence. + cell.option = parsed.option; + if (parsed.cellMergeOwner) { + cell.inSpanOf = topLeftCell; + } + } + } + }); + } // End of fillCellMap + + function ensureBodyOrCornerCell(x: MatrixXYLocator, y: MatrixXYLocator): MatrixBodyCornerCell { + const key = makeCellMapKey(x, y); + let cell = _cellMap.get(key); + if (!cell) { + cell = _cellMap.set(key, { + id: new Point(x, y), + option: null, + inSpanOf: null, + span: null, + spanRect: null, + locatorRange: null, + cellMergeOwner: false, + }); + } + return cell; + } + } + + /** + * Body cells or corner cell are not commonly defined specifically, especially in a large + * table, thus his is a sparse data structure - bodys or corner cells exist only if there + * are options specified to it (in `matrix.body.data` or `matrix.corner.data`); + * otherwise, return `NullUndefined`. + */ + getCell(xy: MatrixXYLocator[]): MatrixBodyCornerCell | NullUndefined { + // Assert xy do not contain NaN + return this._ensureCellMap().get(makeCellMapKey(xy[0], xy[1])); + } + + /** + * Only cell existing (has specific definition or props) will be travelled. + */ + travelExistingCells(cb: (cell: MatrixBodyCornerCell) => void): void { + this._ensureCellMap().each(cb); + } + + /** + * @param locatorRange Must be the return of `parseCoordRangeOption`. + */ + expandRangeByCellMerge(locatorRange: MatrixXYLocatorRange): void { + if ( + isXYLocatorRangeInvalidOnDim(locatorRange, 0) + && isXYLocatorRangeInvalidOnDim(locatorRange, 1) + && locatorRange[0][0] === locatorRange[0][1] + && locatorRange[1][0] === locatorRange[1][1] + ) { + // If it locates to a single cell, use this quick path to avoid travelling. + // It is based on the fact that any cell is not contained by more than one cell merging rect. + _tmpERBCMLocator[0] = locatorRange[0][0]; + _tmpERBCMLocator[1] = locatorRange[1][0]; + const cell = this.getCell(_tmpERBCMLocator); + const inSpanOf = cell.inSpanOf; + if (inSpanOf) { + cloneXYLocatorRange(locatorRange, inSpanOf.locatorRange); + return; + } + } + + const list = this._cellMergeOwnerList; + resolveXYLocatorRangeByCellMerge(locatorRange, null, list, list.length); + } + +} +const _tmpERBCMLocator: MatrixXYLocator[] = []; + +function makeCellMapKey(x: MatrixXYLocator, y: MatrixXYLocator): string { + return `${x}|${y}`; +} diff --git a/src/coord/matrix/MatrixDim.ts b/src/coord/matrix/MatrixDim.ts new file mode 100644 index 0000000000..a13b108271 --- /dev/null +++ b/src/coord/matrix/MatrixDim.ts @@ -0,0 +1,460 @@ +import { + createHashMap, + defaults, + each, eqNaN, isArray, isObject, isString, + retrieve2, +} from 'zrender/src/core/util'; +import Point from 'zrender/src/core/Point'; +import OrdinalMeta from '../../data/OrdinalMeta'; +import { NullUndefined, OrdinalNumber } from '../../util/types'; +import Ordinal from '../../scale/Ordinal'; +import { + MatrixDimensionModel, MatrixDimensionCellOption, MatrixDimensionCellLooseOption, + MatrixDimensionLevelOption, + MatrixCoordValueOption, +} from './MatrixModel'; +import { WH, XY } from '../../util/graphic'; +import { ListIterator } from '../../util/model'; +import { RectLike } from 'zrender/src/core/BoundingRect'; +import { + createNaNRectLike, setDimXYValue +} from './matrixCoordHelper'; +import { error } from '../../util/log'; +import { mathMax } from '../../util/number'; + + +export interface MatrixCellLayoutInfo { + type: MatrixCellLayoutInfoType; + // Represents col/row, serves as both id and locator. + // For a `MatrixDimensionCell`: + // it is `setDimXYValue(new Point(), dimIdx, firstLeafLocator, level - _levels.length)`. + // e.g., `id[dimIdx]` is `0` or `1` or `2` or `3` if there are at most `4` leaves in the tree. + // For a `MatrixDimensionLevelInfo`: + // it is `setDimXYValue(new Point(), dimIdx, 0, level - _levels.length)` + // e.g., `id[1 - dimIdx]` is `-3` or `-2` or `-1` if the tree has `3` levels. + // If negative, locate to corner cells; otherwise, locate to body cells. + id: Point; + // By pixel. Computed left-top x (for x dimension) or y (for y dimension). + // Used to locate. + xy: number; + // By pixel. Computed height (for x dimension) or width (for y dimension). + // Used to locate. + wh: number; + dim: MatrixDim; +} + +export const MatrixCellLayoutInfoType = { + level: 1, + leaf: 2, + nonLeaf: 3, +} as const; +export type MatrixCellLayoutInfoType = (typeof MatrixCellLayoutInfoType)[keyof typeof MatrixCellLayoutInfoType]; + +export type MatrixXYLocator = MatrixCellLayoutInfo['id']['x'] | MatrixCellLayoutInfo['id']['y']; +/** + * [[xmin, xmax], [ymin, ymax]] + * For each internal value, be NaN if invalid or out of boundary (never be null/undefined), + * otherwise must be valid locators. + * @see parseCoordRangeOption + * @see resetXYLocatorRange + */ +export type MatrixXYLocatorRange = MatrixXYLocator[][] & {__brand: 'MatrixXYLocatorRange'}; +/** + * [[xmin, xmax], [ymin, ymax]], be `NullUndefined` if illegal. + */ +export type MatrixXYCellLayoutInfoRange = (MatrixCellLayoutInfo | NullUndefined)[][]; + +export interface MatrixDimensionCell extends MatrixCellLayoutInfo { + // Computed col/row span. Always exists and >= 1. + // `span[XY[dimIdx]]` is actually the leaves count of this subtree. + span: Point; + // Start from 0, tree depth. + level: number; + // It is both `MatrixXYLocator` and `OrdinalNumber` and _cell[index]. + firstLeafLocator: MatrixXYLocator; + // Used to fatch its raw value by `matrixDim.getOrdinalMeta().category[ordinal]`, + // or feach cell by `_cells[ordinal]`. + // The ordinal of the leaf nodes is the same as `id.getOnDim(0)` for quick query, + // but not the same for non-leaf nodes. + ordinal: OrdinalNumber; + // Normalized raw option of `matrix.x/y.data[i]`, not include any parents option. + // Never be NullUndefined + option: MatrixDimensionCellOption; + // The layout rect for rendering. Available after matrix coordinate system resizing. + rect: RectLike; +} + +/** + * Computed properties of a certain tree level. + * In most cases this is used to describe level size or locate corner cells. + */ +export interface MatrixDimensionLevelInfo extends MatrixCellLayoutInfo { + // The raw option of `matrix.levels[i]` + option: MatrixDimensionLevelOption | NullUndefined; +} + +export type MatrixDimPair = { + x: MatrixDim; + y: MatrixDim; +}; + +/** + * Lifetime: the same with `MatrixModel`, but different from `coord/Matrix`. + */ +export class MatrixDim { + // Use it to visit `cell.id` and `cell.span` + readonly dim: 'x' | 'y'; + // Must be `0 | 1`, corresponding to 'x' | 'y' + readonly dimIdx: number; + + // Under the current definition, every leave corresponds a unit cell, + // and leaves can serve as the locator of cells. + // Therefore make sure: + // - The first `_leavesCount` elements in `_cells` are leaves. + // - `_cells[leaf.id[XY[this.dimIdx]]]` is the leaf itself. + // - Leaves of each subtree are placed together, that is, the leaves of a dimCell are: + // `this._cells.slice(dimCell.firstLeafLocator, dimCell.span[XY[this.dimIdx]])` + private _cells: MatrixDimensionCell[] = []; + + // Can be visited by `_levels[cell.level]` or `_levels[cell.id[1 - dimIdx] + _levels.length]`. + // Items are never be null/undefined after initialized. + private _levels: MatrixDimensionLevelInfo[] = []; + + private _leavesCount: number; + + private _model: MatrixDimensionModel; + private _ordinalMeta: OrdinalMeta; + // Only for uniformly parsing. + private _scale: Ordinal; + + private _uniqueValueGen: ReturnType; + + constructor(dim: 'x' | 'y', dimModel: MatrixDimensionModel) { + this.dim = dim; + this.dimIdx = dim === 'x' ? 0 : 1; + this._model = dimModel; + + this._uniqueValueGen = createUniqueValueGenerator(dim); + + let dimModelData = dimModel.get('data', true); + if (dimModelData != null && !isArray(dimModelData)) { + if (__DEV__) { + error(`Illegal echarts option - matrix.${this.dim}.data must be an array if specified.`); + } + dimModelData = []; + } + if (dimModelData) { + this._initByDimModelData(dimModelData); + } + else { + this._initBySeriesData(); + } + } + + private _initByDimModelData(dimModelData: MatrixDimensionCellLooseOption[]) { + const self = this; + const _cells = self._cells; + const _levels = self._levels; + const sameLocatorCellsLists: MatrixDimensionCell[][] = []; // Save for sorting. + let _cellCount = 0; + + self._leavesCount = traverseInitCells(dimModelData, 0, 0); + + postInitCells(); + + return; + + function traverseInitCells( + dimModelData: MatrixDimensionCellLooseOption[] | NullUndefined, + firstLeafLocator: number, + level: number + ): number { + let totalSpan = 0; + if (!dimModelData) { + return totalSpan; + } + + each(dimModelData, (option, optionIdx) => { + let invalidOption = false; + let cellOption: MatrixDimensionCell['option']; + if (isString(option)) { + cellOption = {value: option}; + } + else if (isObject(option)) { + cellOption = option; + if (option.value != null && !isString(option.value)) { + invalidOption = true; + cellOption = {value: null}; + } + } + else { + cellOption = {value: null}; + if (option != null) { + invalidOption = true; + } + } + + if (invalidOption) { + if (__DEV__) { + error(`Illegal echarts option - matrix.${self.dim}.data[${optionIdx}]` + + ' must be `string | {value: string}`.' + ); + } + } + + const cell: MatrixDimensionCell = { + type: MatrixCellLayoutInfoType.nonLeaf, // Update to leaf later if it's a leaf. + ordinal: NaN, // Set it later. + level, + firstLeafLocator, + id: new Point(), // Set it in `_initCellsId`. + span: setDimXYValue(new Point(), self.dimIdx, 1, 1), + option: cellOption, + xy: NaN, + wh: NaN, + dim: self, + rect: createNaNRectLike(), + }; + _cellCount++; + (sameLocatorCellsLists[firstLeafLocator] + || (sameLocatorCellsLists[firstLeafLocator] = []) + ).push(cell); + + if (!_levels[level]) { + // Create a level only if at least one cell exists. + _levels[level] = { + type: MatrixCellLayoutInfoType.level, + xy: NaN, wh: NaN, option: null, id: new Point(), dim: self + }; + } + + const childrenSpan = traverseInitCells( + cellOption.children, firstLeafLocator, level + 1 + ); + const subSpan = Math.max(1, childrenSpan); + cell.span[XY[self.dimIdx]] = subSpan; + + totalSpan += subSpan; + firstLeafLocator += subSpan; + }); + + return totalSpan; + } + + function postInitCells() { + // Sort to make sure the leaves are at the beginning, so that + // they can be used as the locator of body cells. + const categories: (string | NullUndefined)[] = []; + while (_cells.length < _cellCount) { + for (let locator = 0; locator < sameLocatorCellsLists.length; locator++) { + const cell = sameLocatorCellsLists[locator].pop(); + if (cell) { + cell.ordinal = categories.length; + const val = cell.option.value; + categories.push(val); + _cells.push(cell); + self._uniqueValueGen.calcDupBase(val); + } + } + } + self._uniqueValueGen.ensureValueUnique(categories, _cells); + + const ordinalMeta = self._ordinalMeta = new OrdinalMeta({ + categories: categories, + needCollect: false, + deduplication: false, + }); + self._scale = new Ordinal({ordinalMeta}); + + for (let idx = 0; idx < self._leavesCount; idx++) { + const leaf = self._cells[idx]; + leaf.type = MatrixCellLayoutInfoType.leaf; + // Handle the tree level variation: enlarge the span of the leaves to reach the body cells. + leaf.span[XY[1 - self.dimIdx]] = self._levels.length - leaf.level; + } + + self._initCellsId(); + self._initLevelIdOptions(); + } + } + + private _initBySeriesData() { + const self = this; + self._leavesCount = 0; + self._levels = [{ + type: MatrixCellLayoutInfoType.level, + xy: NaN, wh: NaN, option: null, id: new Point(), dim: self + }]; + self._initLevelIdOptions(); + + const ordinalMeta = self._ordinalMeta = new OrdinalMeta({ + needCollect: true, + deduplication: true, + onCollect: (value: unknown, ordinalNumber: number): void => { + const cell = self._cells[ordinalNumber] = { + type: MatrixCellLayoutInfoType.leaf, + ordinal: ordinalNumber, + level: 0, + firstLeafLocator: ordinalNumber, + id: new Point(), // Set it in `_initCellsId`. + span: setDimXYValue(new Point(), self.dimIdx, 1, 1), + // Theoretically `value` is from `dataset` or `series.data`, so it may be any type. + // Do not restrict this case for user's convenience, and here simply convert it to + // string for display. + option: {value: value + ''}, + xy: NaN, + wh: NaN, + dim: self, + rect: createNaNRectLike(), + }; + self._leavesCount++; + self._setCellId(cell); + }, + }); + self._scale = new Ordinal({ordinalMeta}); + } + + private _setCellId(cell: MatrixDimensionCell) { + const levelsLen = this._levels.length; + const dimIdx = this.dimIdx; + setDimXYValue(cell.id, dimIdx, cell.firstLeafLocator, cell.level - levelsLen); + } + + private _initCellsId() { + const levelsLen = this._levels.length; + const dimIdx = this.dimIdx; + each(this._cells, cell => { + setDimXYValue(cell.id, dimIdx, cell.firstLeafLocator, cell.level - levelsLen); + }); + } + + private _initLevelIdOptions() { + const levelsLen = this._levels.length; + const dimIdx = this.dimIdx; + let levelOptionList = this._model.get('levels', true); + levelOptionList = isArray(levelOptionList) ? levelOptionList : []; + + each(this._levels, (levelCfg, level) => { + setDimXYValue(levelCfg.id, dimIdx, 0, level - levelsLen); + levelCfg.option = levelOptionList[level]; + }); + } + + shouldShow(): boolean { + return !!this._model.getShallow('show', true); + } + + /** + * Iterate leaves (they are layout units) if dimIdx === this.dimIdx. + * Iterate levels if dimIdx !== this.dimIdx. + */ + resetLayoutIterator( + it: ListIterator | NullUndefined, + dimIdx: number, + startLocator?: MatrixXYLocator | NullUndefined, + count?: number | NullUndefined, + ): ListIterator { + it = it || new ListIterator(); + if (dimIdx === this.dimIdx) { + const len = this._leavesCount; + const startIdx = startLocator != null ? Math.max(0, startLocator) : 0; + count = count != null ? Math.min(count, len) : len; + it.reset(this._cells, startIdx, startIdx + count); + } + else { + const len = this._levels.length; + // Corner locator is from `-this._levels.length` to `-1`. + const startIdx = startLocator != null ? Math.max(0, startLocator + len) : 0; + count = count != null ? Math.min(count, len) : len; + it.reset(this._levels, startIdx, startIdx + count); + } + return it; + } + + resetCellIterator( + it?: ListIterator + ): ListIterator { + return (it || new ListIterator()).reset(this._cells, 0); + } + + resetLevelIterator( + it?: ListIterator + ): ListIterator { + return (it || new ListIterator()).reset(this._levels, 0); + } + + getLayout(outRect: RectLike, dimIdx: number, locator: MatrixXYLocator): void { + const layout = this.getUnitLayoutInfo(dimIdx, locator); + outRect[XY[dimIdx]] = layout ? layout.xy : NaN; + outRect[WH[dimIdx]] = layout ? layout.wh : NaN; + } + + /** + * Get leaf cell or get level info. + * Should be able to return null/undefined if not found on x or y, thus input `dimIdx` is needed. + */ + getUnitLayoutInfo(dimIdx: number, locator: MatrixXYLocator): MatrixCellLayoutInfo | NullUndefined { + return dimIdx === this.dimIdx + ? (locator < this._leavesCount ? this._cells[locator] : undefined) + : this._levels[locator + this._levels.length]; + } + + /** + * Get dimension cell by data, including leaves and non-leaves. + */ + getCell(value: MatrixCoordValueOption): MatrixDimensionCell | NullUndefined { + const ordinal = this._scale.parse(value); + return eqNaN(ordinal) ? undefined : this._cells[ordinal]; + } + + /** + * Get leaf count or get level count. + */ + getLocatorCount(dimIdx: number): number { + return dimIdx === this.dimIdx ? this._leavesCount : this._levels.length; + } + + getOrdinalMeta(): OrdinalMeta { + return this._ordinalMeta; + } + +} + +function createUniqueValueGenerator(dim: 'x' | 'y') { + const dimUpper = dim.toUpperCase(); + const defaultValReg = new RegExp(`^${dimUpper}([0-9]+)$`); + let dupBase = 0; + + function calcDupBase(val: string | NullUndefined): void { + let matchResult; + if (val != null && (matchResult = val.match(defaultValReg))) { + dupBase = mathMax(dupBase, +matchResult[1] + 1); + } + } + + function makeUniqueValue(): string { + return `${dimUpper}${dupBase++}`; + } + + // Duplicated value is allowed, because the `matrix.x/y.data` can be a tree and it's reasonable + // that leaves in different subtrees has the same text. But only the first one is allowed to be + // queried by the text, and the other ones can only be queried by index. + // Additionally, `matrix.x/y.data: [null, null, ...]` is allowed. + function ensureValueUnique(categories: (string | NullUndefined)[], cells: MatrixDimensionCell[]): void { + // A simple way to deduplicate or handle illegal or not specified values to avoid unexpected behaviors. + // The tree structure should not be broken even if duplicated. + const cateMap = createHashMap(); + for (let idx = 0; idx < categories.length; idx++) { + let value = categories[idx]; + // value may be set to NullUndefined by users or if illegal. + if (value == null || cateMap.get(value) != null) { + // Still display the original option.value if duplicated, but loose the ability to query by text. + categories[idx] = value = makeUniqueValue(); + cells[idx].option = defaults({value}, cells[idx].option); + } + cateMap.set(value, true); + } + } + + return {calcDupBase, ensureValueUnique}; +} diff --git a/src/coord/matrix/MatrixModel.ts b/src/coord/matrix/MatrixModel.ts new file mode 100644 index 0000000000..411f1d2f92 --- /dev/null +++ b/src/coord/matrix/MatrixModel.ts @@ -0,0 +1,333 @@ +/* +* 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 OrdinalMeta from '../../data/OrdinalMeta'; +import ComponentModel from '../../model/Component'; +import Model from '../../model/Model'; +import { + BoxLayoutOptionMixin, CommonTooltipOption, ComponentOption, ItemStyleOption, LabelOption, + LineStyleOption, + NullUndefined, OrdinalNumber, OrdinalRawValue, + PositionSizeOption +} from '../../util/types'; +import Matrix from './Matrix'; +import { MatrixDim, MatrixXYLocator } from './MatrixDim'; +import { MatrixBodyCorner } from './MatrixBodyCorner'; +import { CoordinateSystemHostModel } from '../CoordinateSystem'; +import tokens from '../../visual/tokens'; + + +export interface MatrixOption extends ComponentOption, BoxLayoutOptionMixin { + mainType?: 'matrix'; + x?: MatrixDimensionOption; + y?: MatrixDimensionOption; + body?: MatrixBodyOption; + corner?: MatrixCornerOption; + // Only for the matrix overall style, won't be inherited by x/y/coner/body. + backgroundStyle?: ItemStyleOption; + // Used on the outer border and the divider line. + borderZ2?: number; + tooltip?: CommonTooltipOption; + + // PENDING: do we need to support other states, i.e., `emphasis`, `blur`, `select`? +} + +interface MatrixBodyCornerBaseOption extends MatrixCellStyleOption { + /** + * Only specify some special cell definitions. + * It can represent both body cells and top-left corner cells. + * + * [body/corner cell locating]: + * The rule is uniformly applied, such as, in `matrix.dataToPoint` + * and `matrix.dataToLayout` and `xxxComponent.coord`. + * Suppose the matrix.x/y dimensions (header) are defined as: + * matrix: { + * x: [{ value: 'Xa0', children: ['Xb0', 'Xb1'] }, 'Xa1'], + * y: [{ value: 'Ya0', children: ['Yb0', 'Yb1'] }], + * } + * ----------------------------------------- + * | | | Xa0 | | + * |-------+-------+---------------| Xa1 | + * |cornerQ|cornerP| Xb0 | Xb1 | | + * |-------+-------+-------+-------+-------- + * | | Yb0 | bodyR | bodyS | | + * | Ya0 |-------+-------+---------------| + * | | Yb1 | | bodyT | + * |---------------|------------------------ + * "Locator number" (`MatrixXYLocator`): + * The term `locator` refers to a integer number to locate cells on x or y direction. + * Use the top-left cell of the body as the origin point (0, 0), + * the non-negative locator indicates the right/bottom of the origin point; + * the negative locator indicates the left/top of the origin point. + * "Ordinal number" (`OrdinalNumber`): + * This term follows the same meaning as that in category axis of cartesian. They are + * non-negative integer, designating each string `matrix.x.data[i].value`/`matrix.y.data[i].value`. + * 'Xb0', 'Xb2', 'Xa1', 'Xa0' are assigned with the ordinal numbers 0, 1, 2, 3. + * For every leaf dimension cell, `OrdinalNumber` and `MatrixXYLocator` is the same. + * + * A cell or pixel point or rect can be determined/located by a pair of `MatrixCoordValueOption`. + * See also `MatrixBodyCornerCellOption['coord']`. + * + * - The body cell `bodyS` above can be located by: + * - `coord: [1, 0]` (`MatrixXYLocator` or `OrdinalNumber`, which is a non-negative integer) + * - `coord: ['Xb1', 'Yb0']` + * - `coord: ['Xb1', 0]` (mix them) + * - The corner cell `cornerQ` above can be located by: + * - `coord: [-2, -1]` (negative `MatrixXYLocator`) + * - But it is NOT supported to use `coord: ['Y1_0', 'X1_0']` (XY transposed form) here. + * It's mathematically sound, but may introduce confusion and unnecessary + * complexity (consider the 'Xa1' case), and corner locating is not frequently used. + * - `mergeCells`: Body cells or corner cells can be merged, such as "bodyT" above, an input + * - The merging can be defined by: + * `matrix.data[i]: {coord: [['Xb1', 'Xa1'], 'Yb0'], mergeCells: true}`. + * - Input `['Xa1', 'Yb1']` to `dataToPoint` will get a point in the center of "bodyT". + * - Input `['Xa1', 'Yb1']` to `dataToLayout` will get a rect of the "bodyT". + * - If inputing a non-leaf dimension cell to locate, such as `['Xa0', 'Yb0']`, + * - it returns only according to the center of the dimension cells, regardless of the body span. + * (therefore, the result can be on the boundary of two body cells.) + * And the oridinal number assigned to 'Xa0' is 3, thus input `[3, 'Yb0']` get the some result. + * - The dimension (header) cell can be located by negative `MatrixXYLocator`. For example: + * - The center of the node 'Ya0' can be located by `[-2, 'Ya0']`. + */ + data?: MatrixBodyCornerCellOption[]; +} +export interface MatrixBodyOption extends MatrixBodyCornerBaseOption { +} +export interface MatrixCornerOption extends MatrixBodyCornerBaseOption { +} + +/** + * Commonly used as `MatrixCoordRangeOption[]` + * Can locate a cell or a rect range of cells. + * `[2, 8]` indicates a cell. + * `[2, null/undefined/NaN]` means y is the entire column. + * `[null/undefined/NaN, 8]` is the opposite. + * `[[2, 5], 8]` indicates a rect of cells in x range of `2~5` and y `8`. + * `[[2, 5], null/undefined/NaN]` indicates a x range of `2~5` and y is the entire column. + * `[[2, 5], [7, 8]]` indicates a rect of cells in x range of `2~5` and y range of `7~8`. + * `['aNonLeaf', 8]` indicates a rect of cells in x range of `aNonLeaf` and y `8`. + * @see {parseCoordRangeOption} + * @see {MatrixBodyCornerBaseOption['data']} + */ +export type MatrixCoordRangeOption = (MatrixCoordValueOption | MatrixCoordValueOption[] | NullUndefined); +/** + * `OrdinalRawValue` is originally provided by `matrix.x/y.data[i].value` or `series.data`. + */ +export type MatrixCoordValueOption = OrdinalRawValue | OrdinalNumber | MatrixXYLocator; + + +export interface MatrixBaseCellOption extends MatrixCellStyleOption { +} + +export interface MatrixBodyCornerCellOption extends MatrixBaseCellOption { + // Text that can be displayed. + value?: string; + // Use it to reference a coord in matrix. + coord?: MatrixCoordRangeOption[]; + // Merge cells determined by `coord`. + mergeCells?: boolean; +} + +interface MatrixDimensionOption extends MatrixCellStyleOption, MatrixDimensionLevelOption { + type?: 'category'; // For internal usage; force be 'category'. + show?: boolean; + data?: MatrixDimensionCellLooseOption[]; + // `levels[0]`: the topmost (for x dimension) or leftmost (for y dimension) level. + // If not specified, use null/undefined, such as `levels: [null, null, {levelSize: 10}]` + levels?: (MatrixDimensionLevelOption | NullUndefined)[]; + dividerLineStyle?: LineStyleOption; +} + +export interface MatrixDimensionCellOption extends MatrixBaseCellOption { + // Use it to define a coord in matrix. + // Do not use type `OrdinalRawValue` here. Number input is forbiden due to the possible confusion. + // e.g., if `matrix.x.data: [1, 2, 3]` is allowed, then querying `coord: [1, null]` will actually get + // the second column, since number represents the index (i.e. `MatrixXYLocator` or `OrdinalNumber`). + value?: string; + // column width (for x dimension) or row height (for y dimension). + // If not specified (null/undefined), auto calculate it. + // Only available on leaves, to avoid unnecessary complex. + // If it is a percentage, such as '30%', based on the matrix width/height, rather than the canvas size. + size?: PositionSizeOption; + children?: MatrixDimensionCellOption[]; +} +export type MatrixDimensionCellLooseOption = MatrixDimensionCellOption | MatrixDimensionCellOption['value']; + +export interface MatrixDimensionLevelOption { + // `matrix.levelSize` specifies the default size of every tree levels. + // `matrix.levels[i].levelSize` specifies the size of a certain level. + // For x dimension, that is height; for y dimension, that is width. + // If not specified (null/undefined), auto calculate it. + // If it is a percentage, such as '30%', based on the matrix width/height, rather than the canvas size. + levelSize?: PositionSizeOption; + // Other level specific options may added if needed, such as border-bottom/right style. +} + +export interface MatrixDimensionModel extends Model { +} + +/** + * Two levels of cascade inheritance: + * - priority-high: style options defined in `matrix.x/y/coner/body.data[i]` (in cell) + * - priority-low: style options defined in `matrix.x/y/coner/body` + */ +export interface MatrixCellStyleOption { + // [NOTE - padding]: + // - Consider the option for the space between cell boundary to text, percentage value for + // `label.width/height` is not supported, because it is not ideal - the padding would vary + // with the cell size, making the layout inconsistent. + // - The inner text padding uses `lable.style.padding`. + // The text truncation rect is obtained by cell rect minus by padding. + // - The inner series / other coord sys padding is not supported, to avoid necessary complexity. + // Consider some series, such as heatmap, prefer no padding. + label?: LabelOption; + itemStyle?: ItemStyleOption; + cursor?: string; + // By default, auto decide whether to be silent, considering tooltip. + silent?: boolean | NullUndefined; + // Used when style conflict - especially for thick border style. + z2?: number; +} + +export interface MatrixTooltipFormatterParams { + componentType: 'matrix' + matrixIndex: number + name: string + $vars: ['name', 'xyLocator'] +} + +const defaultLabelOption: LabelOption = { + show: true, + color: tokens.color.secondary, + // overflow: 'truncate', + overflow: 'break', + lineOverflow: 'truncate', + padding: [2, 3, 2, 3], + // Prefer to use `padding`, rather than distance. + distance: 0, +}; +function makeDefaultCellItemStyleOption(isCorner: boolean) { + return { + color: 'none', + borderWidth: 1, + borderColor: isCorner ? 'none' : tokens.color.borderTint, + }; +}; +const defaultDimOption: MatrixDimensionOption = { + show: true, + label: defaultLabelOption, + itemStyle: makeDefaultCellItemStyleOption(false), + silent: undefined, + dividerLineStyle: { + width: 1, + color: tokens.color.border, + }, +}; +const defaultBodyOption: MatrixBodyOption = { + label: defaultLabelOption, + itemStyle: makeDefaultCellItemStyleOption(false), + silent: undefined, +}; +const defaultCornerOption: MatrixCornerOption = { + label: defaultLabelOption, + itemStyle: makeDefaultCellItemStyleOption(true), + silent: undefined, +}; +const defaultMatrixOption: MatrixOption = { + // As a most basic coord sys, `z` should be lower than + // other series and coord sys, such as, grid. + z: -50, + left: '10%', + top: '10%', + right: '10%', + bottom: '10%', + x: defaultDimOption, + y: defaultDimOption, + body: defaultBodyOption, + corner: defaultCornerOption, + backgroundStyle: { + color: 'none', + borderColor: tokens.color.axisLine, + borderWidth: 1, + }, +}; + + +class MatrixModel extends ComponentModel implements CoordinateSystemHostModel { + static type = 'matrix'; + type = MatrixModel.type; + + coordinateSystem: Matrix; + + static layoutMode = 'box' as const; + + private _dimModels: { + x: MatrixDimensionModel; + y: MatrixDimensionModel; + }; + + private _body: MatrixBodyCorner<'body'>; + private _corner: MatrixBodyCorner<'corner'>; + + static defaultOption: MatrixOption = defaultMatrixOption; + + optionUpdated(): void { + // Simply re-create all to follow model changes. + + const dimModels = this._dimModels = { + // Do not use matrixModel as the parent model, for preventing from cascade-fetching options to it. + x: new MatrixDimensionModel(this.get('x', true) || {}), + y: new MatrixDimensionModel(this.get('y', true) || {}), + }; + + dimModels.x.option.type = dimModels.y.option.type = 'category'; + const xDim = dimModels.x.dim = new MatrixDim('x', dimModels.x); + const yDim = dimModels.y.dim = new MatrixDim('y', dimModels.y); + + const dims = {x: xDim, y: yDim}; + this._body = new MatrixBodyCorner( + 'body', new Model(this.getShallow('body')), dims + ); + this._corner = new MatrixBodyCorner( + 'corner', new Model(this.getShallow('corner')), dims + ); + } + + getDimensionModel(dim: 'x' | 'y'): MatrixDimensionModel { + return this._dimModels[dim]; + } + + getBody(): MatrixBodyCorner<'body'> { + return this._body; + } + + getCorner(): MatrixBodyCorner<'corner'> { + return this._corner; + } + +} + +export class MatrixDimensionModel extends Model { + dim: MatrixDim; + getOrdinalMeta(): OrdinalMeta { + return this.dim.getOrdinalMeta(); + } +} + +export default MatrixModel; diff --git a/src/coord/matrix/matrixCoordHelper.ts b/src/coord/matrix/matrixCoordHelper.ts new file mode 100644 index 0000000000..4605d74d2c --- /dev/null +++ b/src/coord/matrix/matrixCoordHelper.ts @@ -0,0 +1,363 @@ +/* +* 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 Point from 'zrender/src/core/Point'; +import { + MatrixCellLayoutInfoType, + MatrixCellLayoutInfo, + MatrixDimensionCell, + MatrixDimPair, + MatrixXYLocator, + MatrixXYLocatorRange, +} from './MatrixDim'; +import { NullUndefined } from '../../util/types'; +import { eqNaN, isArray, isNumber } from 'zrender/src/core/util'; +import { WH, XY } from '../../util/graphic'; +import { MatrixCoordRangeOption, MatrixCoordValueOption } from './MatrixModel'; +import { RectLike } from 'zrender/src/core/BoundingRect'; +import { mathMax, mathMin } from '../../util/number'; + + +/** + * @public Public to users in `chart.convertFromPixel`. + */ +export const MatrixClampOption = { + // No clamp, be falsy, equals to null/undefined. It means if the input part is + // null/undefined/NaN/outOfBoundary, the result part is NaN, rather than clamp to + // the boundary of the matrix. + none: 0, + // Clamp, where null/undefined/NaN/outOfBoundary can be used to cover the entire row/column. + all: 1, + body: 2, + corner: 3, +}; +export type MatrixClampOption = (typeof MatrixClampOption)[keyof typeof MatrixClampOption]; + +/** + * For the x direction, + * - find dimension cell from `xMatrixDim`, + * - If `xDimCell` or `yDimCell` is not a leaf, return the non-leaf cell itself. + * - otherwise find level from `yMatrixDim`. + * - otherwise return `NullUndefined`. + * + * For the y direction, it's the opposite. + */ +export function coordDataToAllCellLevelLayout( + coordValue: MatrixCoordValueOption, + dims: MatrixDimPair, + thisDimIdx: number // 0 | 1 +): MatrixCellLayoutInfo | NullUndefined { + // Find in body. + let result: MatrixCellLayoutInfo | NullUndefined = dims[XY[thisDimIdx]].getCell(coordValue); + // Find in corner or dimension area. + if (!result && isNumber(coordValue) && coordValue < 0) { + result = dims[XY[1 - thisDimIdx]].getUnitLayoutInfo(thisDimIdx, Math.round(coordValue)); + } + return result; +} + +export function resetXYLocatorRange(out: unknown[] | NullUndefined): MatrixXYLocatorRange { + const rg = (out || []) as MatrixXYLocatorRange; + rg[0] = rg[0] || []; + rg[1] = rg[1] || []; + rg[0][0] = rg[0][1] = rg[1][0] = rg[1][1] = NaN; + return rg; +} + +/** + * If illegal or out of boundary, set NaN to `locOut`. (See `isLocatorRangeInvalidOnDim`) + * x dimension and y dimension are calculated separately. + */ +export function parseCoordRangeOption( + locOut: MatrixXYLocatorRange, + // If illegal input or can not find any target, save reason to it. + // Do nothing if `NullUndefined`. + reasonOut: string[] | NullUndefined, + data: MatrixCoordRangeOption[], + dims: MatrixDimPair, + clamp: MatrixClampOption, +): void { + // x and y are supported to be handled separately - if one dimension is invalid + // (may be users do not need that), the other one should also be calculated. + parseCoordRangeOptionOnOneDim(locOut[0], reasonOut, clamp, data, dims, 0); + parseCoordRangeOptionOnOneDim(locOut[1], reasonOut, clamp, data, dims, 1); +} + +function parseCoordRangeOptionOnOneDim( + locDimOut: MatrixXYLocatorRange[number], + reasonOut: string[] | NullUndefined, + clamp: MatrixClampOption, + data: MatrixCoordRangeOption[], + dims: MatrixDimPair, + dimIdx: number, +): void { + locDimOut[0] = Infinity; + locDimOut[1] = -Infinity; + + const dataOnDim = data[dimIdx]; + const coordValArr = isArray(dataOnDim) ? dataOnDim : [dataOnDim]; + const len = coordValArr.length; + const hasClamp = !!clamp; + + if (len === 1 || len === 2) { + parseCoordRangeOptionOnOneDimOnePart(locDimOut, reasonOut, coordValArr, hasClamp, dims, dimIdx, 0); + if (len > 1) { + parseCoordRangeOptionOnOneDimOnePart(locDimOut, reasonOut, coordValArr, hasClamp, dims, dimIdx, 1); + } + } + else { + if (reasonOut) { + reasonOut.push(`Can only contain 1 or 2 coords, rather than ${len}.`); + } + locDimOut[0] = locDimOut[1] = NaN; + } + + if (hasClamp) { + // null/undefined/NaN or illegal data represents the entire row/column; + // Cover the entire locator regardless of body or corner, and confine it later. + let locLowerBound = -dims[XY[1 - dimIdx]].getLocatorCount(dimIdx); + let locUpperBound = dims[XY[dimIdx]].getLocatorCount(dimIdx) - 1; + + if (clamp === MatrixClampOption.body) { + locLowerBound = mathMax(0, locLowerBound); + } + else if (clamp === MatrixClampOption.corner) { + locUpperBound = mathMin(-1, locUpperBound); + } + + if (locUpperBound < locLowerBound) { // Also considered that both x and y has no cell. + locLowerBound = locUpperBound = NaN; + } + + if (eqNaN(locDimOut[0])) { + locDimOut[0] = locLowerBound; + } + if (eqNaN(locDimOut[1])) { + locDimOut[1] = locUpperBound; + } + locDimOut[0] = mathMax(mathMin(locDimOut[0], locUpperBound), locLowerBound); + locDimOut[1] = mathMax(mathMin(locDimOut[1], locUpperBound), locLowerBound); + } +} + +// The return val must be finite or NaN. +function parseCoordRangeOptionOnOneDimOnePart( + locDimOut: MatrixXYLocatorRange[number], + reasonOut: string[] | NullUndefined, + coordValArr: MatrixCoordValueOption[], + hasClamp: boolean, + dims: MatrixDimPair, + dimIdx: number, + partIdx: number, +): void { + const layout = coordDataToAllCellLevelLayout(coordValArr[partIdx], dims, dimIdx); + if (!layout) { + if (!hasClamp && reasonOut) { + reasonOut.push(`Can not find layout by coord[${dimIdx}][${partIdx}].`); + } + locDimOut[0] = locDimOut[1] = NaN; + return; + } + const locatorA = layout.id[XY[dimIdx]]; + let locatorB = locatorA; + const dimCell = cellLayoutInfoToDimCell(layout); + if (dimCell) { // Handle non-leaf + locatorB += dimCell.span[XY[dimIdx]] - 1; + } + locDimOut[0] = mathMin(locDimOut[0], locatorA, locatorB); + locDimOut[1] = mathMax(locDimOut[1], locatorA, locatorB); +} + +/** + * @param locatorRange Must be the return of `parseCoordRangeOption`, + * where if not NaN, it must be a valid locator. + */ +export function isXYLocatorRangeInvalidOnDim( + locatorRange: MatrixXYLocatorRange, dimIdx: number +): boolean { + return eqNaN(locatorRange[dimIdx][0]) || eqNaN(locatorRange[dimIdx][1]); +} + +// `locatorRange` will be expanded (modified) if an intersection is encountered. +export function resolveXYLocatorRangeByCellMerge( + inOutLocatorRange: MatrixXYLocatorRange, + // Item indices coorespond to mergeDefList (len: mergeDefListTravelLen). + // Indicating whether each item has be merged into the `locatorRange` + outMergedMarkList: boolean[] | NullUndefined, + mergeDefList: { + locatorRange: MatrixXYLocatorRange | NullUndefined; + cellMergeOwner: boolean; + }[], + mergeDefListTravelLen: number, +): void { + outMergedMarkList = outMergedMarkList || _tmpOutMergedMarkList; + for (let idx = 0; idx < mergeDefListTravelLen; idx++) { + outMergedMarkList[idx] = false; + } + // In most case, cell merging definition list length is smaller than the range extent, + // therefore, to detection intersection, travelling cell merging definition list is probably + // performant than traveling the four edges of the rect formed by the locator range. + while (true) { + let expanded = false; + for (let idx = 0; idx < mergeDefListTravelLen; idx++) { + const mergeDef = mergeDefList[idx]; + if (!outMergedMarkList[idx] + && mergeDef.cellMergeOwner + && expandXYLocatorRangeIfIntersect(inOutLocatorRange, mergeDef.locatorRange) + ) { + outMergedMarkList[idx] = true; + expanded = true; + } + } + if (!expanded) { + break; + } + } +} +const _tmpOutMergedMarkList: boolean[] = []; + +// Return whether intersect. +// `thisLocRange` will be expanded (modified) if an intersection is encountered. +function expandXYLocatorRangeIfIntersect( + thisLocRange: MatrixXYLocatorRange, + otherLocRange: MatrixXYLocatorRange +): boolean { + if (!locatorRangeIntersectOneDim(thisLocRange[0], otherLocRange[0]) + || !locatorRangeIntersectOneDim(thisLocRange[1], otherLocRange[1]) + ) { + return false; + } + + thisLocRange[0][0] = mathMin(thisLocRange[0][0], otherLocRange[0][0]); + thisLocRange[0][1] = mathMax(thisLocRange[0][1], otherLocRange[0][1]); + thisLocRange[1][0] = mathMin(thisLocRange[1][0], otherLocRange[1][0]); + thisLocRange[1][1] = mathMax(thisLocRange[1][1], otherLocRange[1][1]); + + return true; +} + +// Notice: If containing NaN, not intersect. +function locatorRangeIntersectOneDim( + locRange1OneDim: MatrixXYLocatorRange[number], + locRange2OneDim: MatrixXYLocatorRange[number], +): boolean { + return ( + locRange1OneDim[1] >= locRange2OneDim[0] + && locRange1OneDim[0] <= locRange2OneDim[1] + ); +} + +export function fillIdSpanFromLocatorRange( + owner: {id: Point; span: Point;}, + locatorRange: MatrixXYLocatorRange +): void { + owner.id.set(locatorRange[0][0], locatorRange[1][0]); + owner.span.set(locatorRange[0][1] - owner.id.x + 1, locatorRange[1][1] - owner.id.y + 1); +} + +export function cloneXYLocatorRange( + target: MatrixXYLocatorRange, + source: MatrixXYLocatorRange +): void { + target[0][0] = source[0][0]; + target[0][1] = source[0][1]; + target[1][0] = source[1][0]; + target[1][1] = source[1][1]; +} + +/** + * If illegal, the corresponding x/y/width/height is set to `NaN`. + * `x/width` or `y/height` is supported to be calculated separately, + * i.e., one side are NaN, the other side are normal. + * @param oneDimOut only write to `x/width` or `y/height`, depending on `dimIdx`. + */ +export function xyLocatorRangeToRectOneDim( + oneDimOut: RectLike, + locRange: MatrixXYLocatorRange, + dims: MatrixDimPair, + dimIdx: number +) { + const layoutMin = coordDataToAllCellLevelLayout(locRange[dimIdx][0], dims, dimIdx); + const layoutMax = coordDataToAllCellLevelLayout(locRange[dimIdx][1], dims, dimIdx); + + oneDimOut[XY[dimIdx]] = oneDimOut[WH[dimIdx]] = NaN; + + if (layoutMin && layoutMax) { + oneDimOut[XY[dimIdx]] = layoutMin.xy; + oneDimOut[WH[dimIdx]] = layoutMax.xy + layoutMax.wh - layoutMin.xy; + } +} + +// No need currently, since `span` is not allowed to be defined directly by users. +// /** +// * If either span x or y is valid and > 1, return parsed span, otherwise return `NullUndefined`. +// */ +// export function parseSpanOption( +// spanOptionHost: MatrixCellSpanOptionHost, +// dimCellPair: MatrixCellLayoutInfo[] +// ): Point | NullUndefined { +// const spanX = parseSpanOnDim(spanOptionHost.spanX, dimCellPair[0], 0); +// const spanY = parseSpanOnDim(spanOptionHost.spanY, dimCellPair[1], 1); +// if (!eqNaN(spanX) || !eqNaN(spanY)) { +// return new Point(spanX || 1, spanY || 1); +// } +// function parseSpanOnDim(spanOption: unknown, dimCell: MatrixCellLayoutInfo, dimIdx: number): number { +// if (!isNumber(spanOption)) { +// return NaN; +// } +// // Ensure positive integer (not NaN) to avoid dead loop. +// const span = mathMax(1, Math.round(spanOption || 1)) || 1; +// // Clamp, and consider may also be specified as `Infinity` to span the entire col/row. +// return mathMin(span, mathMax(1, dimCell.dim.getLocatorCount(dimIdx) - dimCell.id[XY[dimIdx]])); +// } +// } + +/** + * @usage To get/set on dimension, use: + * `xyVal[XY[dim]] = val;` // set on this dimension. + * `xyVal[XY[1 - dim]] = val;` // set on the perpendicular dimension. + */ +export function setDimXYValue( + out: Point, + dimIdx: number, // 0 | 1 + valueOnThisDim: MatrixXYLocator, + valueOnOtherDim: MatrixXYLocator +): Point { + out[XY[dimIdx]] = valueOnThisDim; + out[XY[1 - dimIdx]] = valueOnOtherDim; + return out; +} + +/** + * Return NullUndefined if not dimension cell. + */ +function cellLayoutInfoToDimCell( + cellLayoutInfo: MatrixCellLayoutInfo | NullUndefined +): MatrixDimensionCell | NullUndefined { + return ( + cellLayoutInfo && ( + cellLayoutInfo.type === MatrixCellLayoutInfoType.leaf + || cellLayoutInfo.type === MatrixCellLayoutInfoType.nonLeaf + ) + ) ? (cellLayoutInfo as MatrixDimensionCell) : null; +} + +export function createNaNRectLike(): RectLike { + return {x: NaN, y: NaN, width: NaN, height: NaN}; +} diff --git a/src/coord/matrix/prepareCustom.ts b/src/coord/matrix/prepareCustom.ts new file mode 100644 index 0000000000..b3ef772a92 --- /dev/null +++ b/src/coord/matrix/prepareCustom.ts @@ -0,0 +1,48 @@ +/* +* 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 type Matrix from './Matrix'; + +export default function matrixPrepareCustom(coordSys: Matrix) { + const rect = coordSys.getRect(); + + return { + coordSys: { + type: 'matrix', + x: rect.x, + y: rect.y, + width: rect.width, + height: rect.height + }, + api: { + coord: function ( + data: Parameters[0], + opt?: Parameters[1] + ): ReturnType { + return coordSys.dataToPoint(data, opt); + }, + layout: function ( + data: Parameters[0], + opt?: Parameters[1] + ): ReturnType { + return coordSys.dataToLayout(data, opt); + } + } + }; +} diff --git a/src/coord/parallel/Parallel.ts b/src/coord/parallel/Parallel.ts index c694f55ff1..5108bbf914 100644 --- a/src/coord/parallel/Parallel.ts +++ b/src/coord/parallel/Parallel.ts @@ -194,13 +194,8 @@ class Parallel implements CoordinateSystemMaster, CoordinateSystem { * Resize the parallel coordinate system. */ resize(parallelModel: ParallelModel, api: ExtensionAPI): void { - this._rect = layoutUtil.getLayoutRect( - parallelModel.getBoxLayoutParams(), - { - width: api.getWidth(), - height: api.getHeight() - } - ); + const refContainer = layoutUtil.createBoxLayoutReference(parallelModel, api).refContainer; + this._rect = layoutUtil.getLayoutRect(parallelModel.getBoxLayoutParams(), refContainer); this._layoutAxes(); } diff --git a/src/coord/polar/Polar.ts b/src/coord/polar/Polar.ts index 9dd73b4c7a..9667008298 100644 --- a/src/coord/polar/Polar.ts +++ b/src/coord/polar/Polar.ts @@ -138,22 +138,22 @@ class Polar implements CoordinateSystem, CoordinateSystemMaster { * Convert a single data item to (x, y) point. * Parameter data is an array which the first element is radius and the second is angle */ - dataToPoint(data: ScaleDataValue[], clamp?: boolean) { + dataToPoint(data: ScaleDataValue[], clamp?: boolean, out?: number[]) { return this.coordToPoint([ this._radiusAxis.dataToRadius(data[0], clamp), this._angleAxis.dataToAngle(data[1], clamp) - ]); + ], out); } /** * Convert a (x, y) point to data */ - pointToData(point: number[], clamp?: boolean) { + pointToData(point: number[], clamp?: boolean, out?: number[]) { + out = out || []; const coord = this.pointToCoord(point); - return [ - this._radiusAxis.radiusToData(coord[0], clamp), - this._angleAxis.angleToData(coord[1], clamp) - ]; + out[0] = this._radiusAxis.radiusToData(coord[0], clamp); + out[1] = this._angleAxis.angleToData(coord[1], clamp); + return out; } /** @@ -190,14 +190,15 @@ class Polar implements CoordinateSystem, CoordinateSystemMaster { /** * Convert a (radius, angle) coord to (x, y) point */ - coordToPoint(coord: number[]) { + coordToPoint(coord: number[], out?: number[]) { + out = out || []; const radius = coord[0]; const radian = coord[1] / 180 * Math.PI; - const x = Math.cos(radian) * radius + this.cx; + out[0] = Math.cos(radian) * radius + this.cx; // Inverse the y - const y = -Math.sin(radian) * radius + this.cy; + out[1] = -Math.sin(radian) * radius + this.cy; - return [x, y]; + return out; } /** @@ -245,12 +246,16 @@ class Polar implements CoordinateSystem, CoordinateSystemMaster { }; } - convertToPixel(ecModel: GlobalModel, finder: ParsedModelFinder, value: ScaleDataValue[]) { + convertToPixel( + ecModel: GlobalModel, finder: ParsedModelFinder, value: ScaleDataValue[] + ) { const coordSys = getCoordSys(finder); return coordSys === this ? this.dataToPoint(value) : null; } - convertFromPixel(ecModel: GlobalModel, finder: ParsedModelFinder, pixel: number[]) { + convertFromPixel( + ecModel: GlobalModel, finder: ParsedModelFinder, pixel: number[] + ) { const coordSys = getCoordSys(finder); return coordSys === this ? this.pointToData(pixel) : null; } diff --git a/src/coord/polar/polarCreator.ts b/src/coord/polar/polarCreator.ts index fe29f3ed18..224de9c3b0 100644 --- a/src/coord/polar/polarCreator.ts +++ b/src/coord/polar/polarCreator.ts @@ -40,20 +40,21 @@ import { SeriesOption } from '../../util/types'; import { SINGLE_REFERRING } from '../../util/model'; import { AxisBaseModel } from '../AxisBaseModel'; import { CategoryAxisBaseOption } from '../axisCommonTypes'; +import { createBoxLayoutReference } from '../../util/layout'; /** * Resize method bound to the polar */ function resizePolar(polar: Polar, polarModel: PolarModel, api: ExtensionAPI) { const center = polarModel.get('center'); - const width = api.getWidth(); - const height = api.getHeight(); - polar.cx = parsePercent(center[0], width); - polar.cy = parsePercent(center[1], height); + const refContainer = createBoxLayoutReference(polarModel, api).refContainer; + + polar.cx = parsePercent(center[0], refContainer.width) + refContainer.x; + polar.cy = parsePercent(center[1], refContainer.height) + refContainer.y; const radiusAxis = polar.getRadiusAxis(); - const size = Math.min(width, height) / 2; + const size = Math.min(refContainer.width, refContainer.height) / 2; let radius = polarModel.get('radius'); if (radius == null) { diff --git a/src/coord/radar/Radar.ts b/src/coord/radar/Radar.ts index 732eceb84a..6ce88ec75f 100644 --- a/src/coord/radar/Radar.ts +++ b/src/coord/radar/Radar.ts @@ -30,6 +30,7 @@ import { ScaleDataValue } from '../../util/types'; import { ParsedModelFinder } from '../../util/model'; import { map, each, isString, isNumber } from 'zrender/src/core/util'; import { alignScaleTicks } from '../axisAlignTicks'; +import { createBoxLayoutReference } from '../../util/layout'; class Radar implements CoordinateSystem, CoordinateSystemMaster { @@ -122,12 +123,12 @@ class Radar implements CoordinateSystem, CoordinateSystemMaster { } resize(radarModel: RadarModel, api: ExtensionAPI) { + const refContainer = createBoxLayoutReference(radarModel, api).refContainer; + const center = radarModel.get('center'); - const viewWidth = api.getWidth(); - const viewHeight = api.getHeight(); - const viewSize = Math.min(viewWidth, viewHeight) / 2; - this.cx = numberUtil.parsePercent(center[0], viewWidth); - this.cy = numberUtil.parsePercent(center[1], viewHeight); + const viewSize = Math.min(refContainer.width, refContainer.height) / 2; + this.cx = numberUtil.parsePercent(center[0], refContainer.width) + refContainer.x; + this.cy = numberUtil.parsePercent(center[1], refContainer.height) + refContainer.y; this.startAngle = radarModel.get('startAngle') * Math.PI / 180; diff --git a/src/coord/single/Single.ts b/src/coord/single/Single.ts index c9226b5aae..80e2ac04c2 100644 --- a/src/coord/single/Single.ts +++ b/src/coord/single/Single.ts @@ -23,7 +23,7 @@ import SingleAxis from './SingleAxis'; import * as axisHelper from '../axisHelper'; -import {getLayoutRect} from '../../util/layout'; +import {createBoxLayoutReference, getLayoutRect} from '../../util/layout'; import {each} from 'zrender/src/core/util'; import { CoordinateSystem, CoordinateSystemMaster } from '../CoordinateSystem'; import GlobalModel from '../../model/Global'; @@ -111,20 +111,8 @@ class Single implements CoordinateSystem, CoordinateSystemMaster { * Resize the single coordinate system. */ resize(axisModel: SingleAxisModel, api: ExtensionAPI) { - this._rect = getLayoutRect( - { - left: axisModel.get('left'), - top: axisModel.get('top'), - right: axisModel.get('right'), - bottom: axisModel.get('bottom'), - width: axisModel.get('width'), - height: axisModel.get('height') - }, - { - width: api.getWidth(), - height: api.getHeight() - } - ); + const refContainer = createBoxLayoutReference(axisModel, api).refContainer; + this._rect = getLayoutRect(axisModel.getBoxLayoutParams(), refContainer); this._adjustAxis(); } @@ -215,38 +203,44 @@ class Single implements CoordinateSystem, CoordinateSystemMaster { } } - pointToData(point: number[]) { + pointToData(point: number[], reserved?: null, out?: number[]) { + out = out || []; const axis = this.getAxis(); - return [axis.coordToData(axis.toLocalCoord( + out[0] = axis.coordToData(axis.toLocalCoord( point[axis.orient === 'horizontal' ? 0 : 1] - ))]; + )); + return out; } /** * Convert the series data to concrete point. * Can be [val] | val */ - dataToPoint(val: ScaleDataValue | ScaleDataValue[]) { + dataToPoint(val: ScaleDataValue | ScaleDataValue[], reserved?: unknown, out?: number[]) { const axis = this.getAxis(); const rect = this.getRect(); - const pt = []; + out = out || []; const idx = axis.orient === 'horizontal' ? 0 : 1; if (val instanceof Array) { val = val[0]; } - pt[idx] = axis.toGlobalCoord(axis.dataToCoord(+val)); - pt[1 - idx] = idx === 0 ? (rect.y + rect.height / 2) : (rect.x + rect.width / 2); - return pt; + out[idx] = axis.toGlobalCoord(axis.dataToCoord(+val)); + out[1 - idx] = idx === 0 ? (rect.y + rect.height / 2) : (rect.x + rect.width / 2); + return out; } - convertToPixel(ecModel: GlobalModel, finder: ParsedModelFinder, value: ScaleDataValue[]) { + convertToPixel( + ecModel: GlobalModel, finder: ParsedModelFinder, value: ScaleDataValue[] + ) { const coordSys = getCoordSys(finder); return coordSys === this ? this.dataToPoint(value) : null; } - convertFromPixel(ecModel: GlobalModel, finder: ParsedModelFinder, pixel: number[]) { + convertFromPixel( + ecModel: GlobalModel, finder: ParsedModelFinder, pixel: number[] + ) { const coordSys = getCoordSys(finder); return coordSys === this ? this.pointToData(pixel) : null; } diff --git a/src/core/CoordinateSystem.ts b/src/core/CoordinateSystem.ts index b1f59222c3..d3535347e3 100644 --- a/src/core/CoordinateSystem.ts +++ b/src/core/CoordinateSystem.ts @@ -21,42 +21,333 @@ import * as zrUtil from 'zrender/src/core/util'; import type GlobalModel from '../model/Global'; import type ExtensionAPI from './ExtensionAPI'; -import type { CoordinateSystemCreator, CoordinateSystemMaster } from '../coord/CoordinateSystem'; +import type { CoordinateSystem, CoordinateSystemCreator, CoordinateSystemMaster } from '../coord/CoordinateSystem'; +import { SINGLE_REFERRING } from '../util/model'; +import ComponentModel from '../model/Component'; +import SeriesModel from '../model/Series'; +import { error } from '../util/log'; +import { CoordinateSystemDataCoord, NullUndefined } from '../util/types'; -const coordinateSystemCreators: {[type: string]: CoordinateSystemCreator} = {}; +type CoordinateSystemCreatorMap = {[type: string]: CoordinateSystemCreator}; + +/** + * FIXME: + * `nonSeriesBoxCoordSysCreators` and `_nonSeriesBoxMasterList` are hardcoded implementations. + * Regarding "coord sys layout based on another coord sys", currently we only exprimentally support one level + * dpendency, such as, "grid(cartesian)s can be laid out based on matrix/calendar coord sys." + * But a comprehensive implementation may need to support: + * - Recursive dependencies. e.g., a matrix coord sys lays out based on another matrix coord sys. + * That requires in the implementation `create` and `update` of coord sys are called by a dependency graph. + * (@see enableTopologicalTravel in `util/component.ts`) + */ +const nonSeriesBoxCoordSysCreators: CoordinateSystemCreatorMap = {}; +const normalCoordSysCreators: CoordinateSystemCreatorMap = {}; class CoordinateSystemManager { - private _coordinateSystems: CoordinateSystemMaster[] = []; + private _normalMasterList: CoordinateSystemMaster[] = []; + private _nonSeriesBoxMasterList: CoordinateSystemMaster[] = []; + /** + * Typically, + * - in `create`, a coord sys lays out based on a given rect; + * - in `update`, update the pixel and data extent of there axes (if any) based on processed `series.data`. + * After that, a coord sys can serve (typically by `dataToPoint`/`dataToLayout`/`pointToData`). + * If the coordinate system do not lay out based on `series.data`, `update` is not needed. + */ create(ecModel: GlobalModel, api: ExtensionAPI): void { - let coordinateSystems: CoordinateSystemMaster[] = []; - zrUtil.each(coordinateSystemCreators, function (creator, type) { - const list = creator.create(ecModel, api); - coordinateSystems = coordinateSystems.concat(list || []); - }); + this._nonSeriesBoxMasterList = dealCreate(nonSeriesBoxCoordSysCreators, true); + this._normalMasterList = dealCreate(normalCoordSysCreators, false); + + function dealCreate(creatorMap: CoordinateSystemCreatorMap, canBeNonSeriesBox: boolean) { + let coordinateSystems: CoordinateSystemMaster[] = []; + zrUtil.each(creatorMap, function (creator, type) { + const list = creator.create(ecModel, api); + coordinateSystems = coordinateSystems.concat(list || []); - this._coordinateSystems = coordinateSystems; + if (__DEV__) { + if (canBeNonSeriesBox) { + // Disallow `update` is a brutal way to ensure `_nonSeriesBoxMasterList`s are ready to + // serve after `create`. But if `update` has to be involved in `_nonSeriesBoxMasterList` + // for some future case, more complicated mechanisms need to be introduced. + zrUtil.each(list, master => zrUtil.assert(!master.update)); + } + } + }); + return coordinateSystems; + } } + /** + * @see CoordinateSystem['create'] + */ update(ecModel: GlobalModel, api: ExtensionAPI): void { - zrUtil.each(this._coordinateSystems, function (coordSys) { + zrUtil.each(this._normalMasterList, function (coordSys) { coordSys.update && coordSys.update(ecModel, api); }); } getCoordinateSystems(): CoordinateSystemMaster[] { - return this._coordinateSystems.slice(); + return this._normalMasterList.concat(this._nonSeriesBoxMasterList); } static register = function (type: string, creator: CoordinateSystemCreator): void { - coordinateSystemCreators[type] = creator; + if (type === 'matrix' || type === 'calendar') { // FIXME: hardcode, @see nonSeriesBoxCoordSysCreators + nonSeriesBoxCoordSysCreators[type] = creator; + return; + } + normalCoordSysCreators[type] = creator; }; static get = function (type: string): CoordinateSystemCreator { - return coordinateSystemCreators[type]; + return normalCoordSysCreators[type] || nonSeriesBoxCoordSysCreators[type]; }; } +function canBeNonSeriesBoxCoordSys(coordSysType: string): boolean { + return !!nonSeriesBoxCoordSysCreators[coordSysType]; +} + + +export const BoxCoordinateSystemCoordFrom = { + // By default fetch coord from `model.get('coord')`. + coord: 1, + // Some model/series, such as pie, is allowed to also get coord from `model.get('center')`, + // if cannot get from `model.get('coord')`. But historically pie use `center` option, but + // geo use `layoutCenter` option to specify layout center; they are not able to be unified. + // Therefor it is not recommended. + coord2: 2, +} as const; +export type BoxCoordinateSystemCoordFrom = + (typeof BoxCoordinateSystemCoordFrom)[keyof typeof BoxCoordinateSystemCoordFrom]; + +type BoxCoordinateSystemGetCoord2 = (model: ComponentModel) => CoordinateSystemDataCoord; + +/** + * @see_also `createBoxLayoutReference` + * @see_also `injectCoordSysByOption` + */ +export function registerLayOutOnCoordSysUsage(opt: { + // `SomeSeries.type` or `SomeComponent.type` + fullType: ComponentModel['type'], + // @see BoxCoordinateSystemCoordFrom . Be `false` by default. + getCoord2?: BoxCoordinateSystemGetCoord2 +}) { + if (__DEV__) { + zrUtil.assert(!coordSysUseMap.get(opt.fullType)); + } + coordSysUseMap.set(opt.fullType, {getCoord2: undefined}).getCoord2 = opt.getCoord2; +} +const coordSysUseMap = zrUtil.createHashMap< + {getCoord2: BoxCoordinateSystemGetCoord2 | NullUndefined}, + ComponentModel['type'] +>(); + +/** + * @return Be an object, but never be NullUndefined. + */ +export function getCoordForBoxCoordSys( + model: ComponentModel +): { + coord: CoordinateSystemDataCoord | NullUndefined + from: BoxCoordinateSystemCoordFrom +} { + let coord: CoordinateSystemDataCoord = model.getShallow('coord', true); + let from: BoxCoordinateSystemCoordFrom = BoxCoordinateSystemCoordFrom.coord; + if (coord == null) { + const store = coordSysUseMap.get(model.type); + if (store && store.getCoord2) { + from = BoxCoordinateSystemCoordFrom.coord2; + coord = store.getCoord2(model); + } + } + return {coord, from}; +} + +/** + * - "dataCoordSys": each data item is laid out based on a coord sys. + * - "boxCoordSys": the overall bounding rect or anchor point is calculated based on a coord sys. + * e.g., + * grid rect (cartesian rect) is calculate based on matrix/calendar coord sys; + * pie center is calculated based on calendar/cartesian; + * + * The default value (if not declared in option `coordinateSystemUsage`): + * For series, use `dataCoordSys`, since this is the most case and backward compatible. + * For non-series components, use `boxCoordSys`, since `dataCoordSys` is not applicable. + */ +export const CoordinateSystemUsageKind = { + none: 0, + dataCoordSys: 1, + boxCoordSys: 2, +} as const; +export type CoordinateSystemUsageKind = (typeof CoordinateSystemUsageKind)[keyof typeof CoordinateSystemUsageKind]; + +export function decideCoordSysUsageKind( + // Component or series + model: ComponentModel, + printError?: boolean +): { + kind: CoordinateSystemUsageKind; + coordSysType: string | NullUndefined; +} { + // For backward compat, still not use `true` in model.get. + const coordSysType = model.getShallow('coordinateSystem'); + let coordSysUsageOption = model.getShallow('coordinateSystemUsage', true); + const isDeclaredExplicitly = coordSysUsageOption != null; + let kind: CoordinateSystemUsageKind = CoordinateSystemUsageKind.none; + + if (coordSysType) { + const isSeries = model.mainType === 'series'; + if (coordSysUsageOption == null) { + coordSysUsageOption = isSeries ? 'data' : 'box'; + } + + if (coordSysUsageOption === 'data') { + kind = CoordinateSystemUsageKind.dataCoordSys; + if (!isSeries) { + if (__DEV__) { + if (isDeclaredExplicitly && printError) { + error('coordinateSystemUsage "data" is not supported in non-series components.'); + } + } + kind = CoordinateSystemUsageKind.none; + } + } + else if (coordSysUsageOption === 'box') { + kind = CoordinateSystemUsageKind.boxCoordSys; + if (!isSeries && !canBeNonSeriesBoxCoordSys(coordSysType)) { + if (__DEV__) { + if (isDeclaredExplicitly && printError) { + error(`coordinateSystem "${coordSysType}" cannot be used` + + ` as coordinateSystemUsage "box" for "${model.type}" yet.` + ); + } + } + kind = CoordinateSystemUsageKind.none; + } + } + } + + return {coordSysType, kind}; +} + +/** + * These cases are considered: + * (A) Most series can use only "dataCoordSys", but "boxCoordSys" is not applicable: + * - e.g., series.heatmap, series.line, series.bar, series.scatter, ... + * (B) Some series and most components can use only "boxCoordSys", but "dataCoordSys" is not applicable: + * - e.g., series.pie, series.funnel, ... + * - e.g., grid, polar, geo, title, ... + * (C) Several series can use both "boxCoordSys" and "dataCoordSys", even at the same time: + * - e.g., series.graph, series.map + * - If graph or map series use a "boxCoordSys", it creates a internal "dataCoordSys" to lay out its data. + * - Graph series can use matrix coord sys as either the "dataCoordSys" (each item layout on one cell) + * or "boxCoordSys" (the entire series are layout within one cell). + * - To achieve this effect, + * `series.coordinateSystemUsage: 'box'` needs to be specified explicitly. + * + * Check these echarts option settings: + * - If `series: {type: 'bar'}`: + * dataCoordSys: "cartesian2d", boxCoordSys: "none". + * (since `coordinateSystem: 'cartesian2d'` is the default option in bar.) + * - If `grid: {coordinateSystem: 'matrix'}` + * dataCoordSys: "none", boxCoordSys: "matrix". + * - If `series: {type: 'pie', coordinateSystem: 'matrix'}`: + * dataCoordSys: "none", boxCoordSys: "matrix". + * (since `coordinateSystemUsage: 'box'` is the default option in pie.) + * - If `series: {type: 'graph', coordinateSystem: 'matrix'}`: + * dataCoordSys: "matrix", boxCoordSys: "none" + * - If `series: {type: 'graph', coordinateSystem: 'matrix', coordinateSystemUsage: 'box'}`: + * dataCoordSys: "an internal view", boxCoordSys: "the internal view is laid out on a matrix" + * - If `series: {type: 'map'}`: + * dataCoordSys: "a internal geo", boxCoordSys: "none" + * - If `series: {type: 'map', coordinateSystem: 'geo', geoIndex: 0}`: + * dataCoordSys: "a geo", boxCoordSys: "none" + * - If `series: {type: 'map', coordinateSystem: 'matrix'}`: + * not_applicable + * - If `series: {type: 'map', coordinateSystem: 'matrix', coordinateSystemUsage: 'box'}`: + * dataCoordSys: "an internal geo", boxCoordSys: "the internal geo is laid out on a matrix" + * + * @usage + * For case (A) & (B), + * call `injectCoordSysByOption({coordSysType: 'aaa', ...})` once for each series/components. + * For case (C), + * call `injectCoordSysByOption({coordSysType: 'aaa', ...})` once for each series/components, + * and then call `injectCoordSysByOption({coordSysType: 'bbb', ..., isDefaultDataCoordSys: true})` + * once for each series/components. + * + * @return Whether injected. + */ +export function injectCoordSysByOption(opt: { + // series or component + targetModel: ComponentModel; + coordSysType: string; + coordSysProvider: CoordSysInjectionProvider; + isDefaultDataCoordSys?: boolean; + allowNotFound?: boolean +}): boolean { + const { + targetModel, + coordSysType, + coordSysProvider, + isDefaultDataCoordSys, + allowNotFound, + } = opt; + if (__DEV__) { + zrUtil.assert(!!coordSysType); + } + + let {kind, coordSysType: declaredType} = decideCoordSysUsageKind(targetModel, true); + + if (isDefaultDataCoordSys + && kind !== CoordinateSystemUsageKind.dataCoordSys + ) { + // If both dataCoordSys and boxCoordSys declared in one model. + // There is the only case in series-graph, and no other cases yet. + kind = CoordinateSystemUsageKind.dataCoordSys; + declaredType = coordSysType; + } + + if (kind === CoordinateSystemUsageKind.none || declaredType !== coordSysType) { + return false; + } + + const coordSys = coordSysProvider(coordSysType, targetModel); + if (!coordSys) { + if (__DEV__) { + if (!allowNotFound) { + error(`${coordSysType} cannot be found for` + + ` ${targetModel.type} (index: ${targetModel.componentIndex}).` + ); + } + } + return false; + } + + if (kind === CoordinateSystemUsageKind.dataCoordSys) { + if (__DEV__) { + zrUtil.assert(targetModel.mainType === 'series'); + } + (targetModel as SeriesModel).coordinateSystem = coordSys; + } + else { // kind === 'boxCoordSys' + targetModel.boxCoordinateSystem = coordSys; + } + + return true; +} + +type CoordSysInjectionProvider = ( + coordSysType: string, injectTargetModel: ComponentModel +) => CoordinateSystem | NullUndefined; + +export const simpleCoordSysInjectionProvider: CoordSysInjectionProvider = function (coordSysType, injectTargetModel) { + const coordSysModel = injectTargetModel.getReferringComponents( + coordSysType, SINGLE_REFERRING + ).models[0] as (ComponentModel & {coordinateSystem: CoordinateSystem}); + return coordSysModel && coordSysModel.coordinateSystem; +}; + + export default CoordinateSystemManager; diff --git a/src/core/ExtensionAPI.ts b/src/core/ExtensionAPI.ts index 98f15e5cab..047bb704c9 100644 --- a/src/core/ExtensionAPI.ts +++ b/src/core/ExtensionAPI.ts @@ -72,6 +72,7 @@ abstract class ExtensionAPI { abstract getViewOfComponentModel(componentModel: ComponentModel): ComponentView; abstract getViewOfSeriesModel(seriesModel: SeriesModel): ChartView; abstract getModel(): GlobalModel; + abstract getMainProcessVersion(): number; } export default ExtensionAPI; diff --git a/src/core/echarts.ts b/src/core/echarts.ts index 1c56ecef42..e1875429a9 100644 --- a/src/core/echarts.ts +++ b/src/core/echarts.ts @@ -109,6 +109,9 @@ import { ZRElementEventName, ECElementEvent, AnimationOption, + CoordinateSystemDataLayout, + NullUndefined, + CoordinateSystemDataCoord, } from '../util/types'; import Displayable from 'zrender/src/graphic/Displayable'; import { seriesSymbolTask, dataSymbolTask } from '../visual/symbol'; @@ -133,7 +136,6 @@ import lifecycle, { import { platformApi, setPlatformAPI } from 'zrender/src/core/platform'; import { getImpl } from './impl'; import type geoSourceManager from '../coord/geo/geoSourceManager'; -import { ExtendedElement } from './ExtendedElement'; import { registerCustomSeries as registerCustom } from '../chart/custom/customSeriesRegister'; @@ -202,6 +204,11 @@ export const PRIORITY = { // This flag is used to carry out this rule. // All events will be triggered out side main process (i.e. when !this[IN_MAIN_PROCESS]). const IN_MAIN_PROCESS_KEY = '__flagInMainProcess' as const; +// Useful for detecting outdated rendering results in scenarios that these issues are involved: +// - Use shortcut (such as, updateTransform, or no update) to start a main process. +// - Asynchronously update rendered view (e.g., graph force layout). +// - Multiple ChartView/ComponentView render to one group cooperatively. +const MAIN_PROCESS_VERSION_KEY = '__mainProcessVersion' as const; const PENDING_UPDATE = '__pendingUpdate' as const; const STATUS_NEEDS_UPDATE_KEY = '__needsUpdateStatus' as const; const ACTION_REG = /^[a-zA-Z0-9_]+$/; @@ -287,12 +294,29 @@ let updateMethods: { updateVisual: UpdateMethod, updateLayout: UpdateMethod }; -let doConvertPixel: ( - ecIns: ECharts, - methodName: string, - finder: ModelFinder, - value: (number | number[]) | (ScaleDataValue | ScaleDataValue[]) -) => (number | number[]); +let doConvertPixel: { + ( + ecIns: ECharts, + methodName: 'convertFromPixel', + finder: ModelFinder, + value: number | number[], + opt: unknown + ): number | number[]; + ( + ecIns: ECharts, + methodName: 'convertToPixel', + finder: ModelFinder, + value: CoordinateSystemDataCoord, + opt: unknown + ): number | number[]; + ( + ecIns: ECharts, + methodName: 'convertToLayout', + finder: ModelFinder, + value: CoordinateSystemDataCoord, + opt: unknown + ): CoordinateSystemDataLayout; +}; let updateStreamModes: (ecIns: ECharts, ecModel: GlobalModel) => void; let doDispatchAction: (this: ECharts, payload: Payload, silent: boolean) => void; let flushPendingActions: (this: ECharts, silent: boolean) => void; @@ -316,6 +340,7 @@ let enableConnect: (ecIns: ECharts) => void; let markStatusToUpdate: (ecIns: ECharts) => void; let applyChangedStates: (ecIns: ECharts) => void; +let updateMainProcessVersion: (ecIns: ECharts) => void; type RenderedEventParam = { elapsedTime: number }; type ECEventDefinition = { @@ -398,6 +423,7 @@ class ECharts extends Eventful { updateParams: UpdateLifecycleParams }; private [IN_MAIN_PROCESS_KEY]: boolean; + private [MAIN_PROCESS_VERSION_KEY]: number; private [CONNECT_STATUS_KEY]: ConnectStatus; private [STATUS_NEEDS_UPDATE_KEY]: boolean; @@ -417,6 +443,8 @@ class ECharts extends Eventful { let defaultCoarsePointer: 'auto' | boolean = 'auto'; let defaultUseDirtyRect = false; + this[MAIN_PROCESS_VERSION_KEY] = 1; + if (__DEV__) { const root = ( /* eslint-disable-next-line */ @@ -510,6 +538,7 @@ class ECharts extends Eventful { const silent = (this[PENDING_UPDATE] as any).silent; this[IN_MAIN_PROCESS_KEY] = true; + updateMainProcessVersion(this); try { prepare(this); @@ -636,6 +665,7 @@ class ECharts extends Eventful { } this[IN_MAIN_PROCESS_KEY] = true; + updateMainProcessVersion(this); if (!this._model || notMerge) { const optionManager = new OptionManager(this._api); @@ -726,6 +756,7 @@ class ECharts extends Eventful { } this[IN_MAIN_PROCESS_KEY] = true; + updateMainProcessVersion(this); try { this._updateTheme(theme); @@ -996,21 +1027,61 @@ class ECharts extends Eventful { /** * Convert from logical coordinate system to pixel coordinate system. * See CoordinateSystem#convertToPixel. + * + * TODO / PENDING: + * currently `convertToPixel` `convertFromPixel` `convertToLayout` may not be suitable + * for some extremely performance-sensitive scenarios (such as, handling massive amounts of data), + * since it performce "find component" every time. + * And it is not friendly to the nuances between different coordinate systems. + * @see https://github.com/apache/echarts/issues/20985 for details + * + * @see CoordinateSystem['dataToPoint'] for parameters and return. + * @see CoordinateSystemDataCoord */ convertToPixel(finder: ModelFinder, value: ScaleDataValue): number; convertToPixel(finder: ModelFinder, value: ScaleDataValue[]): number[]; - convertToPixel(finder: ModelFinder, value: ScaleDataValue | ScaleDataValue[]): number | number[] { - return doConvertPixel(this, 'convertToPixel', finder, value); + convertToPixel( + finder: ModelFinder, value: ScaleDataValue | ScaleDataValue[] + ): number | number[]; + // The above are signatures from before v6, thus they should be preserved for backward compat. + convertToPixel( + finder: ModelFinder, value: (ScaleDataValue | ScaleDataValue[] | NullUndefined)[] + ): number | number[]; + convertToPixel( + finder: ModelFinder, + value: (ScaleDataValue | NullUndefined) | (ScaleDataValue | ScaleDataValue[] | NullUndefined)[], + opt?: unknown + ): number | number[] { + return doConvertPixel(this, 'convertToPixel', finder, value, opt); + } + + /** + * Convert from logical coordinate system to pixel coordinate system. + * See CoordinateSystem#convertToPixel. + * + * @see CoordinateSystem['dataToLayout'] for parameters and return. + * @see CoordinateSystemDataCoord + */ + convertToLayout( + finder: ModelFinder, + value: (ScaleDataValue | NullUndefined) | (ScaleDataValue | ScaleDataValue[] | NullUndefined)[], + opt?: unknown + ): CoordinateSystemDataLayout { + return doConvertPixel(this, 'convertToLayout', finder, value, opt); } /** * Convert from pixel coordinate system to logical coordinate system. * See CoordinateSystem#convertFromPixel. + * + * @see CoordinateSystem['pointToData'] for parameters and return. */ convertFromPixel(finder: ModelFinder, value: number): number; convertFromPixel(finder: ModelFinder, value: number[]): number[]; - convertFromPixel(finder: ModelFinder, value: number | number[]): number | number[] { - return doConvertPixel(this, 'convertFromPixel', finder, value); + convertFromPixel(finder: ModelFinder, value: number | number[]): number | number[]; + // The above are signatures from before v6, thus they should be preserved for backward compat. + convertFromPixel(finder: ModelFinder, value: number | number[], opt?: unknown): number | number[] { + return doConvertPixel(this, 'convertFromPixel', finder, value, opt); } /** @@ -1317,6 +1388,7 @@ class ECharts extends Eventful { } this[IN_MAIN_PROCESS_KEY] = true; + updateMainProcessVersion(this); try { needPrepare && prepare(this); @@ -1895,12 +1967,34 @@ class ECharts extends Eventful { } }; - doConvertPixel = function ( + function doConvertPixelImpl( + ecIns: ECharts, + methodName: 'convertFromPixel', + finder: ModelFinder, + value: number | number[], + opt: unknown + ): number | number[]; + function doConvertPixelImpl( + ecIns: ECharts, + methodName: 'convertToPixel', + finder: ModelFinder, + value: CoordinateSystemDataCoord, + opt: unknown + ): number | number[]; + function doConvertPixelImpl( + ecIns: ECharts, + methodName: 'convertToLayout', + finder: ModelFinder, + value: CoordinateSystemDataCoord, + opt: unknown + ): CoordinateSystemDataLayout; + function doConvertPixelImpl( ecIns: ECharts, - methodName: 'convertFromPixel' | 'convertToPixel', + methodName: 'convertFromPixel' | 'convertToPixel' | 'convertToLayout', finder: ModelFinder, - value: (number | number[]) | (ScaleDataValue | ScaleDataValue[]) - ): (number | number[]) { + value: number | number[] | CoordinateSystemDataCoord, + opt: unknown + ) { if (ecIns._disposed) { disposedWarning(ecIns.id); return; @@ -1914,7 +2008,7 @@ class ECharts extends Eventful { for (let i = 0; i < coordSysList.length; i++) { const coordSys = coordSysList[i]; if (coordSys[methodName] - && (result = coordSys[methodName](ecModel, parsedFinder, value as any)) != null + && (result = coordSys[methodName](ecModel, parsedFinder, value as any, opt)) != null ) { return result; } @@ -1926,6 +2020,7 @@ class ECharts extends Eventful { ); } }; + doConvertPixel = doConvertPixelImpl; updateStreamModes = function (ecIns: ECharts, ecModel: GlobalModel): void { const chartsMap = ecIns._chartsMap; @@ -1946,6 +2041,7 @@ class ECharts extends Eventful { const cptType = cptTypeTmp[0] != null && parseClassType(cptTypeTmp[0]); this[IN_MAIN_PROCESS_KEY] = true; + updateMainProcessVersion(this); let payloads: Payload[] = [payload]; let batched = false; @@ -2328,6 +2424,10 @@ class ECharts extends Eventful { ecIns.getZr().wakeUp(); }; + updateMainProcessVersion = function (ecIns: ECharts): void { + ecIns[MAIN_PROCESS_VERSION_KEY] = (ecIns[MAIN_PROCESS_VERSION_KEY] + 1) % 1000; + }; + applyChangedStates = function (ecIns: ECharts): void { if (!ecIns[STATUS_NEEDS_UPDATE_KEY]) { return; @@ -2415,76 +2515,15 @@ class ECharts extends Eventful { if (model.preventAutoZ) { return; } - const z = model.get('z') || 0; - const zlevel = model.get('zlevel') || 0; + const zInfo = graphic.retrieveZInfo(model); // Set z and zlevel view.eachRendered((el) => { - doUpdateZ(el, z, zlevel, -Infinity, false); + graphic.traverseUpdateZ(el, zInfo.z, zInfo.zlevel); // Don't traverse the children because it has been traversed in _updateZ. return true; }); }; - function doUpdateZ( - el: Element, - z: number, - zlevel: number, - maxZ2: number, - ignoreModelZ: boolean - ): number { - // Group may also have textContent - const label = el.getTextContent(); - const labelLine = el.getTextGuideLine(); - const isGroup = el.isGroup; - - if (isGroup) { - // set z & zlevel of children elements of Group - const children = (el as graphic.Group).childrenRef(); - for (let i = 0; i < children.length; i++) { - ignoreModelZ = ignoreModelZ || (el as ExtendedElement).ignoreModelZ; - maxZ2 = Math.max( - doUpdateZ( - children[i], - z, - zlevel, - maxZ2, - ignoreModelZ || (el as ExtendedElement).ignoreModelZ - ), - maxZ2 - ); - } - } - else { - if (ignoreModelZ || (el as ExtendedElement).ignoreModelZ) { - // This child element will not be set z and zlevel of the group - return maxZ2; - } - - // not Group - (el as Displayable).z = z; - (el as Displayable).zlevel = zlevel; - - maxZ2 = Math.max((el as Displayable).z2, maxZ2); - } - - // always set z and zlevel if label/labelLine exists - if (label) { - label.z = z; - label.zlevel = zlevel; - // lift z2 of text content - // TODO if el.emphasis.z2 is spcefied, what about textContent. - isFinite(maxZ2) && (label.z2 = maxZ2 + 2); - } - if (labelLine) { - const textGuideLineConfig = el.textGuideLineConfig; - labelLine.z = z; - labelLine.zlevel = zlevel; - isFinite(maxZ2) - && (labelLine.z2 = maxZ2 + (textGuideLineConfig && textGuideLineConfig.showAbove ? 1 : -1)); - } - return maxZ2; - } - // Clear states without animation. // TODO States on component. function clearStates(model: ComponentModel, view: ComponentView | ChartView): void { @@ -2617,6 +2656,9 @@ class ECharts extends Eventful { getViewOfSeriesModel(seriesModel: SeriesModel): ChartView { return ecIns.getViewOfSeriesModel(seriesModel); } + getMainProcessVersion(): number { + return ecIns[MAIN_PROCESS_VERSION_KEY]; + } })(ecIns); }; diff --git a/src/data/OrdinalMeta.ts b/src/data/OrdinalMeta.ts index 441e012afc..85f08b75ce 100644 --- a/src/data/OrdinalMeta.ts +++ b/src/data/OrdinalMeta.ts @@ -33,18 +33,46 @@ class OrdinalMeta { private _map: HashMap; - readonly uid: number; + private _onCollect: (category: OrdinalRawValue, index: number) => void; + readonly uid: number; + /** + * PENDING - Regarding forcibly converting to string: + * In the early days, the underlying hash map impl used JS plain object and converted the key to + * string; later in https://github.com/ecomfe/zrender/pull/966 it was changed to a JS Map (in supported + * platforms), which does not require string keys. But consider any input that `scale/Ordinal['parse']` + * is involved, a number input represents an `OrdinalNumber` (i.e., an index), and affect the query + * behavior: + * - If forcbily converting to string: + * pros: users can use numeric string (such as, '123') to query the raw data (123), tho it's probably + * still confusing. + * cons: NaN/null/undefined in data will be equals to 'NaN'/'null'/'undefined', if simply using + * `val + ''` to convert them, like currently `getName` does. + * - Otherwise: + * pros: see NaN/null/undefined case above. + * cons: users cannot query the raw data (123) any more. + * There are two inconsistent behaviors in the current impl: + * - Force conversion is applied on the case `xAxis{data: ['aaa', 'bbb', ...]}`, + * but no conversion applied to the case `xAxis{data: [{value: 'aaa'}, ...]}` and + * the case `dataset: {source: [['aaa', 123], ['bbb', 234], ...]}`. + * - behaves differently according to whether JS Map is supported (the polyfill is simply using JS + * plain object) (tho it seems rare platform that do not support it). + * Since there's no sufficient good solution to offset cost of the breaking change, we preserve the + * current behavior, until real issues is reported. + */ constructor(opt: { categories?: OrdinalRawValue[], needCollect?: boolean deduplication?: boolean + // Called only on `needCollect` is true and collect happens. + onCollect?: OrdinalMeta['_onCollect'] }) { this.categories = opt.categories || []; this._needCollect = opt.needCollect; this._deduplication = opt.deduplication; this.uid = ++uidBase; + this._onCollect = opt.onCollect; } static createByAxisModel(axisModel: Model): OrdinalMeta { @@ -61,7 +89,6 @@ class OrdinalMeta { }; getOrdinal(category: OrdinalRawValue): OrdinalNumber { - // @ts-ignore return this._getOrCreateMap().get(category); } @@ -92,19 +119,19 @@ class OrdinalMeta { if (needCollect && !this._deduplication) { index = this.categories.length; this.categories[index] = category; + this._onCollect && this._onCollect(category, index); return index; } const map = this._getOrCreateMap(); - // @ts-ignore index = map.get(category); if (index == null) { if (needCollect) { index = this.categories.length; this.categories[index] = category; - // @ts-ignore map.set(category, index); + this._onCollect && this._onCollect(category, index); } else { index = NaN; diff --git a/src/data/helper/dataProvider.ts b/src/data/helper/dataProvider.ts index caf047fb91..c1ac07eebb 100644 --- a/src/data/helper/dataProvider.ts +++ b/src/data/helper/dataProvider.ts @@ -21,7 +21,7 @@ // ??? refactor? check the outer usage of data provider. // merge with defaultDimValueGetter? -import {isTypedArray, extend, assert, each, isObject, bind} from 'zrender/src/core/util'; +import {isTypedArray, extend, assert, each, isObject, bind, isArray} from 'zrender/src/core/util'; import {getDataItemValue} from '../../util/model'; import { createSourceFromSeriesDataOption, Source, isSourceInstance } from '../Source'; import {ArrayLike, Dictionary} from 'zrender/src/core/types'; @@ -37,6 +37,7 @@ import { OptionDataItem, OptionDataValue, SourceFormat, SeriesLayoutBy, ParsedValue, DimensionLoose, NullUndefined } from '../../util/types'; import SeriesData from '../SeriesData'; +import { error } from '../../util/log'; export interface DataProvider { /** @@ -112,9 +113,11 @@ export class DefaultDataProvider implements DataProvider { // declare source is Source; this._source = source; const data = this._data = source.data; + const sourceFormat = source.sourceFormat; + const seriesLayoutBy = source.seriesLayoutBy; // Typed array. TODO IE10+? - if (source.sourceFormat === SOURCE_FORMAT_TYPED_ARRAY) { + if (sourceFormat === SOURCE_FORMAT_TYPED_ARRAY) { if (__DEV__) { if (dimSize == null) { throw new Error('Typed array data must specify dimension size'); @@ -125,6 +128,11 @@ export class DefaultDataProvider implements DataProvider { this._data = data; } + if (__DEV__) { + const validator = rawSourceDataValidatorMap[getMethodMapKey(sourceFormat, seriesLayoutBy)]; + validator && validator(data, source.dimensionsDefine); + } + mountMethods(this, data, source); } @@ -286,6 +294,39 @@ export class DefaultDataProvider implements DataProvider { } +type RawSourceDataValidator = ( + rawData: OptionSourceData, + dimsDef: { name?: DimensionName }[], +) => void; + +const validateSimply: RawSourceDataValidator = function ( + rawData +) { + if (!isArray(rawData)) { + error('series.data or dataset.source must be an array.'); + } +}; + +/** + * Only run in dev mode - hint users for debug. + */ +const rawSourceDataValidatorMap: Dictionary = { + [SOURCE_FORMAT_ARRAY_ROWS + '_' + SERIES_LAYOUT_BY_COLUMN]: validateSimply, + [SOURCE_FORMAT_ARRAY_ROWS + '_' + SERIES_LAYOUT_BY_ROW]: validateSimply, + [SOURCE_FORMAT_OBJECT_ROWS]: validateSimply, + [SOURCE_FORMAT_KEYED_COLUMNS]: function ( + rawData, dimsDef + ) { + for (let i = 0; i < dimsDef.length; i++) { + const dimName = dimsDef[i].name; + if (dimName == null) { + error('dimension name must not be null/undefined.'); + } + } + }, + [SOURCE_FORMAT_ORIGINAL]: validateSimply +}; + type RawSourceItemGetter = ( rawData: OptionSourceData, @@ -328,12 +369,7 @@ const rawSourceItemGetterMap: Dictionary = { const item = out || []; for (let i = 0; i < dimsDef.length; i++) { const dimName = dimsDef[i].name; - if (__DEV__) { - if (dimName == null) { - throw new Error(); - } - } - const col = (rawData as Dictionary)[dimName]; + const col = dimName != null ? (rawData as Dictionary)[dimName] : null; item[i] = col ? col[idx] : null; } return item; @@ -383,12 +419,7 @@ const rawSourceDataCounterMap: Dictionary = { rawData, startIndex, dimsDef ) { const dimName = dimsDef[0].name; - if (__DEV__) { - if (dimName == null) { - throw new Error(); - } - } - const col = (rawData as Dictionary)[dimName]; + const col = dimName != null ? (rawData as Dictionary)[dimName] : null; return col ? col.length : 0; }, [SOURCE_FORMAT_ORIGINAL]: countSimply diff --git a/src/echarts.all.ts b/src/echarts.all.ts index 7c05737bfc..13aca093e1 100644 --- a/src/echarts.all.ts +++ b/src/echarts.all.ts @@ -66,6 +66,7 @@ import { SingleAxisComponent, ParallelComponent, CalendarComponent, + MatrixComponent, GraphicComponent, ToolboxComponent, TooltipComponent, @@ -83,6 +84,7 @@ import { VisualMapComponent, VisualMapContinuousComponent, VisualMapPiecewiseComponent, + ThumbnailComponent, AriaComponent, DatasetComponent, TransformComponent @@ -211,7 +213,14 @@ use(ParallelComponent); // ); use(CalendarComponent); - +// `matrix` coordinate system. for example, +// chart.setOption({ +// matrix: {...}, +// series: [{ +// coordinateSystem: 'matrix' +// }] +// ); +use(MatrixComponent); // ------------------ // Other components @@ -324,6 +333,8 @@ use(VisualMapContinuousComponent); // }); use(VisualMapPiecewiseComponent); +use(ThumbnailComponent); + // `aria` component providing aria, for example: // chart.setOption({ // aria: {...} diff --git a/src/export/components.ts b/src/export/components.ts index 8e4e14e26b..6a4091ae50 100644 --- a/src/export/components.ts +++ b/src/export/components.ts @@ -28,6 +28,7 @@ export {install as GeoComponent} from '../component/geo/install'; export {install as SingleAxisComponent} from '../component/singleAxis/install'; export {install as ParallelComponent} from '../component/parallel/install'; export {install as CalendarComponent} from '../component/calendar/install'; +export {install as MatrixComponent} from '../component/matrix/install'; export {install as GraphicComponent} from '../component/graphic/install'; @@ -57,6 +58,8 @@ export {install as VisualMapContinuousComponent} from '../component/visualMap/in export {install as VisualMapPiecewiseComponent} from '../component/visualMap/installVisualMapPiecewise'; +export {install as ThumbnailComponent} from '../component/thumbnail/install'; + export {install as AriaComponent} from '../component/aria/install'; export {install as TransformComponent} from '../component/transform/install'; @@ -77,6 +80,7 @@ export { SingleAxisComponentOption, ParallelComponentOption, CalendarComponentOption, + MatrixComponentOption, GraphicComponentOption, @@ -98,6 +102,8 @@ export { VisualMapComponentOption, + ThumbnailComponentOption, + AriaComponentOption, DatasetComponentOption diff --git a/src/export/option.ts b/src/export/option.ts index a39fe92e6e..1859271193 100644 --- a/src/export/option.ts +++ b/src/export/option.ts @@ -33,6 +33,7 @@ import type {SingleAxisOption as SingleAxisComponentOption} from '../coord/singl import type {ParallelAxisOption as ParallelAxisComponentOption} from '../coord/parallel/AxisModel'; import type {ParallelCoordinateSystemOption as ParallelComponentOption} from '../coord/parallel/ParallelModel'; import type {CalendarOption as CalendarComponentOption} from '../coord/calendar/CalendarModel'; +import type {MatrixOption as MatrixComponentOption} from '../coord/matrix/MatrixModel'; import type {ToolboxOption} from '../component/toolbox/ToolboxModel'; import type { TooltipOption as TooltipComponentOption, @@ -41,6 +42,7 @@ import type { import type {AxisPointerOption as AxisPointerComponentOption} from '../component/axisPointer/AxisPointerModel'; import type {BrushOption as BrushComponentOption} from '../component/brush/BrushModel'; import type {TitleOption as TitleComponentOption} from '../component/title/install'; +import type {ThumbnailOption as ThumbnailComponentOption} from '../component/thumbnail/ThumbnailModel'; import type {TimelineOption as TimelineComponentOption} from '../component/timeline/TimelineModel'; import type {SliderTimelineOption as TimelineSliderComponentOption} from '../component/timeline/SliderTimelineModel'; @@ -156,6 +158,7 @@ export { AngleAxisComponentOption, ParallelComponentOption, CalendarComponentOption, + MatrixComponentOption, TooltipComponentOption, AxisPointerComponentOption, BrushComponentOption, @@ -165,6 +168,7 @@ export { MarkPointComponentOption, MarkAreaComponentOption, ToolboxComponentOption, + ThumbnailComponentOption, GraphicComponentOption, AriaComponentOption, DatasetComponentOption @@ -262,6 +266,7 @@ export interface EChartsOption extends ECBasicOption { parallel?: ParallelComponentOption | ParallelComponentOption[]; parallelAxis?: ParallelAxisComponentOption | ParallelAxisComponentOption[]; calendar?: CalendarComponentOption | CalendarComponentOption[]; + matrix?: MatrixComponentOption | MatrixComponentOption[]; toolbox?: ToolboxComponentOption | ToolboxComponentOption[]; tooltip?: TooltipComponentOption | TooltipComponentOption[]; axisPointer?: AxisPointerComponentOption | AxisPointerComponentOption[]; @@ -270,6 +275,7 @@ export interface EChartsOption extends ECBasicOption { legend?: LegendComponentOption | (LegendComponentOption)[]; dataZoom?: DataZoomComponentOption | (DataZoomComponentOption)[]; visualMap?: VisualMapComponentOption | (VisualMapComponentOption)[]; + thumbnail?: ThumbnailComponentOption | (ThumbnailComponentOption)[]; graphic?: GraphicComponentOption | GraphicComponentOption[]; // TODO Generally we support specify a single object on series. diff --git a/src/label/labelStyle.ts b/src/label/labelStyle.ts index 510445a2b3..4bcbc14b05 100644 --- a/src/label/labelStyle.ts +++ b/src/label/labelStyle.ts @@ -65,6 +65,9 @@ type TextCommonParams = { // supportLegacyAuto?: boolean textStyle?: ZRStyleProps + + autoOverflowArea?: ElementTextConfig['autoOverflowArea'], + layoutRect?: ElementTextConfig['layoutRect'], }; const EMPTY_OBJ = {}; @@ -313,7 +316,7 @@ export function getLabelStatesModels( export function createTextStyle( textStyleModel: Model, specifiedTextStyle?: TextStyleProps, // Fixed style in the code. Can't be set by model. - opt?: Pick, + opt?: Parameters[2], isNotNormal?: boolean, isAttached?: boolean // If text is attached on an element. If so, auto color will handling in zrender. ) { @@ -324,8 +327,11 @@ export function createTextStyle( return textStyle; } export function createTextConfig( - textStyleModel: Model, - opt?: Pick, + textStyleModel: Model>, + opt?: Pick< + TextCommonParams, + 'defaultOutsidePosition' | 'inheritColor' | 'autoOverflowArea' | 'layoutRect' + >, isNotNormal?: boolean ) { opt = opt || {}; @@ -337,7 +343,7 @@ export function createTextConfig( labelPosition = textStyleModel.getShallow('position') || (isNotNormal ? null : 'inside'); // 'outside' is not a valid zr textPostion value, but used - // in bar series, and magric type should be considered. + // in bar series, and magic type should be considered. labelPosition === 'outside' && (labelPosition = opt.defaultOutsidePosition || 'top'); if (labelPosition != null) { textConfig.position = labelPosition; @@ -356,8 +362,15 @@ export function createTextConfig( textConfig.outsideFill = textStyleModel.get('color') === 'inherit' ? (opt.inheritColor || null) : 'auto'; + if (opt.autoOverflowArea != null) { + textConfig.autoOverflowArea = opt.autoOverflowArea; + } + if (opt.layoutRect != null) { + textConfig.layoutRect = opt.layoutRect; + } return textConfig; } + /** * The uniform entry of set text style, that is, retrieve style definitions * from `model` and set to `textStyle` object. @@ -426,6 +439,10 @@ function setTextStyleCommon( if (overflow) { textStyle.overflow = overflow; } + const lineOverflow = textStyleModel.get('lineOverflow'); + if (lineOverflow) { + textStyle.lineOverflow = lineOverflow; + } const margin = textStyleModel.get('minMargin'); if (margin != null) { textStyle.margin = margin; diff --git a/src/model/Component.ts b/src/model/Component.ts index 889159ff4d..29fdf0be17 100644 --- a/src/model/Component.ts +++ b/src/model/Component.ts @@ -40,8 +40,10 @@ import { ComponentSubType, ComponentFullType, ComponentLayoutMode, - BoxLayoutOptionMixin + BoxLayoutOptionMixin, + NullUndefined } from '../util/types'; +import { CoordinateSystem } from '../coord/CoordinateSystem'; const inner = makeInner<{ defaultOption: ComponentOption @@ -118,6 +120,11 @@ class ComponentModel extends Mode // // `CoordinateSystemHostModel` itself. // coordinateSystem: CoordinateSystemMaster | CoordinateSystemExecutive; + // Determine the layout box based on that coordinate system, if specified. + // Will be injected. + // @see injectCoordinateSystem + boxCoordinateSystem?: CoordinateSystem | NullUndefined; + /** * Support merge layout params. * Only support 'box' now (left/right/top/bottom/width/height). @@ -133,6 +140,7 @@ class ComponentModel extends Mode __viewId: string; __requireNewView: boolean; + static protoInitialize = (function () { const proto = ComponentModel.prototype; proto.type = 'component'; @@ -300,15 +308,8 @@ class ComponentModel extends Mode getBoxLayoutParams() { // Consider itself having box layout configs. - const boxLayoutModel = this as Model; - return { - left: boxLayoutModel.get('left'), - top: boxLayoutModel.get('top'), - right: boxLayoutModel.get('right'), - bottom: boxLayoutModel.get('bottom'), - width: boxLayoutModel.get('width'), - height: boxLayoutModel.get('height') - }; + // For backward compatibility, by default do not `ignoreParent`. + return layout.getBoxLayoutParams(this as Model, false); } /** diff --git a/src/model/Global.ts b/src/model/Global.ts index 1150635a25..c1e2be3250 100644 --- a/src/model/Global.ts +++ b/src/model/Global.ts @@ -87,6 +87,7 @@ const BUITIN_COMPONENTS_MAP = { singleAxis: 'SingleAxisComponent', parallel: 'ParallelComponent', calendar: 'CalendarComponent', + matrix: 'MatrixComponent', graphic: 'GraphicComponent', toolbox: 'ToolboxComponent', tooltip: 'TooltipComponent', diff --git a/src/model/Series.ts b/src/model/Series.ts index 1b24c027e1..17779c912a 100644 --- a/src/model/Series.ts +++ b/src/model/Series.ts @@ -147,7 +147,8 @@ class SeriesModel extends ComponentMode // @readonly seriesIndex: number; - // coordinateSystem will be injected in the echarts/CoordinateSystem + // Will be injected. + // @see `injectCoordinateSystem` coordinateSystem: CoordinateSystem; // Injected outside diff --git a/src/model/referHelper.ts b/src/model/referHelper.ts index a3c62c890a..e9010a9835 100644 --- a/src/model/referHelper.ts +++ b/src/model/referHelper.ts @@ -35,6 +35,10 @@ import { SINGLE_REFERRING } from '../util/model'; import { ParallelSeriesOption } from '../chart/parallel/ParallelSeries'; import ParallelModel from '../coord/parallel/ParallelModel'; import ParallelAxisModel from '../coord/parallel/AxisModel'; +import MatrixModel from '../coord/matrix/MatrixModel'; +import type Model from './Model'; +import { AxisBaseOptionCommon } from '../coord/axisCommonTypes'; +import { AxisModelExtendedInCreator } from '../coord/axisModelCreator'; /** * @class @@ -74,12 +78,15 @@ class CoordSysInfo { } } -type SupportedCoordSys = 'cartesian2d' | 'polar' | 'singleAxis' | 'geo' | 'parallel'; +type SupportedCoordSys = 'cartesian2d' | 'polar' | 'singleAxis' | 'geo' | 'parallel' | 'matrix'; +type FetcherAxisModel = + Model> + & Pick; type Fetcher = ( seriesModel: SeriesModel, result: CoordSysInfo, - axisMap: HashMap, - categoryAxisMap: HashMap + axisMap: HashMap, + categoryAxisMap: HashMap ) => void; export function getCoordSysInfoBySeries(seriesModel: SeriesModel) { @@ -92,6 +99,7 @@ export function getCoordSysInfoBySeries(seriesModel: SeriesModel) { } } +// TODO: refactor them to static member of each coord sys, rather than hard code here. const fetchers: Record = { cartesian2d: function ( @@ -202,7 +210,27 @@ const fetchers: Record = { } } }); - } + }, + + matrix: function (seriesModel, result, axisMap, categoryAxisMap) { + const matrixModel = seriesModel.getReferringComponents( + 'matrix', SINGLE_REFERRING + ).models[0] as MatrixModel; + + if (__DEV__) { + if (!matrixModel) { + throw new Error('matrix coordinate system should be specified.'); + } + } + + result.coordSysDims = ['x', 'y']; + const xModel = matrixModel.getDimensionModel('x'); + const yModel = matrixModel.getDimensionModel('y'); + axisMap.set('x', xModel); + axisMap.set('y', yModel); + categoryAxisMap.set('x', xModel); + categoryAxisMap.set('y', yModel); + }, }; function isCategory(axisModel: AxisBaseModel) { diff --git a/src/theme/dark.ts b/src/theme/dark.ts index c486b43916..eb7b059705 100644 --- a/src/theme/dark.ts +++ b/src/theme/dark.ts @@ -53,6 +53,14 @@ const axisCommon = function () { axisName: {} }; }; +const matrixAxis = { + label: { + color: color.secondary + }, + itemStyle: { + borderColor: color.neutral20 + } +}; const theme = { darkMode: true, @@ -176,6 +184,16 @@ const theme = { color: color.secondary } }, + matrix: { + x: matrixAxis, + y: matrixAxis, + backgroundColor: { + borderColor: '#817f91' + }, + innerBackgroundStyle: { + borderColor: '#484753' + } + }, timeAxis: axisCommon(), logAxis: axisCommon(), valueAxis: axisCommon(), diff --git a/src/util/graphic.ts b/src/util/graphic.ts index d2efa8baa2..a2838adac7 100644 --- a/src/util/graphic.ts +++ b/src/util/graphic.ts @@ -38,7 +38,7 @@ import Arc from 'zrender/src/graphic/shape/Arc'; import CompoundPath from 'zrender/src/graphic/CompoundPath'; import LinearGradient from 'zrender/src/graphic/LinearGradient'; import RadialGradient from 'zrender/src/graphic/RadialGradient'; -import BoundingRect from 'zrender/src/core/BoundingRect'; +import BoundingRect, { RectLike } from 'zrender/src/core/BoundingRect'; import OrientedBoundingRect from 'zrender/src/core/OrientedBoundingRect'; import Point from 'zrender/src/core/Point'; import IncrementalDisplayable from 'zrender/src/graphic/IncrementalDisplayable'; @@ -52,7 +52,9 @@ import { ZRRectLike, ZRStyleProps, CommonTooltipOption, - ComponentItemTooltipLabelFormatterParams + ComponentItemTooltipLabelFormatterParams, + NullUndefined, + ComponentOption } from './types'; import { extend, @@ -64,6 +66,7 @@ import { each, hasOwn, isArray, + isNumber, clone, } from 'zrender/src/core/util'; import { getECData } from './innerStore'; @@ -76,6 +79,7 @@ import { removeElementWithFadeOut, isElementRemoved } from '../animation/basicTransition'; +import { ExtendedElement } from '../core/ExtendedElement'; /** * @deprecated export for compatitable reason @@ -91,6 +95,9 @@ const _customShapeMap: Dictionary<{ new(): Path }> = {}; type ExtendShapeOpt = Parameters[0]; type ExtendShapeReturn = ReturnType; +export const XY = ['x', 'y'] as const; +export const WH = ['width', 'height'] as const; + /** * Extend shape with parameters */ @@ -281,16 +288,16 @@ export function subPixelOptimizeLine( /** * Sub pixel optimize rect for canvas */ -export function subPixelOptimizeRect(param: { +export function subPixelOptimizeRect( shape: { x: number, y: number, width: number, height: number }, style: { - lineWidth: number + lineWidth?: number } -}) { - subPixelOptimizeUtil.subPixelOptimizeRect(param.shape, param.shape, param.style); - return param; +) { + subPixelOptimizeUtil.subPixelOptimizeRect(shape, shape, style); + return shape; } /** @@ -562,6 +569,88 @@ function nearZero(val: number) { return val <= (1e-6) && val >= -(1e-6); } +/** + * NOTE: + * A negative-width/height rect (due to negative margins) is not supported; + * it will be clampped to zero width/height. + * Although negative-width/height rects can be defined reasonably following the + * similar sense in CSS, but they are rarely used, hard to understand and complicated. + * + * @param rect Assume its width/height >= 0 if existing. + * x/y/width/height is allowed to be NaN, + * for the case that only x/width or y/height is intended to be computed. + * @param delta + * If be `number[]`, should be `[top, right, bottom, left]`, + * which can be used in padding or margin case. + * @see `normalizeCssArray` in `util/format.ts` + * If be `number`, it means [delta, delta, delta, delta], + * which can be used in lineWidth (borderWith) case, + * [NOTICE]: commonly pass lineWidth / 2, following the convention that border is + * half inside half outside of the rect. + * @param shrinkOrExpand + * `true` - shrink if `delta[i]` is positive, commmonly used in `padding` case. + * `false` - expand if `delta[i]` is positive, commmonly used in `margin` case. (default) + * @param noNegative + * `true` - negative `delta[i]` will be clampped to 0. + * `false` - No clamp to `delta`. (defualt). + * @return The input `rect`. + */ +export function expandOrShrinkRect( + rect: TRect, + delta: number[] | number | NullUndefined, + shrinkOrExpand: boolean, + noNegative: boolean +): TRect { + if (delta == null) { + return rect; + } + else if (isNumber(delta)) { + _tmpExpandRectDelta[0] = _tmpExpandRectDelta[1] = _tmpExpandRectDelta[2] = _tmpExpandRectDelta[3] = delta; + } + else { + _tmpExpandRectDelta[0] = delta[0]; + _tmpExpandRectDelta[1] = delta[1]; + _tmpExpandRectDelta[2] = delta[2]; + _tmpExpandRectDelta[3] = delta[3]; + } + if (noNegative) { + _tmpExpandRectDelta[0] = Math.max(0, _tmpExpandRectDelta[0]); + _tmpExpandRectDelta[1] = Math.max(0, _tmpExpandRectDelta[1]); + _tmpExpandRectDelta[2] = Math.max(0, _tmpExpandRectDelta[2]); + _tmpExpandRectDelta[3] = Math.max(0, _tmpExpandRectDelta[3]); + } + if (shrinkOrExpand) { + _tmpExpandRectDelta[0] = -_tmpExpandRectDelta[0]; + _tmpExpandRectDelta[1] = -_tmpExpandRectDelta[1]; + _tmpExpandRectDelta[2] = -_tmpExpandRectDelta[2]; + _tmpExpandRectDelta[3] = -_tmpExpandRectDelta[3]; + } + expandRectOnOneDimension(rect, _tmpExpandRectDelta, 'x', 'width', 3, 1); + expandRectOnOneDimension(rect, _tmpExpandRectDelta, 'y', 'height', 0, 2); + + return rect; +} +const _tmpExpandRectDelta = [0, 0, 0, 0]; +function expandRectOnOneDimension( + rect: RectLike, delta: number[], xy: 'x' | 'y', wh: 'width' | 'height', ltIdx: 3 | 0, rbIdx: 1 | 2 +): void { + const deltaSum = delta[rbIdx] + delta[ltIdx]; + const oldSize = rect[wh]; + rect[wh] += deltaSum; + if (rect[wh] < 0) { + rect[wh] = 0; + // Try to make the position of the zero rect reasonable in most visual cases. + rect[xy] += ( + delta[ltIdx] >= 0 ? -delta[ltIdx] + : delta[rbIdx] >= 0 ? oldSize + delta[rbIdx] + : Math.abs(deltaSum) > 1e-8 ? oldSize * delta[ltIdx] / deltaSum + : 0 + ); + } + else { + rect[xy] -= delta[ltIdx]; + } +} export function setTooltipConfig(opt: { el: Element, @@ -635,6 +724,129 @@ export function traverseElements(els: Element | Element[] | undefined | null, cb } } +export function retrieveZInfo( + model: Model>>, +): { + z: ComponentOption['z'] + zlevel: ComponentOption['zlevel'] +} { + return { + z: model.get('z') || 0, + zlevel: model.get('zlevel') || 0, + }; +} + +/** + * Assume all of the elements has the same `z` and `zlevel`. + */ +export function calcZ2Range(el: Element): { + min: number + max: number +} { + let max = -Infinity; + let min = Infinity; + traverseElement(el, el => { + visitEl(el); + visitEl(el.getTextContent()); + visitEl(el.getTextGuideLine()); + }); + function visitEl(el: Element): void { + if (!el || el.isGroup) { + return; + } + const currentStates = el.currentStates; + if (currentStates.length) { + for (let idx = 0; idx < currentStates.length; idx++) { + calcZ2(el.states[currentStates[idx]] as Displayable); + } + } + calcZ2(el as Displayable); + } + function calcZ2(entity: Pick): void { + if (entity) { + const z2 = entity.z2; + // Consider z2 may be NullUndefined + if (z2 > max) { + max = z2; + } + if (z2 < min) { + min = z2; + } + } + } + if (min > max) { + min = max = 0; + } + return {min, max}; +} + +export function traverseUpdateZ( + el: Element, + z: number, + zlevel: number, +): void { + doUpdateZ(el, z, zlevel); +} + +function doUpdateZ( + el: Element, + z: number, + zlevel: number +): number { + let maxZ2 = -Infinity; + + // `ignoreModelZ` is used to intentionally lift elements to cover other elements, + // where maxZ2 (for label.z2) should also not be counted for its parents. + if ((el as ExtendedElement).ignoreModelZ) { + return maxZ2; + } + + // Group may also have textContent + const label = el.getTextContent(); + const labelLine = el.getTextGuideLine(); + const isGroup = el.isGroup; + + if (isGroup) { + // set z & zlevel of children elements of Group + const children = (el as Group).childrenRef(); + for (let i = 0; i < children.length; i++) { + maxZ2 = Math.max( + doUpdateZ( + children[i], + z, + zlevel, + ), + maxZ2 + ); + } + } + else { + // not Group + (el as Displayable).z = z; + (el as Displayable).zlevel = zlevel; + + maxZ2 = Math.max((el as Displayable).z2 || 0, maxZ2); + } + + // always set z and zlevel if label/labelLine exists + if (label) { + label.z = z; + label.zlevel = zlevel; + // lift z2 of text content + // TODO if el.emphasis.z2 is spcefied, what about textContent. + isFinite(maxZ2) && (label.z2 = maxZ2 + 2); + } + if (labelLine) { + const textGuideLineConfig = el.textGuideLineConfig; + labelLine.z = z; + labelLine.zlevel = zlevel; + isFinite(maxZ2) + && (labelLine.z2 = maxZ2 + (textGuideLineConfig && textGuideLineConfig.showAbove ? 1 : -1)); + } + return maxZ2; +} + + // Register built-in shapes. These shapes might be overwritten // by users, although we do not recommend that. registerShape('circle', Circle); diff --git a/src/util/layout.ts b/src/util/layout.ts index dcf2f77626..b8a8dc9bf7 100644 --- a/src/util/layout.ts +++ b/src/util/layout.ts @@ -23,16 +23,27 @@ import * as zrUtil from 'zrender/src/core/util'; import BoundingRect, { RectLike } from 'zrender/src/core/BoundingRect'; import {parsePercent} from './number'; import * as formatUtil from './format'; -import { BoxLayoutOptionMixin, CircleLayoutOptionMixin, ComponentLayoutMode, SeriesOption } from './types'; +import { + BoxLayoutOptionMixin, CircleLayoutOptionMixin, NullUndefined, ComponentLayoutMode, SeriesOption, + PreserveAspectMixin, + ComponentOption +} from './types'; import Group from 'zrender/src/graphic/Group'; import { SectorShape } from 'zrender/src/graphic/shape/Sector'; import Element from 'zrender/src/Element'; import { Dictionary } from 'zrender/src/core/types'; -import SeriesModel from '../model/Series'; import ExtensionAPI from '../core/ExtensionAPI'; +import { error } from './log'; +import { BoxCoordinateSystemCoordFrom, getCoordForBoxCoordSys } from '../core/CoordinateSystem'; +import SeriesModel from '../model/Series'; +import type Model from '../model/Model'; +import type ComponentModel from '../model/Component'; const each = zrUtil.each; +/** + * @see {getLayoutRect} + */ export interface LayoutRect extends BoundingRect { margin: number[] } @@ -155,61 +166,67 @@ export const vbox = zrUtil.curry(boxLayout, 'vertical'); */ export const hbox = zrUtil.curry(boxLayout, 'horizontal'); -/** - * If x or x2 is not specified or 'center' 'left' 'right', - * the width would be as long as possible. - * If y or y2 is not specified or 'middle' 'top' 'bottom', - * the height would be as long as possible. - */ -export function getAvailableSize( - positionInfo: { - left?: number | string - top?: number | string - right?: number | string - bottom?: number | string - }, - containerRect: { width: number, height: number }, - margin?: number[] | number -) { - const containerWidth = containerRect.width; - const containerHeight = containerRect.height; - - let x = parsePercent(positionInfo.left, containerWidth); - let y = parsePercent(positionInfo.top, containerHeight); - let x2 = parsePercent(positionInfo.right, containerWidth); - let y2 = parsePercent(positionInfo.bottom, containerHeight); - - (isNaN(x) || isNaN(parseFloat(positionInfo.left as string))) && (x = 0); - (isNaN(x2) || isNaN(parseFloat(positionInfo.right as string))) && (x2 = containerWidth); - (isNaN(y) || isNaN(parseFloat(positionInfo.top as string))) && (y = 0); - (isNaN(y2) || isNaN(parseFloat(positionInfo.bottom as string))) && (y2 = containerHeight); - - margin = formatUtil.normalizeCssArray(margin || 0); +export function getBoxLayoutParams(boxLayoutModel: Model, ignoreParent: boolean) { return { - width: Math.max(x2 - x - margin[1] - margin[3], 0), - height: Math.max(y2 - y - margin[0] - margin[2], 0) + left: boxLayoutModel.getShallow('left', ignoreParent), + top: boxLayoutModel.getShallow('top', ignoreParent), + right: boxLayoutModel.getShallow('right', ignoreParent), + bottom: boxLayoutModel.getShallow('bottom', ignoreParent), + width: boxLayoutModel.getShallow('width', ignoreParent), + height: boxLayoutModel.getShallow('height', ignoreParent) }; } -export function getViewRect(seriesModel: SeriesModel, api: ExtensionAPI) { - return getLayoutRect( - seriesModel.getBoxLayoutParams(), { - width: api.getWidth(), - height: api.getHeight() - } - ); +type CircleLayoutSeriesOption = SeriesOption & CircleLayoutOptionMixin<{ + // `center: string | number` has been accepted in series.pie. + centerExtra: string | number +}>; + +function getViewRectAndCenterForCircleLayout( + seriesModel: SeriesModel, + api: ExtensionAPI +) { + const layoutRef = createBoxLayoutReference(seriesModel, api, { + enableLayoutOnlyByCenter: true, + }); + const boxLayoutParams = seriesModel.getBoxLayoutParams(); + + let viewRect: LayoutRect; + let center: number[]; + if (layoutRef.type === BoxLayoutReferenceType.point) { + center = layoutRef.refPoint; + // `viewRect` is required in `pie/labelLayout.ts`. + viewRect = getLayoutRect( + boxLayoutParams, {width: api.getWidth(), height: api.getHeight()} + ); + } + else { // layoutRef.type === layout.BoxLayoutReferenceType.rect + const centerOption = seriesModel.get('center'); + const centerOptionArr = zrUtil.isArray(centerOption) + ? centerOption : [centerOption, centerOption]; + viewRect = getLayoutRect( + boxLayoutParams, layoutRef.refContainer + ); + center = layoutRef.boxCoordFrom === BoxCoordinateSystemCoordFrom.coord2 + ? layoutRef.refPoint // option `series.center` has been used as coord. + : [ + parsePercent(centerOptionArr[0], viewRect.width) + viewRect.x, + parsePercent(centerOptionArr[1], viewRect.height) + viewRect.y, + ]; + } + + return {viewRect, center}; } -export function getCircleLayout( - seriesModel: SeriesModel, +export function getCircleLayout( + seriesModel: SeriesModel, api: ExtensionAPI -): - Pick { - const viewRect = getViewRect(seriesModel, api); +): Pick & {viewRect: LayoutRect} { // center can be string or number when coordinateSystem is specified - let center = seriesModel.get('center'); + const {viewRect, center} = getViewRectAndCenterForCircleLayout(seriesModel, api); + let radius = seriesModel.get('radius'); if (!zrUtil.isArray(radius)) { @@ -221,54 +238,50 @@ export function getCircleLayout containerWidth / containerHeight) { width = containerWidth * 0.8; } @@ -359,15 +375,150 @@ export function getLayoutRect( height = containerHeight - verticalMargin - top - (bottom || 0); } - const rect = new BoundingRect(left + margin[3], top + margin[0], width, height) as LayoutRect; + const rect = new BoundingRect( + (containerRect.x || 0) + left + margin[3], + (containerRect.y || 0) + top + margin[0], + width, + height + ) as LayoutRect; rect.margin = margin; - if (containerRect.x) { - rect.x += containerRect.x; + + return rect; +} + +/** + * PENDING: + * when preserveAspect: 'cover' and aspect is near Infinity + * or when preserveAspect: 'contain' and aspect is near 0, + * the result width or height is near Inifity. It's logically correct, + * Therefore currently we do not handle it, until bad cases arise. + */ +export function applyPreserveAspect( + component: ComponentModel, + layoutRect: LayoutRect, + // That is, `width / height`. + // Assume `aspect` is positive. + aspect: number, +): LayoutRect { + const preserveAspect = component.getShallow('preserveAspect', true); + if (!preserveAspect) { + return layoutRect; } - if (containerRect.y) { - rect.y += containerRect.y; + + const actualAspect = layoutRect.width / layoutRect.height; + + if (Math.abs(Math.atan(aspect) - Math.atan(actualAspect)) < 1e-9) { + return layoutRect; } - return rect; + + const preserveAspectAlign = component.getShallow('preserveAspectAlign', true); + const preserveAspectVerticalAlign = component.getShallow('preserveAspectVerticalAlign', true); + const layoutOptInner: BoxLayoutOptionMixin = {width: layoutRect.width, height: layoutRect.height}; + const isCover = preserveAspect === 'cover'; + + if ((actualAspect > aspect && !isCover) || (actualAspect < aspect && isCover)) { + layoutOptInner.width = layoutRect.height * aspect; + preserveAspectAlign === 'left' ? (layoutOptInner.left = 0) + : preserveAspectAlign === 'right' ? (layoutOptInner.right = 0) + : (layoutOptInner.left = 'center'); + } + else { + layoutOptInner.height = layoutRect.width / aspect; + preserveAspectVerticalAlign === 'top' ? (layoutOptInner.top = 0) + : preserveAspectVerticalAlign === 'bottom' ? (layoutOptInner.bottom = 0) + : (layoutOptInner.top = 'middle'); + } + return getLayoutRect(layoutOptInner, layoutRect); +} + + +type CreateBoxLayoutReferenceOpt = { + // Use this only if: + // - Intending to layout based on coord sys that can not get a rect from `dataToLayout`. + // - Can layout based only on center but a rect is not essential, such as pie, chord. + enableLayoutOnlyByCenter?: TEnableByCenter; +}; +export const BoxLayoutReferenceType = { + rect: 1, + point: 2, +} as const; +export type BoxLayoutReferenceType = (typeof BoxLayoutReferenceType)[keyof typeof BoxLayoutReferenceType]; + +type BoxLayoutReferenceResult = TEnableByCenter extends true + ? (BoxLayoutReferenceRectResult | BoxLayoutReferencePointResult) + : BoxLayoutReferenceRectResult; +type BoxLayoutReferenceRectResult = { + // This is the defualt way. + type: typeof BoxLayoutReferenceType.rect; + refContainer: LayoutRect; + refPoint: number[]; // The center of rect in this case. + boxCoordFrom: BoxCoordinateSystemCoordFrom | NullUndefined; +}; +type BoxLayoutReferencePointResult = { + // This is available only if `enableLayoutOnlyByCenter: true` + // and `layoutRect` is not available. + type: typeof BoxLayoutReferenceType.point; + refPoint: number[]; + boxCoordFrom: BoxCoordinateSystemCoordFrom | NullUndefined; +}; + +/** + * Uniformly calculate layout reference (rect or center) based on either: + * - viewport: + * - Get `refContainer` as `{x: 0, y: 0, width: api.getWidth(), height: api.getHeight()}` + * - coordinate system, which can serve in several ways: + * - Use `dataToPoint` to get the `refPoint`, such as, in cartesian2d coord sys. + * - Use `dataToLayout` to get the `refContainer`, such as, in matrix coord sys. + */ +export function createBoxLayoutReference( + model: ComponentModel, + api: ExtensionAPI, + opt?: CreateBoxLayoutReferenceOpt +): BoxLayoutReferenceResult { + + let refContainer: RectLike | NullUndefined; + let refPoint: number[] | NullUndefined; + let layoutRefType: BoxLayoutReferenceType | NullUndefined; + + const boxCoordSys = model.boxCoordinateSystem; + let boxCoordFrom: BoxCoordinateSystemCoordFrom | NullUndefined; + if (boxCoordSys) { + const {coord, from} = getCoordForBoxCoordSys(model); + // Do not use `clamp` in `dataToLayout` and `dataToPoint`, because: + // 1. Should support overflow (such as, by dataZoom), where NaN should be in the result. + // 2. Be consistent with the way used in `series.data` + if (boxCoordSys.dataToLayout) { + layoutRefType = BoxLayoutReferenceType.rect; + boxCoordFrom = from; + const result = boxCoordSys.dataToLayout(coord); + refContainer = result.contentRect || result.rect; + } + else if (opt && opt.enableLayoutOnlyByCenter && boxCoordSys.dataToPoint) { + layoutRefType = BoxLayoutReferenceType.point; + boxCoordFrom = from; + refPoint = boxCoordSys.dataToPoint(coord); + } + else { + if (__DEV__) { + error(`${model.type}[${model.componentIndex}]` + + ` layout based on ${boxCoordSys.type} is not supported.` + ); + } + } + } + + if (layoutRefType == null) { + layoutRefType = BoxLayoutReferenceType.rect; + } + + if (layoutRefType === BoxLayoutReferenceType.rect) { + if (!refContainer) { + refContainer = {x: 0, y: 0, width: api.getWidth(), height: api.getHeight()}; + } + refPoint = [refContainer.x + refContainer.width / 2, refContainer.y + refContainer.height / 2]; + } + + return {type: layoutRefType, refContainer, refPoint, boxCoordFrom} as BoxLayoutReferenceResult; } @@ -414,7 +565,7 @@ export function getLayoutRect( export function positionElement( el: Element, positionInfo: BoxLayoutOptionMixin, - containerRect: {width: number, height: number}, + containerRect: GetLayoutRectInputContainerRect, margin?: number[] | number, opt?: { hv: [1 | 0 | boolean, 1 | 0 | boolean], @@ -624,15 +775,3 @@ export function copyLayoutParams(target: BoxLayoutOptionMixin, source: BoxLayout }); return target; } - -/** - * Apply pedding (CSS like) to a rect, and return the input rect. - */ -export function applyPedding(rect: TRect, pedding?: number | number[]): TRect { - const peddingArr = formatUtil.normalizeCssArray(pedding || 0); - rect.x += peddingArr[3]; - rect.y += peddingArr[0]; - rect.width -= peddingArr[1] + peddingArr[3]; - rect.height -= peddingArr[0] + peddingArr[2]; - return rect; -} diff --git a/src/util/model.ts b/src/util/model.ts index 7b6ca499dd..77758454fb 100644 --- a/src/util/model.ts +++ b/src/util/model.ts @@ -45,14 +45,16 @@ import { Payload, OptionId, OptionName, - InterpolatableValue + InterpolatableValue, + NullUndefined, } from './types'; import { Dictionary } from 'zrender/src/core/types'; import SeriesModel from '../model/Series'; import CartesianAxisModel from '../coord/cartesian/AxisModel'; import GridModel from '../coord/cartesian/GridModel'; import { isNumeric, getRandomIdBase, getPrecision, round } from './number'; -import { warn } from './log'; +import { error, warn } from './log'; +import type Model from '../model/Model'; function interpolateNumber(p0: number, p1: number, percent: number): number { return (p1 - p0) * percent + p0; @@ -777,6 +779,7 @@ export type ModelFinderObject = { xAxisIndex?: ModelFinderIndexQuery, xAxisId?: ModelFinderIdQuery, xAxisName?: ModelFinderNameQuery yAxisIndex?: ModelFinderIndexQuery, yAxisId?: ModelFinderIdQuery, yAxisName?: ModelFinderNameQuery gridIndex?: ModelFinderIndexQuery, gridId?: ModelFinderIdQuery, gridName?: ModelFinderNameQuery + matrixIndex?: ModelFinderIndexQuery, matrixId?: ModelFinderIdQuery, matrixName?: ModelFinderNameQuery dataIndex?: number, dataIndexInside?: number // ... (can be extended) }; @@ -951,16 +954,32 @@ export function queryReferringComponents( } if (indexOption === 'none' || indexOption === false) { - assert(opt.enableNone, '`"none"` or `false` is not a valid value on index option.'); - result.models = []; - return result; + if (opt.enableNone) { + result.models = []; + return result; + } + else { + // Do not throw; consider if some component previously does not use this method, + // and start to use it, need to be fault-tolerant for backward compatibility. + if (__DEV__) { + error('`"none"` or `false` is not a valid value on index option.'); + } + indexOption = -1; // Can not query by index but may still query by id/name if specified. + } } // `queryComponents` will return all components if // both all of index/id/name are null/undefined. if (indexOption === 'all') { - assert(opt.enableAll, '`"all"` is not a valid value on index option.'); - indexOption = idOption = nameOption = null; + if (opt.enableAll) { + indexOption = idOption = nameOption = null; + } + else { + if (__DEV__) { + error('`"all"` is not a valid value on index option.'); + } + indexOption = -1; + } } result.models = ecModel.queryComponents({ mainType: mainType, @@ -1094,3 +1113,58 @@ export function interpolateRawValues( return interpolated; } } + +/** + * Use an iterator to avoid exposing the internal list or duplicating it + * for the outside traveller, and no extra heap allocation. + * @usage + * for (const it = resetIterator(); it.next();) { + * const item = it.item; + * const key = it.key; + * const itIdx = it.itIdx; + * // ... + * } + * @usage + * const it = resetIterator(); + * while (it.next()) { ... } + * @usage + * for (resetIterator(it); it.next();) { ... } + */ +export class ListIterator { + + private _idx: number; + private _end: number; + private _list: TItem[]; + private _step: number; + + item: TItem | NullUndefined; + key: number; + + /** + * The loop condition is `idx < end` if `step > 0`; + * The loop condition is `idx >= end` if `step < 0`. + * + * @param end By default `list.length` if `step > 0`; `0` if `step < 0`. + * @param step By default `1`. + */ + reset(list: TItem[], start: number, end?: number, step?: number): ListIterator { + this._list = list; + this._step = step = step || 1; + this._idx = start; + this._end = end != null ? end : step > 0 ? list.length : 0; + + this.item = null; + this.key = NaN; + + return this; + } + + next(): boolean { + if (this._step > 0 ? this._idx < this._end : this._idx >= this._end) { + this.item = this._list[this._idx]; + this.key = this._idx = this._idx + this._step; + return true; + } + return false; + } +} diff --git a/src/util/number.ts b/src/util/number.ts index cd6f42994b..85807cb3c3 100644 --- a/src/util/number.ts +++ b/src/util/number.ts @@ -37,6 +37,9 @@ function _trim(str: string): string { return str.replace(/^\s+|\s+$/g, ''); } +export const mathMin = Math.min; +export const mathMax = Math.max; + /** * Linear mapping a value from domain to range * @param val @@ -100,33 +103,48 @@ export function linearMap( } /** - * Convert a percent string to absolute number. - * Returns NaN if percent is not a valid string or number + * Preserve the name `parsePercent` for backward compatibility, + * and it's effectively published as `echarts.number.parsePercent`. */ -export function parsePercent(percent: number | string, all: number): number { - switch (percent) { +export const parsePercent = parsePositionOption; + +/** + * @see {parsePositionSizeOption} and also accept a string preset. + * @see {PositionSizeOption} + */ +export function parsePositionOption(option: unknown, percentBase: number, percentOffset?: number): number { + switch (option) { case 'center': case 'middle': - percent = '50%'; + option = '50%'; break; case 'left': case 'top': - percent = '0%'; + option = '0%'; break; case 'right': case 'bottom': - percent = '100%'; + option = '100%'; break; } - if (zrUtil.isString(percent)) { - if (_trim(percent).match(/%$/)) { - return parseFloat(percent) / 100 * all; - } + return parsePositionSizeOption(option, percentBase, percentOffset); +} - return parseFloat(percent); +/** + * Accept number, or numeric stirng (`'123'`), or percentage ('100%'), as x/y/width/height pixel number. + * If null/undefined or invalid, return NaN. + * (But allow JS type coercion (`+option`) due to backward compatibility) + * @see {PositionSizeOption} + */ +export function parsePositionSizeOption(option: unknown, percentBase: number, percentOffset?: number): number { + if (zrUtil.isString(option)) { + if (_trim(option).match(/%$/)) { + return parseFloat(option) / 100 * percentBase + (percentOffset || 0); + } + return parseFloat(option); } - - return percent == null ? NaN : +percent; + // Allow flexible input due to backward compatibility. + return option == null ? NaN : +option; } /** diff --git a/src/util/types.ts b/src/util/types.ts index ef21e13e40..93ed67d585 100644 --- a/src/util/types.ts +++ b/src/util/types.ts @@ -411,9 +411,10 @@ export type TooltipOrderMode = 'valueAsc' | 'valueDesc' | 'seriesAsc' | 'seriesD // Data and dimension related types // --------------------------------- +// Represents the values in series.data that need to be treated in a non-numeric way. +// Can be used by axis.type 'category' (i.e. scale.type 'ordinal'). // Finally the user data will be parsed and stored in `list._storage`. // `NaN` represents "no data" (raw data `null`/`undefined`/`NaN`/`'-'`). -// `Date` will be parsed to timestamp. // Ordinal/category data will be parsed to its index if possible, otherwise // keep its original string in list._storage. // Check `convertValue` for more details. @@ -459,6 +460,24 @@ export type ParsedValueNumeric = number | OrdinalNumber; */ export type ScaleDataValue = ParsedValueNumeric | OrdinalRawValue | Date; + +/** + * - `ScaleDataValue`: + * e.g. geo accept that primative input, like `convertToPixel('some_place')`; + * Some coord sys, such as 'cartesian2d', also supports that for only query only a single axis. + * - `ScaleDataValue[]`: + * This is the most common case, each array item represent each data in + * every dimension required by the coord sys. e.g., `[12, 98]` represents `[xData, yData]`. + * - `(ScaleDataValue[])[]`: + * represents `[data_range_x, data_range_y]`. e.g., `dataToPoint([[5, 600], [8889, 9000]])`, + * represents data range `[5, 600]` in x, and `[8889, 9000]` in y. + * Can be also `[5, [8999, 9000]]`. + */ +export type CoordinateSystemDataCoord = + (ScaleDataValue | NullUndefined) + | (ScaleDataValue | NullUndefined)[] + | (ScaleDataValue | ScaleDataValue[] | NullUndefined)[]; + export type AxisBreakOption = { start: ScaleDataValue, end: ScaleDataValue, @@ -562,6 +581,22 @@ export interface OrdinalScaleTick extends ScaleTick { value: number }; +/** + * Return type of API `CoordinateSystem['dataToLayout']`, expose to users. + */ +export interface CoordinateSystemDataLayout { + // Base layout rect for a data item. + rect?: RectLike; + // Commonly equals or shrinked from `rect, may considered padding and border + // (depends on every coordinate system). + contentRect?: RectLike; + // Only available in matrix coordinate system. + matrixXYLocatorRange?: number[][]; + + // May extend. +} + + // Can only be string or index, because it is used in object key in some code. // Making the type alias here just intending to show the meaning clearly in code. export type DimensionIndex = number; @@ -673,6 +708,8 @@ export type ECUnitOption = { useUTC?: boolean hoverLayerThreshold?: number + legacyViewCoordSysCenterBase?: boolean + [key: string]: ComponentOption | ComponentOption[] | Dictionary | unknown stateAnimation?: AnimationOption @@ -913,21 +950,28 @@ export interface ColorPaletteOptionMixin { color?: ZRColor | ZRColor[] colorLayer?: ZRColor[][] } + /** * Mixin of option set to control the box layout of each component. */ export interface BoxLayoutOptionMixin { - width?: number | string; - height?: number | string; - top?: number | string; - right?: number | string; - bottom?: number | string; - left?: number | string; + width?: PositionSizeOption; + height?: PositionSizeOption; + top?: PositionSizeOption; + right?: PositionSizeOption; + bottom?: PositionSizeOption; + left?: PositionSizeOption; } +/** + * Need to be parsed by `parsePositionOption` or `parsePositionSizeOption`. + * Accept number, or numeric stirng (`'123'`), or percentage ('100%'), as x/y/width/height pixel number. + * If null/undefined or invalid, return NaN. + */ +export type PositionSizeOption = number | string; -export interface CircleLayoutOptionMixin { +export interface CircleLayoutOptionMixin { // Can be percent - center?: (number | string)[] + center?: (number | string)[] | TNuance['centerExtra'] // Can specify [innerRadius, outerRadius] radius?: (number | string)[] | number | string } @@ -1018,6 +1062,24 @@ export interface RoamOptionMixin { * If enable roam. can be specified 'scale' or 'move' */ roam?: boolean | 'pan' | 'move' | 'zoom' | 'scale' + /** + * Hover over an area where roaming is triggered. + * - if `null`/`undefined`, the trigger area is + * the intersection of "self bounding rect" and "clipping rect (if any)". + * - if 'global', the trigger area is + * the intersection of "the entire canvas" and "clipping rect (if any)". + * NOTE: + * The clipping rect, which can be enabled by `clip: true`, is typically the layout rect. + * The layout rect is typically determined by option `left`/`right`/`top`/`bottom`/`width`/`height`, some + * components/series, such as `geo` and `series.map` can also be determined by `layoutCenter`/`layoutSize`, + * and may modified by `preserveAspect`. + * + * PENDING: do we need to support to only trigger roaming on the shapes themselves, + * rather than the bounding rect? + * PENDING: do we need to support to check by the laytout rect? But in this case, + * `roamTrigger: 'global', clip: true` is more reasonable. + */ + roamTrigger?: 'global' | 'selfRect' | NullUndefined /** * Current center position. */ @@ -1033,6 +1095,21 @@ export interface RoamOptionMixin { } } +export interface PreserveAspectMixin { + // Suppose a "viewport" is decided by `left`/`right`/`top`/`bottom`/`width`/`height`, + // - null/undefined/false (default): aspect ratio will not be preserved, but stretched to fill + // the "viewport". + // - 'contain'/true: The aspect ratio is preserved; the viewRect is contained by the "viewport", + // and scaled up as much as possible to meet the "viewport". + // - 'cover': The aspect ratio is preserved; the viewRect covers the "viewport", and scaled down + // as much as possible to meet the "viewport". + preserveAspect?: boolean | 'contain' | 'cover'; + // By default 'center' + preserveAspectAlign?: 'left' | 'right' | 'center'; + // By default 'middle' + preserveAspectVerticalAlign?: 'top' | 'bottom' | 'middle'; +} + // TODO: TYPE value type? export type SymbolSizeCallback = (rawValue: any, params: T) => number | number[]; export type SymbolCallback = (rawValue: any, params: T) => string; @@ -1161,7 +1238,7 @@ export interface TextCommonOption extends ShadowOptionMixin { borderRadius?: number | number[] padding?: number | number[] - width?: number | string// Percent + width?: number | string // Percent height?: number textBorderColor?: string textBorderWidth?: number @@ -1183,14 +1260,16 @@ export interface LabelFormatterCallback { * LabelOption is an option set to control the style of labels. * Include color, background, shadow, truncate, rotation, distance, etc.. */ -export interface LabelOption extends TextCommonOption { +export interface LabelOption< + TNuance extends {positionExtra: unknown} = {positionExtra: never} +> extends TextCommonOption { /** * If show label */ show?: boolean // TODO: TYPE More specified 'inside', 'insideTop'.... // x, y can be both percent string or number px. - position?: ElementTextConfig['position'] + position?: ElementTextConfig['position'] | TNuance['positionExtra'] distance?: number rotate?: number offset?: number[] @@ -1204,6 +1283,7 @@ export interface LabelOption extends TextCommonOption { overflow?: TextStyleProps['overflow'] ellipsis?: TextStyleProps['ellipsis'] + lineOverflow?: TextStyleProps['lineOverflow'] silent?: boolean precision?: number | 'auto' @@ -1228,8 +1308,11 @@ export interface LabelExtendedText extends ZRText { __marginType?: (typeof LabelMarginType)[keyof typeof LabelMarginType] } -export interface SeriesLabelOption extends LabelOption { - formatter?: string | LabelFormatterCallback +export interface SeriesLabelOption< + TCallbackDataParams extends CallbackDataParams = CallbackDataParams, + TNuance extends {positionExtra: unknown} = {positionExtra: never} +> extends LabelOption { + formatter?: string | LabelFormatterCallback } /** @@ -1634,8 +1717,25 @@ export interface ComponentOption { z?: number; zlevel?: number; + + coordinateSystem?: string + coordinateSystemUsage?: CoordinateSystemUsageOption + coord?: CoordinateSystemDataCoord } +/** + * - "data": Use it as "dataCoordSys", each data item is laid out based on a coord sys. + * - "box": Use it as "boxCoordSys", the overall bounding rect or anchor point is calculated based on a coord sys. + * e.g., + * grid rect (cartesian rect) is calculate based on matrix/calendar coord sys; + * pie center is calculated based on calendar/cartesian; + * + * The default value (if not declared in option `coordinateSystemUsage`): + * For series, be "data", since this is the most case and backward compatible. + * For non-series components, be "box", since "data" is not applicable. + */ +export type CoordinateSystemUsageOption = 'data' | 'box'; + export type BlurScope = 'coordinateSystem' | 'series' | 'global'; /** @@ -1767,10 +1867,6 @@ export interface SeriesOption< progressive?: number | false progressiveThreshold?: number progressiveChunkMode?: 'mod' - /** - * Not available on every series - */ - coordinateSystem?: string hoverLayerThreshold?: number @@ -1837,6 +1933,11 @@ export interface SeriesOnCalendarOptionMixin { calendarId?: string } +export interface ComponentOnMatrixOptionMixin { + matrixIndex?: number + matrixId?: string +} + export interface SeriesLargeOptionMixin { large?: boolean largeThreshold?: number diff --git a/test/build/mktest-tpl.html b/test/build/mktest-tpl.html index 84218ef3f3..15a0cec0a7 100644 --- a/test/build/mktest-tpl.html +++ b/test/build/mktest-tpl.html @@ -28,14 +28,19 @@ - + + - @@ -48,33 +53,101 @@ - diff --git a/test/calendar-other-coord-sys.html b/test/calendar-other-coord-sys.html new file mode 100644 index 0000000000..35004b94bd --- /dev/null +++ b/test/calendar-other-coord-sys.html @@ -0,0 +1,882 @@ + + + + + + + calendar + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + diff --git a/test/geo-map-roam.html b/test/geo-map-roam.html new file mode 100644 index 0000000000..6d943b2dd7 --- /dev/null +++ b/test/geo-map-roam.html @@ -0,0 +1,556 @@ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + diff --git a/test/geo-svg-demo.html b/test/geo-svg-demo.html index 4f76e38b8f..ab99d94a84 100644 --- a/test/geo-svg-demo.html +++ b/test/geo-svg-demo.html @@ -100,6 +100,7 @@ tooltip: { }, geo: { + clip: true, left: 10, right: '50%', map: 'seatmap', diff --git a/test/graph-layout-roam.html b/test/graph-layout-roam.html new file mode 100644 index 0000000000..6658d887bf --- /dev/null +++ b/test/graph-layout-roam.html @@ -0,0 +1,515 @@ + + + + + + + + + + + + + + + +
+ + + + \ No newline at end of file diff --git a/test/graph-thumbnail.html b/test/graph-thumbnail.html index 86a17840bf..3f109b9824 100644 --- a/test/graph-thumbnail.html +++ b/test/graph-thumbnail.html @@ -19,9 +19,9 @@ }
-
+
- - + + + + + + \ No newline at end of file diff --git a/test/lib/config.js b/test/lib/config.js index caa9826d66..bc49eefb45 100644 --- a/test/lib/config.js +++ b/test/lib/config.js @@ -55,6 +55,8 @@ var ecDistPath; if (params.__ECDIST__ && !params.__CASE_FRAME__) { ecDistPath = ({ + 'dist': '../dist/echarts', + 'dist/echarts.simple': '../dist/echarts.simple', 'webpack-req-ec': '../../echarts-boilerplate/echarts-webpack/dist/webpack-req-ec', 'webpack-req-eclibec': '../../echarts-boilerplate/echarts-webpack/dist/webpack-req-eclibec', 'webpackold-req-ec': '../../echarts-boilerplate/echarts-webpackold/dist/webpackold-req-ec', @@ -91,7 +93,7 @@ decorateGlobalRequire(); } - async function decorateGlobalRequire() { + function decorateGlobalRequire() { if (window.__ECHARTS__DEFAULT__THEME__ == null) { return; } diff --git a/test/lib/draggable.js b/test/lib/draggable.js index c5bf49d8cd..01741ef2a3 100644 --- a/test/lib/draggable.js +++ b/test/lib/draggable.js @@ -39,12 +39,18 @@ * @param {boolean} [opt.lockY=false] * @param {number} [opt.throttle=false] * @param {Function} [opt.onDrag] + * @param {Function} [opt.onResize] * @return {type} description */ init: function (mainEl, chart, opt) { opt = opt || {}; - var onDrag = opt.onDrag || $.proxy(chart.resize, chart); + var onDrag = opt.onDrag || function () { + chart.resize(); + if (opt.onResize) { + opt.onResize(); + } + } var onDragThrottled = chart ? onDrag : function () {}; if (opt.throttle) { diff --git a/test/lib/reset.css b/test/lib/reset.css index f1666779cd..6457741849 100644 --- a/test/lib/reset.css +++ b/test/lib/reset.css @@ -58,13 +58,33 @@ body > .main { * otherwise, some recorded visual test cases may fail. */ +.test-inputs { + background: #eee; +} .test-inputs button { margin: 10px 5px; } .test-inputs-fix-height { position: relative; overflow: scroll; - box-shadow: inset -3px -3px 3px rgba(0, 0, 0, 0.3); + box-sizing: border-box; + background: #eee; + padding: 0; +} +.test-inputs-fix-height::before { + content: 'Scroll ⬇'; + position: sticky; + float: right; + top: 0; + right: 0; + padding: 0 2px 0 5px; + height: 14px; + line-height: 14px; + font-size: 11px; + color: #333; + z-index: 9999; + background: #ccc; + text-align: right; } .test-inputs-style-compact button { margin-top: 2px; @@ -82,14 +102,23 @@ body > .main { margin-bottom: 2px; } .test-inputs-slider-input { - width: 129px; + width: 109px; height: 16px; } +.test-inputs-slider-no-delta-buttons .test-inputs-slider-input { + width: 129px; +} .test-inputs-style-compact .test-inputs-slider-input { - width: 90px; + width: 75px; } .test-inputs-slider-sub { margin-left: -10px; + margin-top: 0; + margin-bottom: 0; +} +.test-inputs-style-compact .test-inputs-slider-sub { + margin-top: 0; + margin-bottom: 0; } .test-inputs-slider span { vertical-align: middle; @@ -102,6 +131,48 @@ body > .main { vertical-align: middle; margin: 0 3px; } +.test-inputs-slider-btn-incdec { + display: inline-block; + vertical-align: middle; + width: 0; + height: 0; + border-style: solid; + background: none; + border-color: transparent; + padding: 0; + user-select: none; + cursor: pointer; +} +.test-inputs-slider-btn-decrease { + border-width: 5px 7px 5px 0; + border-right-color: rgb(48,119,226); + margin: 0 1px 0 2px; +} +.test-inputs-slider-btn-decrease:hover { + border-right-color: #245dc1; +} +.test-inputs-slider-btn-increase { + border-width: 5px 0 5px 7px; + border-left-color: rgb(48,119,226); + margin: 0 2px 0 1px; +} +.test-inputs-slider-btn-increase:hover { + border-left-color: #245dc1; +} +.test-inputs-slider-disabled .test-inputs-slider-btn-decrease { + border-right-color: #bbb; + cursor: default; +} +.test-inputs-slider-disabled .test-inputs-slider-btn-increase { + border-left-color: #bbb; + cursor: default; +} +.test-inputs-slider-disabled .test-inputs-slider-btn-decrease:hover { + border-left-color: #bbb; +} +.test-inputs-slider-disabled .test-inputs-slider-btn-increase:hover { + border-left-color: #bbb; +} .test-inputs-select { white-space: nowrap; display: inline-block; @@ -117,6 +188,9 @@ body > .main { vertical-align: middle; margin: 0 3px; } +.test-inputs-select-disabled span { + color: #aaa; +} .test-inputs-select select { vertical-align: middle; margin: 0; @@ -124,6 +198,74 @@ body > .main { font-size: 13.3333px; height: 19px; } +.test-inputs-groupset { + position: relative; + overflow: auto; + border: 1px solid #bbb; + padding: 0; + margin: 0 3px; /* margin-bottom will collapse. */ + background: #fff; + display: block; + width: auto; +} +.test-inputs-groupset.test-inputs-fix-height::before { + right: 0; + left: 0; + height: 12px; + line-height: 12px; + font-size: 9px; +} +.test-inputs-groupset-text { + position: sticky; + display: block; + top: 0; + left: 0; + padding: 0; + margin: 0; + padding-left: 10px; + color: #333; + z-index: 9998; + height: 12px; + line-height: 12px; + font-size: 9px; + background: #ccc; +} +.test-inputs-groupset-group { + position: relative; + margin: 5px 2px; + background: #fff; +} +.test-inputs-groupset-margin-bottom { + /* A workaround for margin collapse, without breaking parent CSS (may fail previous visual tests) */ + height: 3px; + position: relative; + display: block; + width: auto; +} +.test-inputs-hr { + position: relative; + background: #ccc; + padding: 0; + height: 1px; + border-width: 0; + margin-block: unset; + margin-inline: unset; + margin: 8px 2px; +} +.test-inputs-hr-text { + position: absolute; + color: #333; + background: #ddd; + right: 0; + top: -6px; + text-align: right; + vertical-align: middle; + white-space: nowrap; + font-size: 10px; + line-height: 10px; + padding: 2px 5px; + margin: 0; +} .test-chart-block { position: relative; @@ -144,6 +286,21 @@ body > .main { .test-chart-block-has-right .test-chart-block-left { margin-right: 320px; } +.test-chart-wrapper { + position: relative; + padding: 0; + margin: 0; + border-width: 0; +} +.test-bounding-rects { + position: absolute; + left: 0; + top: 0; + padding: 0; + margin: 0; + border-width: 0; + z-index: 999999; +} .test-info { padding-left: 10px; overflow: auto; diff --git a/test/lib/testHelper.js b/test/lib/testHelper.js index b005a99fb9..0b748e53cf 100644 --- a/test/lib/testHelper.js +++ b/test/lib/testHelper.js @@ -57,103 +57,222 @@ /** * @param {Object} opt - * @param {string|Array.} [opt.title] If array, each item is on a single line. + * @param {string|string[]} [opt.title] If array, each item is on a single line. * Can use '**abc**', means abc. - * @param {Option} opt.option - * @param {Object} [opt.info] info object to display. - * info can be updated by `chart.__testHelper.updateInfo(someInfoObj, 'some_info_key');` - * @param {string} [opt.infoKey='option'] - * @param {Object|Array} [opt.dataTable] - * @param {Array.} [opt.dataTables] Multiple dataTables. - * @param {number} [opt.dataTableLimit=DEFAULT_DATA_TABLE_LIMIT] - * @param {number} [opt.width] - * @param {number} [opt.height] - * @param {boolean} [opt.draggable] - * @param {boolean} [opt.lazyUpdate] - * @param {boolean} [opt.notMerge] - * @param {boolean} [opt.autoResize=true] + * @param {Option} opt.option The chart option. + * + * @param {number} [opt.width] Optional. Specify a different chart width. + * @param {number} [opt.height] Optional. Specify a different chart height. + * @param {boolean} [opt.notMerge] Optional. `chart.setOption(option, {norMerge});` + * @param {boolean} [opt.lazyUpdate] Optional. `chart.setOption(option, {lazyUpdate});` + * @param {boolean} [opt.autoResize=true] Optional. Enable chart auto response to window resize. + * @param {Function} [opt.onResize] Optional. Available when `opt.autoResize` or `opt.draggable` is true. + * @param {string} [opt.renderer] Optional. 'canvas' or 'svg'. DO NOT set it in formmal test cases; + * leave it controlled by __ECHARTS__DEFAULT__RENDERER__ for visual testing. + * + * @param {boolean} [opt.draggable] Optional. Add a draggable button to mutify the chart size. + * This feature require "test/lib/draggable.js" + * * @param {string} [opt.inputsStyle='normal'] Optional, can be 'normal', 'compact'. * Can be either `inputsStyle` or `buttonsStyle`. - * @param {number} [opt.inputsHeight] Default not fix height. If specified, a scroll + * @param {number} [opt.inputsHeight] Optional. By default not fix height. If specified, a scroll * bar will be displayed if overflow the height. In visual test, once a height changed * by adding something, the subsequent position will be changed, leading to test failures. * Fixing the height helps avoid this. * Can be either `inputsHeight` or `buttonsHeight`. - * @param {Array.|Object|Function} [opt.inputs] - * They are the same: `opt.buttons` `opt.button`, `opt.inputs`, `opt.input` - * It can be a function that return buttons configuration, like: + * @param {boolean} [opt.saveInputsInitialState] Optional. + * Required by `chart.__testHelper.restoreInputsToInitialState` + * @param {InputDefine[]|InputDefine|()=>InputDefine[]} [opt.inputs] Optional. + * definitions of button/range/select/br/hr. + * They are the same: `opt.buttons` `opt.button`, `opt.inputs`, `opt.input`. + * It can be a function that return inputs definitions, like: * inputs: chart => { return [{text: 'xxx', onclick: fn}, ...]; } - * Item can be these types: - * [{ - * // A button (default). - * text: 'xxx', - * // They are the same: `onclick`, `click` (capital insensitive) - * onclick: fn - * }, { - * // A range slider (HTML ). - * type: 'range', // Or 'slider' - * id: 'some_id', // Optional. Can be used in `getState` and `setState`. - * stateGroup: 'some_state_group', // Optional. Can be used in `getState` and `setState`. - * text: 'xxx', // Optional - * min: 0, // Optional - * max: 100, // Optional - * value: 30, // Optional. Must be a number. - * step: 1, // Optional - * // They are the same: `oninput` `input` - * // `onchange` `change` - * // `onselect` `select` (capital insensitive) - * onchange: function () { console.log(this.value); } - * }, { - * // A select (HTML ). - * type: 'select', // Or `selection` - * id: 'some_id', // Optional. Can be used in `getState` and `setState`. - * stateGroup: 'some_state_group', // Optional. Can be used in `getState` and `setState`. - * // Either `values` or `options` can be used. - * // Items in `values` or `options[i].value` can be any type, like `true`, `123`, etc. - * values: ['a', 'b', 'c'], - * options: [ - * {text: 'a', value: 123}, - * {value: {some: {some: 456}}}, // `text` can be omitted and auto generated by `value`. - * {text: 'c', input: ...}, // `input` can be used as shown below. - * ... - * ], - * // `options[i]` can nest other input type, currently only support `type: range`: - * options: [ - * {value: undefined}, - * {text: 'c', input: { - * type: 'range', - * // ... Other properties of `range` input except `onchange` and `text`. - * // When this option is not selected, the range input will be disabled. - * }} - * ], - * valueIndex: 0, // Optional. The initial value index. By default, the first option. - * value: 'cval', // Optional. The initial value. By default, the first option. - * // Can be any type, like `true`, `123`, etc. - * // But can only be JS primitive type, as `===` is used internally. - * text: 'xxx', // Optional. - * // They are the same: `oninput` `input` - * // `onchange` `change` - * // `onselect` `select` (capital insensitive) - * onchange: function () { console.log(this.value); } - * }, { - * // A line break. - * // They are the same: `br` `lineBreak` `break` `wrap` `newLine` `endOfLine` `carriageReturn` - * // `lineFeed` `lineSeparator` `nextLine` (capital insensitive) - * type: 'br', - * }, - * // ... + * Inputs can be these types: + * [ + * { + * // A button (default). + * text: 'xxx', + * // They are the same: `onclick`, `click` (capital insensitive) + * onclick: fn, + * disabled: false, // Optional. + * prevent: { // Optional. + * recordInputs: false, // Optional. + * inputsState: false, // Optional. + * }, + * }, + * { + * // A range slider (HTML ). + * type: 'range', // They are the same: 'range' 'slider' + * id: 'some_id', // Optional. Can be used in `switchGroup`. + * text: 'xxx', // Optional + * min: 0, // Optional + * max: 100, // Optional + * value: 30, // Optional. Must be a number. + * step: 1, // Optional + * suffix: '%', // Optional. e.g., '%' means the number is displayed as '33%' + * disabled: false, // Optional. + * prevent: { // Optional. + * recordInputs: false, // Optional. + * inputsState: false, // Optional. + * }, + * // They are the same: `oninput` `input` + * // `onchange` `change` `onchanged` `changed` + * // `onselect` `select` (capital insensitive) + * onchange: function () { console.log(this.value); } + * }, + * { + * // A select (HTML ). + * type: 'select', // They are the same: 'select' 'selection' + * id: 'some_id', // Optional. Can be used in `getState` and `setState`. + * // Either `values` or `options` can be used. + * // Items in `values` or `options[i].value` can be any type, like `true`, `123`, etc. + * values: ['a', 'b', 'c'], + * options: [ + * {text: 'a', value: 123}, + * {value: {some: {some: 456}}}, // `text` can be omitted and auto generated by `value`. + * {text: 'c', input: ...}, // `input` can be used as shown below. + * ... + * ], + * // `options[i]` can nest other input type, currently only support `type: range`: + * options: [ + * {value: undefined}, + * {text: 'c', input: { + * type: 'range', + * // ... Other properties of `range` input except `onchange` and `text`. + * // When this option is not selected, the range input will be disabled. + * }}, + * // If more than one options have internal `input`, `id` (option id) must be specified. + * // It can be visited by `onchange() { if (this.optionId) {...} }`. + * {text: 'd', id: 'some_option_id', input: {...}} + * ], + * optionIndex: 0, // Optional. Or `valueIndex`. The initial value index. + * // By default, the first option. + * value: 'cval', // Optional. The initial value. By default, the first option. + * // Can be any type, like `true`, `123`, etc. + * // But can only be JS primitive type, as `===` is used internally. + * text: 'xxx', // Optional. + * disabled: false, // Optional. + * prevent: { // Optional. + * recordInputs: false, // Optional. + * inputsState: false, // Optional. + * }, + * // They are the same: `oninput` `input` + * // `onchange` `change` `onchanged` `changed` + * // `onselect` `select` (capital insensitive) + * onchange: function () { console.log(this.value, this.optionId); } + * }, + * { + * // Group inputs. Only one group can be displayed at a time with in a group set. + * type: 'groups', // They are the same: 'groups' 'group' 'groupset' + * // `inputsHeight` is mandatory in group set to avoid height change to affects visual testing + * // when switching groups. It will be applied to all groups. + * inputsHeight, + * // `inputsHeight` will be applied to all groups. + * inputsStyle, + * disabled: false, // Optional. Controlls all groups inside, + * // unless `group.disabled` or `input.disbles` specified. + * prevent: { // Optional. + * recordInputs: false, // Optional. + * inputsState: false, // Optional. + * }, + * groups: [{ + * id: 'group_A', + * text: 'xxx', // Optional. Or `title`. Displayed in the header line of the group content. + * disabled: false, // Optional. Controlls all inputs inside, unless `input.disabled` specified. + * inputs: [{...}, {...}, ...], + * }, { + * id: 'group_B', + * inputs: [{...}, {...}, ...], + * }, ...] + * // Group switching API: @see chart.__testHelper.switchGroup(groupId); + * }, + * { + * // A line break. + * // They are the same: `br` `lineBreak` `break` `wrap` `newLine` `endOfLine` `carriageReturn` + * // `lineFeed` `lineSeparator` `nextLine` (capital insensitive) + * type: 'br', + * }, + * { + * // A separate line. + * type: 'hr', + * text: 'xxx', // Optional. Display text on the split line. + * }, + * // ... * ] - * The value of the inputs can be update by: - * chart.__testHelper.setState({'some_id_1': 'value1', 'some_id_2': 'value2'}); - * // Only set state from input that has 'some_state_group_1'. - * chart.__testHelper.setState({...}, 'some_state_group_1'); - * // Get: {some_id_1: 'value1', some_id_2: 'value2'} - * chart.__testHelper.getState(); - * // Only get state from input that has 'some_state_group_1'. - * chart.__testHelper.getState('some_state_group_1'); - * @param {boolean} [opt.recordCanvas] 'test/lib/canteen.js' is required. - * @param {boolean} [opt.recordVideo] - * @param {string} [opt.renderer] 'canvas' or 'svg' + * ----------------------------- Inputs related API ----------------------------------- + * @function chart.__testHelper.switchGroup Switch group. + * chart.__testHelper.switchGroup( + * groupId: string, + * opt?: { + * recordInputs: boolean, // Optional. @see `chart.__testHelper.recordInputs`. + * } + * ); + * + * @function chart.__testHelper.disableInputs Disable the specified inputs. + * chart.__testHelper.disableInputs(opt: { + * disabled: boolean, // disables/enables + * inputId: string, // Optional. id or id array. disables/enables the id-specified inputs. + * groupId: string, // Optional. id or id array. disables/enables the inputs within the group. + * recordInputs: boolean, // Optional. @see `chart.__testHelper.recordInputs`. + * }) + * + * @function chart.__testHelper.recordInputs + * @see `prevent` in `inputs` to prevent record. + * chart.__testHelper.recordInputs(opt: { // start record inputs operations for replay. + * action: 'start' + * }) + * chart.__testHelper.recordInputs(opt: { // stop record inputs operations and output. + * action: 'stop', + * outputType?: 'clipboard' | 'console', // Optional. 'clipboard' by default. + * printObjectOpt?: {} // Optional. the opt of `testHelper.printObject`. + * }) + * Note: if some API `chart.__testHelper.xxx` has parameter `recordInputs`, it indicates that wether + * record this call. It is `false` by default, and: + * - When this API is called in a callback function of an input where no `prevent.recordInputs` is + * declared, this option should be kept `false`. (This is the most cases.) + * - Otherwise it should be `true`. + * @function chart.__testHelper.replayInputs + * chart.__testHelper.replayInputs(inputsRecord) + * + * (TL;DR) NOTE: Currently, echarts can not be restored to the initial state by + * `setOption({..., xxx: undefined})` or `setOption({..., xxx: 'auto'})` in most options. + * That is, the initial state can only be obtained by: + * - either "not specified in echarts option" from the beginning; + * - or "sepecify the exact default value to match the internal default value in echarts option". + * + * @function chart.__testHelper.getInputsState Get the current state of `inputs`. + * chart.__testHelper.getInputsState() + * e.g., result: {some_id_1: 'value1', some_id_2: 'value2'} + * @see `prevent` in `inputs` to prevent. + * @function chart.__testHelper.setInputsState Set the current state of `inputs`. + * chart.__testHelper.setInputsState(state) + * @function chart.__testHelper.restoreInputsToInitialState + * chart.__testHelper.restoreInputsToInitialState() + * @see opt.saveInputsInitialState which must be specified as true for this API. + * ------------------------------------------------------------------------------------ + * + * @param {BoundingRectOpt} [opt.boundingRect] Optional. + * @typedef {boolean | {color?: string, slient: boolean}} BoundingRectOpt + * Enable display bounding rect for zrender elements. + * - `true`: Simply display the bounding rects. + * - `opt.boundingRect.color`: a string to indicate the color, like 'red', 'rgba(0,0,0,0.2)', '#fff'. + * - `opt.boundingRect.silent`: by default `false`; + * if `false`, click on the bounding rect, window.$0 will be assigned the original zrender element. + * - Can be switched dynamically by: + * // Update BoundingRectOpt, typically used to show/hide bounding rects. + * @function chart.__testHelper.boundingRect + * chart.__testHelper.boundingRect(opt: BoundingRectOpt); + * chart.__testHelper.boundingRect(); // Use the last BoundingRectOpt. + * + * @param {boolean} [opt.recordCanvas] Optional. 'test/lib/canteen.js' is required. + * @param {boolean} [opt.recordVideo] Optional. + * + * @param {Object} [opt.info] Optional. info object to display. + * @api info can be updated by `chart.__testHelper.updateInfo(someInfoObj, 'some_info_key');` + * @param {string} [opt.infoKey='option'] Optional. + * @param {Object|Array} [opt.dataTable] Optional. + * @param {Array.} [opt.dataTables] Optional. Multiple dataTables. + * @param {number} [opt.dataTableLimit=DEFAULT_DATA_TABLE_LIMIT] Optional. */ testHelper.create = function (echarts, domOrId, opt) { var dom = getDom(domOrId); @@ -164,43 +283,31 @@ var errMsgPrefix = '[testHelper dom: ' + domOrId + ']'; - var title = document.createElement('div'); + var titleContainer = document.createElement('div'); var left = document.createElement('div'); + var chartContainerWrapper = document.createElement('div'); var chartContainer = document.createElement('div'); var inputsContainer = document.createElement('div'); var dataTableContainer = document.createElement('div'); var infoContainer = document.createElement('div'); var recordCanvasContainer = document.createElement('div'); var recordVideoContainer = document.createElement('div'); + var boundingRectsContainer = document.createElement('div'); - title.setAttribute('title', dom.getAttribute('id')); + titleContainer.setAttribute('title', dom.getAttribute('id')); - var inputsHeight = testHelper.retrieveValue(opt.inputsHeight, opt.buttonsHeight, null); - if (inputsHeight != null) { - inputsHeight = parseFloat(inputsHeight); - } - - title.className = 'test-title'; + titleContainer.className = 'test-title'; dom.className = 'test-chart-block'; left.className = 'test-chart-block-left'; + chartContainerWrapper.className = 'test-chart-wrapper'; chartContainer.className = 'test-chart'; dataTableContainer.className = 'test-data-table'; infoContainer.className = 'test-info'; + boundingRectsContainer.className = 'test-bounding-rects'; + boundingRectsContainer.style.display = 'none'; recordCanvasContainer.className = 'record-canvas'; recordVideoContainer.className = 'record-video'; - inputsContainer.className = [ - 'test-inputs', - 'test-buttons', // deprecated but backward compat. - 'test-inputs-style-' + (opt.inputsStyle || opt.buttonsStyle || 'normal'), - (inputsHeight != null ? 'test-inputs-fix-height' : '') - ].join(' '); - if (inputsHeight != null) { - inputsContainer.style.cssText = [ - 'height:' + inputsHeight + 'px', - ].join(';') + ';'; - } - if (opt.info) { dom.className += ' test-chart-block-has-right'; infoContainer.className += ' test-chart-block-right'; @@ -210,43 +317,49 @@ left.appendChild(recordVideoContainer); left.appendChild(inputsContainer); left.appendChild(dataTableContainer); - left.appendChild(chartContainer); + left.appendChild(chartContainerWrapper); + chartContainerWrapper.appendChild(chartContainer); + chartContainerWrapper.appendChild(boundingRectsContainer); dom.appendChild(infoContainer); dom.appendChild(left); - dom.parentNode.insertBefore(title, dom); + dom.parentNode.insertBefore(titleContainer, dom); - var chart; + initTestTitle(opt, titleContainer); + + var chart = testHelper.createChart(echarts, chartContainer, opt.option, opt, opt.setOptionOpts, errMsgPrefix); + chart.__testHelper = {}; + + initDataTables(opt, dataTableContainer); + + if (chart) { + initInputs(chart, opt, inputsContainer, errMsgPrefix); + initUpdateInfo(opt, chart, infoContainer); + initRecordCanvas(opt, chart, recordCanvasContainer); + if (opt.recordVideo) { + testHelper.createRecordVideo(chart, recordVideoContainer); + } + initShowBoundingRects(chart, echarts, opt, boundingRectsContainer); + } + return chart; + }; + + function initTestTitle(opt, titleContainer) { var optTitle = opt.title; if (optTitle) { if (optTitle instanceof Array) { optTitle = optTitle.join('\n'); } - title.innerHTML = '
' - + testHelper.encodeHTML(optTitle) + titleContainer.innerHTML = '
' + + encodeHTML(optTitle) .replace(/\*\*([^*]+?)\*\*/g, '$1') .replace(/\n/g, '
') + '
'; } + } - chart = testHelper.createChart(echarts, chartContainer, opt.option, opt, opt.setOptionOpts, errMsgPrefix); - - var dataTables = opt.dataTables; - if (!dataTables && opt.dataTable) { - dataTables = [opt.dataTable]; - } - if (dataTables) { - var tableHTML = []; - for (var i = 0; i < dataTables.length; i++) { - tableHTML.push(createDataTableHTML(dataTables[i], opt)); - } - dataTableContainer.innerHTML = tableHTML.join(''); - } - - var inputsResult; - if (chart) { - inputsResult = initInputs(chart, opt, inputsContainer, errMsgPrefix); - } + function initUpdateInfo(opt, chart, infoContainer) { + assert(chart.__testHelper); if (opt.info) { updateInfo(opt.info, opt.infoKey); @@ -256,27 +369,17 @@ infoContainer.innerHTML = createObjectHTML(info, infoKey || 'option'); } - initRecordCanvas(opt, chart, recordCanvasContainer); - - if (opt.recordVideo) { - testHelper.createRecordVideo(chart, recordVideoContainer); - } - - chart.__testHelper = { - updateInfo: updateInfo, - setState: inputsResult && inputsResult.setState, - getState: inputsResult && inputsResult.getState - }; - - return chart; - }; + chart.__testHelper.updateInfo = updateInfo; + } function initInputs(chart, opt, inputsContainer, errMsgPrefix) { + assert(chart.__testHelper); + var NAMES_ON_INPUT_CHANGE = makeFlexibleNames([ - 'input', 'on-input', 'on-change', 'select', 'on-select' + 'input', 'on-input', 'change', 'on-change', 'changed', 'on-changed', 'select', 'on-select' ]); var NAMES_ON_CLICK = makeFlexibleNames([ - 'on-click', 'click' + 'click', 'on-click' ]); var NAMES_TYPE_BUTTON = makeFlexibleNames(['button', 'btn']); var NAMES_TYPE_RANGE = makeFlexibleNames(['range', 'slider']); @@ -285,70 +388,450 @@ 'br', 'line-break', 'break', 'wrap', 'new-line', 'end-of-line', 'carriage-return', 'line-feed', 'line-separator', 'next-line' ]); - // key: id, value: {setState, getState} + var NAMES_TYPE_HR = makeFlexibleNames([ + 'hr', 'horizontal-line', 'divider', 'separate-line' + ]); + var NAMES_TYPE_GROUP_SET = makeFlexibleNames(['group', 'groups', 'group-set']); + /** + * key: inputId, + * value: { + * id: inputId, + * disable?, + * switchGroup?, + * setState?, + * getState?, + * } + */ var _inputsDict = {}; + var NAMES_RECORD_INPUTS_ACTION_START = makeFlexibleNames(['start', 'begin']); + var NAMES_RECORD_INPUTS_ACTION_STOP = makeFlexibleNames(['stop', 'end', 'finish']); + var _inputsRecord = null; + /** + * key: inputId + * value: @see makeInputRecorder + */ + var _inputRecorderWrapperMap = {}; + var _INPUTS_RECORD_VERSION = '1.0.0'; + var NANES_PREVENT_INPUTS_STATE = makeFlexibleNames([ + 'inputs-state', 'input-state', 'inputs-states', 'input-states' + ]); + var NANES_PREVENT_RECORD_INPUTS = makeFlexibleNames([ + 'record-inputs', 'record-input', + 'input-record', 'inputs-record', + ]); + var _initStateBackup = null; + + initInputsContainer(inputsContainer, opt); + var inputsDefineList = retrieveInputDefineList(opt); + dealInitEachInput(inputsDefineList, inputsContainer); + + // --- Input operation related API --- + chart.__testHelper.switchGroup + = makeSwitchGroup(); + chart.__testHelper.disableInputs + = chart.__testHelper.disableInput + = makeDisableInputs(); + + // --- Input meta related API --- + chart.__testHelper.recordInputs + = recordInputs; + chart.__testHelper.replayInputs + = chart.__testHelper.replayInput + = replayInputs; + chart.__testHelper.getInputsState + = chart.__testHelper.getInputState + = getInputsState; + chart.__testHelper.setInputsState + = chart.__testHelper.setInputState + = setInputsState; + chart.__testHelper.restoreInputsToInitialState + = restoreInputsToInitialState; + + if (opt.saveInputsInitialState) { + _initStateBackup = chart.__testHelper.getInputsState(); + } + + return; + + function makeDisableInputs() { + var inputRecorderWrapper = makeInputRecorder(); + inputRecorderWrapper.setupInputId('__\0testHelper_disableInputs'); + var disableInputsWithRecordInputs = inputRecorderWrapper.inputRecorder.wrapUserInputListener({ + listener: disableInputs, + op: 'disableInputs' + }); - init(); - - return {setState: setState, getState: getState}; - + /** + * @param {string|Array.?} opt.groupId + * @param {string|Array.?} opt.inputId + * @param {boolean} opt.recordInputs + */ + return function (opt) { + opt.recordInputs + ? disableInputsWithRecordInputs(opt) + : disableInputs(opt); + } + + function disableInputs(opt) { + assert(opt, '[disableInputs] requires parameters.'); + var groupId = opt.groupId; + var inputId = opt.inputId; + assert( + groupId != null || inputId != null, + '[disableInputs] requires `groupId` or/and `inputId`.' + ); + var inputIdList = []; + if (inputId != null) { + if (getType(inputId) !== 'array') { + inputId = [inputId]; + } + for (var idx = 0; idx < inputId.length; idx++) { + var id = inputId[idx]; + findInputCreatedAndCheck(id, {throw: true}); + inputIdList.push(id); + } + } + if (groupId != null) { + if (getType(groupId) !== 'array') { + groupId = [groupId]; + } + for (var idx = 0; idx < groupId.length; idx++) { + inputIdList = inputIdList.concat(retrieveAndVerifyGroup(groupId[idx]).idList); + } + } + var disabled = opt.disabled; + for (var idx = 0; idx < inputIdList.length; idx++) { + var id = inputIdList[idx]; + if (_inputsDict[id].disable) { + _inputsDict[id].disable({disabled: disabled}); + } + } + } + } - function init() { - var inputsDefineList = retrieveInputDefineList(); + /** + * @param {string} opt.action 'start' or 'stop'. + * @param {string} opt.outputType Optional. 'clipboard' or 'console'. + * @param {Object} opt.printObjectOpt Optional. The opt of `testHelper.printObject`. + */ + function recordInputs(opt) { + var action = opt.action; + assert( + NAMES_RECORD_INPUTS_ACTION_START.indexOf(action) >= 0 + || NAMES_RECORD_INPUTS_ACTION_STOP.indexOf(action) >= 0, + 'Invalide recordInputs action: ' + action + '. Should be ' + + NAMES_RECORD_INPUTS_ACTION_START + ' ' + NAMES_RECORD_INPUTS_ACTION_STOP + ); + if (NAMES_RECORD_INPUTS_ACTION_START.indexOf(action) >= 0) { + _inputsRecord = { + version: _INPUTS_RECORD_VERSION, + startTime: +(new Date()), + operations: [], + }; + } + else if (NAMES_RECORD_INPUTS_ACTION_STOP.indexOf(action) >= 0) { + if (_inputsRecord == null) { + console.error( + 'Inputs record is not started. Please call' + + ' `chart.__testHelper.recordInputs({action: "start"})` first.' + ); + return; + } + _inputsRecord.endTime = +(new Date()); + var inputsRecord = _inputsRecord; + _inputsRecord = null; + outputInputsRecord(inputsRecord); + return inputsRecord; + } - for (var i = 0; i < inputsDefineList.length; i++) { - var singleCreated = createInputByDefine(inputsDefineList[i]); - if (!singleCreated) { - continue; + function outputInputsRecord(record) { + if (opt.outputType === 'console') { + console.log(testHelper.printObject(record, opt.printObjectOpt)); } - for (var j = 0; j < singleCreated.elList.length; j++) { - inputsContainer.appendChild(singleCreated.elList[j]); + else { + testHelper.clipboard(record, opt.printObjectOpt); } - var id = retrieveId(inputsDefineList[i], 'id'); - var stateGroup = retrieveId(inputsDefineList[i], 'stateGroup'); - if (stateGroup != null) { - if (id == null) { - id = generateId('test_inputs_'); - } - singleCreated.stateGroup = stateGroup; + } + } + + function replayInputs(inputsRecord) { + assert( + inputsRecord.version === _INPUTS_RECORD_VERSION, + 'Not supported inputs record version. expect' + _INPUTS_RECORD_VERSION + ' Need to re-record.' + ); + for (var idx = 0; idx < inputsRecord.operations.length; idx++) { + var opItem = inputsRecord.operations[idx]; + findInputCreatedAndCheck(opItem.id, {throw: true}); + assert( + !shouldPrevent(opItem.id, NANES_PREVENT_RECORD_INPUTS), + 'Input (id:' + opItem.id + ') has prevented recording. This may caused by test case change.' + ); + var inputRecorderWrapper = _inputRecorderWrapperMap[opItem.id]; + assert(inputRecorderWrapper); + assert(getType(opItem.op) === 'string', 'Invalid op: ' + opItem.op); + var listenerDefine = inputRecorderWrapper.listenerDefineMap[opItem.op]; + assert( + listenerDefine, + 'Can not find listener by op: ' + opItem.op + ' This may caused by test case change.' + ); + var prepared = {this: [], arguments: {}}; + if (listenerDefine.prepareReplay) { + prepared = listenerDefine.prepareReplay(opItem.args); + assert( + isObject(prepared) + && prepared.hasOwnProperty('this') + && getType(prepared.arguments) === 'array', + '`prepareReplay` must return an object: {this: any, arguments: []}.' + ); } - if (id != null) { - if (_inputsDict[singleCreated.id]) { - throw new Error(errMsgPrefix + 'Duplicate input id: ' + singleCreated.id); + listenerDefine.listener.apply(prepared.this, prepared.arguments); + } + } + + function makeInputRecorder() { + var _inputId = null; + var inputRecorderWrapper = { + setupInputId: function (inputId) { + _inputId = inputId; + _inputRecorderWrapperMap[inputId] = inputRecorderWrapper; + }, + inputRecorder: { + wrapUserInputListener: wrapUserInputListener + }, + /** + * key: op, + */ + listenerDefineMap: {}, + }; + + return inputRecorderWrapper; + + function wrapUserInputListener(listenerDefine) { + assert( + getType(listenerDefine.listener) === 'function', + 'Must provide a function `listener`.' + ); + assert( + getType(listenerDefine.op) === 'string', + 'Must provide an `op` string to identify this listener.' + ); + + assert( + !inputRecorderWrapper.listenerDefineMap[listenerDefine.op], + '`op` ' + listenerDefine.op + ' overlapped.' + ); + inputRecorderWrapper.listenerDefineMap[listenerDefine.op] = listenerDefine; + + return function wrappedListener() { + assert(_inputId != null); + if (_inputsRecord && !shouldPrevent(_inputId, NANES_PREVENT_RECORD_INPUTS)) { + var recordWrapper = {id: _inputId, op: listenerDefine.op}; + if (listenerDefine.createRecordArgs) { + recordWrapper.args = listenerDefine.createRecordArgs.apply(this, arguments); + } + _inputsRecord.operations.push(recordWrapper); } - singleCreated.id = id; - _inputsDict[singleCreated.id] = singleCreated; - } + return listenerDefine.listener.apply(this, arguments); + }; } } - function setState(state, stateGroup) { + function setInputsState(state) { + var changedCreatedList = []; for (var id in state) { if (state.hasOwnProperty(id)) { - if (_inputsDict[id] == null) { - throw new Error(errMsgPrefix + 'No input with id: ' + id); + var inputCreated = findInputCreatedAndCheck(id, {log: true}); + if (!inputCreated) { + continue; } - if (!stateGroup || _inputsDict[id].stateGroup === stateGroup) { - _inputsDict[id].setState(state[id]); + if (shouldPrevent(id, NANES_PREVENT_INPUTS_STATE) || !inputCreated.setState) { + continue; } + inputCreated.setState(state[id]); + changedCreatedList.push(inputCreated); } } } - function getState(stateGroup) { + function getInputsState() { var result = {}; for (var id in _inputsDict) { - if (_inputsDict.hasOwnProperty(id) - && (!stateGroup || _inputsDict[id].stateGroup === stateGroup) - ) { - result[id] = _inputsDict[id].getState(); + if (_inputsDict.hasOwnProperty(id)) { + var inputCreated = _inputsDict[id]; + if (shouldPrevent(id, NANES_PREVENT_INPUTS_STATE) || !inputCreated.getState) { + continue; + } + if (inputCreated.idCanNotPersist) { + throw new Error( + errMsgPrefix + '[getInputsState]. Please specify an id explicitly or unique text' + + ' for input:' + printObject(inputCreated.__inputDefine) + ); + } + result[id] = inputCreated.getState(); } } return result; } - function retrieveInputDefineList() { - var defineList = testHelper.retrieveValue(opt.buttons, opt.button, opt.input, opt.inputs); + function restoreInputsToInitialState() { + assert( + _initStateBackup != null, + 'opt.saveInputsInitialState must be true to use `restoreInputsToInitialState`.' + ); + setInputsState(_initStateBackup); + } + + function initInputsContainer(container, define, features) { + assert(container.tagName.toLowerCase() === 'div'); + container.innerHTML = ''; + + var ignoreFixHeight = features && features.ignoreFixHeight; + var ignoreInputsStyle = features && features.ignoreInputsStyle; + + var inputsHeight = retrieveValue(define.inputsHeight, define.buttonsHeight, null); + if (inputsHeight != null) { + inputsHeight = parseFloat(inputsHeight); + } + + var classNameArr = []; + if (features && features.className) { + classNameArr.push(features.className); + } + if (!ignoreInputsStyle) { + classNameArr.push( + 'test-inputs', + 'test-buttons', // deprecated but backward compat. + 'test-inputs-style-' + (define.inputsStyle || define.buttonsStyle || 'normal') + ); + } + if (!ignoreFixHeight && inputsHeight != null) { + classNameArr.push('test-inputs-fix-height'); + container.style.cssText += [ + 'height:' + inputsHeight + 'px' + ].join(';') + ';'; + } + + container.className = classNameArr.join(' '); + } + + function dealInitEachInput(inputsDefineList, inputsContainer) { + var idList = []; + for (var i = 0; i < inputsDefineList.length; i++) { + var inputDefine = inputsDefineList[i]; + var inputRecorderWrapper = makeInputRecorder(); + var inputCreated = createInputByDefine( + inputDefine, + inputRecorderWrapper.inputRecorder + ); + if (!inputCreated) { + continue; + } + for (var j = 0; j < inputCreated.elList.length; j++) { + inputsContainer.appendChild(inputCreated.elList[j]); + } + var id = storeToInputDict(inputDefine, inputCreated, inputRecorderWrapper.setupInputId); + idList.push(id); + } + return idList; + } + + function storeToInputDict(inputDefine, inputCreated, inputRecorderSetupInputId) { + var id = retrieveId(inputDefine, 'id'); + if (id != null) { + id = '' + id; + if (_inputsDict[id]) { + throw new Error(errMsgPrefix + ' Duplicate input id: ' + id); + } + } + if (id == null) { + var text = retrieveValue(inputDefine.text, '') + ''; + if (text) { + var textBasedId = '__inputs|' + text + '|'; + if (!_inputsDict[textBasedId]) { + id = textBasedId; + } + } + } + if (id == null) { + id = generateNonPersistentId('__inputs_non_persist'); + assert(!_inputsDict[id]); + inputCreated.idCanNotPersist = true; + } + inputCreated.id = id; + inputCreated.__inputDefine = inputDefine; + _inputsDict[id] = inputCreated; + if (inputRecorderSetupInputId) { + inputRecorderSetupInputId(id); + } + return id; + } + + function retrieveAndVerifyGroup(groupId) { + var groupCreated = _inputsDict[groupId]; + assert(groupCreated, 'Can not find group by id: ' + groupId); + assert(groupCreated.groupParent, 'This is not a group. id: ' + groupId); + return groupCreated; + } + + function makeSwitchGroup() { + var inputRecorderWrapper = makeInputRecorder(); + inputRecorderWrapper.setupInputId('__\0testHelper_switchGroup'); + var switchGroupWithRecordInputs = inputRecorderWrapper.inputRecorder.wrapUserInputListener({ + listener: dealSwitchGroup, + op: 'switchGroup' + }); + + return function (groupId, opt) { + (opt && opt.recordInputs) + ? switchGroupWithRecordInputs(groupId, opt) + : dealSwitchGroup(groupId); + }; + + function dealSwitchGroup(groupId) { + var groupCreatedToShow = retrieveAndVerifyGroup(groupId); + var groupSetCreated = groupCreatedToShow.groupParent; + groupSetCreated.switchGroup(groupId); + } + } + + function showHideGroupInGroupSet(groupCreated, showOrHide) { + groupCreated.inputsContainerEl.style.display = showOrHide + ? 'block' : 'none'; + var groupDefine = groupCreated.groupDefine; + groupCreated.groupSetTextEl.innerHTML = showOrHide + ? encodeHTML(retrieveValue(groupDefine.text, groupDefine.title, '')) + : ''; + } + + function shouldPrevent(inputId, names) { + var prevent = _inputsDict[inputId].__inputDefine.prevent || {}; + for (var idx = 0; idx < names.length; idx++) { + if (prevent[names[idx]]) { + return true; + } + } + return false; + } + + function findInputCreatedAndCheck(inputId, errorHandling) { + var inputCreated = _inputsDict[inputId]; + if (!inputCreated) { + var errMsg = errMsgPrefix + ' No input found by id: ' + inputId + '. May caused by test case change.'; + if (errorHandling.log) { + console.error(errMsg); + } + else if (errorHandling.throw) { + throw new Error(errMsg); + } + else { + throw new Error('internal failure.') + } + } + return inputCreated; + } + + function retrieveInputDefineList(define) { + var defineList = retrieveValue(define.buttons, define.button, define.input, define.inputs); if (typeof defineList === 'function') { defineList = defineList(chart); } @@ -358,12 +841,10 @@ return defineList; } - function getBtnTextHTML(inputDefine, defaultText) { - return testHelper.encodeHTML(testHelper.retrieveValue(inputDefine.name, inputDefine.text, defaultText)); - } - function getBtnDefineAttr(inputDefine, attr, defaultValue) { - return inputDefine[attr] != null ? inputDefine[attr] : defaultValue; + function getInputsTextHTML(inputDefine, defaultText) { + return encodeHTML(retrieveValue(inputDefine.name, inputDefine.text, defaultText)); } + function getBtnEventListener(inputDefine, names) { for (var idx = 0; idx < names.length; idx++) { if (inputDefine[names[idx]]) { @@ -376,186 +857,316 @@ if (inputDefine && inputDefine[idPropName] != null) { var type = getType(inputDefine[idPropName]); if (type !== 'string' && type != 'number') { - throw new Error(errMsgPrefix + 'id must be string or number.'); + throw new Error(errMsgPrefix + ' id must be string or number.'); } return inputDefine[idPropName] + ''; } } - function createInputByDefine(inputDefine) { + function createInputByDefine(inputDefine, inputRecorder) { if (!inputDefine) { return; } var inputType = inputDefine.hasOwnProperty('type') ? inputDefine.type : 'button'; if (arrayIndexOf(NAMES_TYPE_RANGE, inputType) >= 0) { - var rangeInputCreated = createRangeInput(inputDefine); - return { - elList: [rangeInputCreated.el], - getState: rangeInputCreated.getState, - setState: rangeInputCreated.setState - }; + return createRangeInput(inputDefine, null, inputRecorder); } else if (arrayIndexOf(NAMES_TYPE_SELECT, inputType) >= 0) { - return createSelectInput(inputDefine); + return createSelectInput(inputDefine, inputRecorder); } else if (arrayIndexOf(NAMES_TYPE_BR, inputType) >= 0) { - return { - elList: [createBr(inputDefine)] - }; + return createBr(inputDefine, inputRecorder); + } + else if (arrayIndexOf(NAMES_TYPE_HR, inputType) >= 0) { + return createHr(inputDefine, inputRecorder); } else if (arrayIndexOf(NAMES_TYPE_BUTTON, inputType) >= 0) { - return { - elList: [createButtonInput(inputDefine)] - }; + return createButtonInput(inputDefine, inputRecorder); } - else { - throw new Error(errMsgPrefix + 'Unsupported button type: ' + inputType); - } - } - - function createRangeInput(inputDefine, internallyForceDef) { - var sliderWrapperEl = document.createElement('span'); - resetWrapperCSS(false); - - var sliderTextEl = document.createElement('span'); - sliderTextEl.className = 'test-inputs-slider-text'; - sliderTextEl.innerHTML = internallyForceDef - ? getBtnTextHTML(internallyForceDef, '') - : getBtnTextHTML(inputDefine, ''); - sliderWrapperEl.appendChild(sliderTextEl); - - var sliderInputEl = document.createElement('input'); - sliderInputEl.className = 'test-inputs-slider-input'; - sliderInputEl.setAttribute('type', 'range'); - var sliderListener = internallyForceDef - ? getBtnEventListener(internallyForceDef, NAMES_ON_INPUT_CHANGE) - : getBtnEventListener(inputDefine, NAMES_ON_INPUT_CHANGE); - if (!sliderListener) { - throw new Error(errMsgPrefix + 'No listener (either ' + NAMES_ON_INPUT_CHANGE.join(', ') + ') specified for slider.'); - } - sliderInputEl.addEventListener('input', function () { - updateSliderValueEl(); - var target = {value: this.value}; - sliderListener.call(target, {target: target}); - }); - sliderInputEl.setAttribute('min', getBtnDefineAttr(inputDefine, 'min', 0)); - sliderInputEl.setAttribute('max', getBtnDefineAttr(inputDefine, 'max', 100)); - sliderInputEl.setAttribute('value', getBtnDefineAttr(inputDefine, 'value', 30)); - sliderInputEl.setAttribute('step', getBtnDefineAttr(inputDefine, 'step', 1)); - sliderWrapperEl.appendChild(sliderInputEl); - - var sliderValueEl = document.createElement('span'); - sliderValueEl.className = 'test-inputs-slider-value'; - function updateSliderValueEl() { - var val = sliderInputEl.value; - updateText(val); - } - function updateText(val) { - sliderValueEl.innerHTML = testHelper.encodeHTML(val); - } - updateSliderValueEl(); - sliderWrapperEl.appendChild(sliderValueEl); - - function resetWrapperCSS(disabled) { - sliderWrapperEl.className = 'test-inputs-slider' - + (internallyForceDef ? ' test-inputs-slider-sub' : '') - + (disabled ? ' test-inputs-slider-disabled' : ''); + else if (arrayIndexOf(NAMES_TYPE_GROUP_SET, inputType) >= 0) { + return createGroupSetInput(inputDefine, inputRecorder); } + else { + throw new Error(errMsgPrefix + ' Unsupported button type: ' + inputType); + } + } + + function createRangeInput(inputDefine, internallyForceDef, inputRecorder) { + var _currVal = +retrieveValue(inputDefine.value, 0); + var _disabled = false; + var _step = +retrieveValue(inputDefine.step, 1); + var _minVal = +retrieveValue(inputDefine.min, 0); + var _maxVal = +retrieveValue(inputDefine.max, 100); + var _precision = Math.max( + getPrecision(_minVal), + getPrecision(_maxVal), + getPrecision(_currVal), + getPrecision(_step) + ); + var _noDeltaButtons = !!inputDefine.noDeltaButtons; // Only for backward compat. + var _rangeInputWrapperEl; + var _rangeInputListener; + var _rangeInputEl; + var _rangeInputValueEl; + var _opSuffix = internallyForceDef && internallyForceDef.id || ''; + + dealInitRangeInput(); return { - el: sliderWrapperEl, - setState: function (state) { - if (state == null || !isFinite(+state.value)) { - throw new Error(errMsgPrefix + 'Invalid state: ' + printObject(state) + ' for range'); + elList: [_rangeInputWrapperEl], + disable: resetRangeInputDisabled, + getState: getRangeInputState, + setState: setRangeInputState, + }; + + function dealInitRangeInput() { + _rangeInputWrapperEl = document.createElement('span'); + resetRangeInputWrapperCSS(_rangeInputWrapperEl, false); + + _rangeInputListener = internallyForceDef + ? getBtnEventListener(internallyForceDef, NAMES_ON_INPUT_CHANGE) + : getBtnEventListener(inputDefine, NAMES_ON_INPUT_CHANGE); + if (!_rangeInputListener) { + throw new Error( + errMsgPrefix + ' No listener (either ' + + NAMES_ON_INPUT_CHANGE.join(', ') + ') specified for slider.' + ); + } + + var sliderTextEl = document.createElement('span'); + sliderTextEl.className = 'test-inputs-slider-text'; + sliderTextEl.innerHTML = internallyForceDef + ? getInputsTextHTML(internallyForceDef, '') + : getInputsTextHTML(inputDefine, ''); + _rangeInputWrapperEl.appendChild(sliderTextEl); + + function createRangeInputDeltaBtn(btnName, delta) { + if (_noDeltaButtons) { return; } + var sliderLRBtnEl = document.createElement('div'); + sliderLRBtnEl.className = 'test-inputs-slider-btn-incdec test-inputs-slider-btn-' + btnName; + _rangeInputWrapperEl.appendChild(sliderLRBtnEl); + sliderLRBtnEl.addEventListener('click', inputRecorder.wrapUserInputListener({ + listener: function () { + if (_disabled) { return; } + // 0.1 + 0.2 = 0.30000000000000004 + _currVal = round(_currVal + delta, _precision); + updateRangeInputViewValue(_currVal); + dispatchRangeInputChangedEvent(); + }, + op: btnName + _opSuffix + })); + } + createRangeInputDeltaBtn('decrease', -_step); + createRangeInputDeltaBtn('increase', _step); + + _rangeInputEl = document.createElement('input'); + _rangeInputEl.className = 'test-inputs-slider-input'; + _rangeInputEl.setAttribute('type', 'range'); + _rangeInputEl.addEventListener('input', inputRecorder.wrapUserInputListener({ + listener: function () { + if (_disabled) { return; } + _currVal = +this.value; + updateRangeInputViewValue(_currVal); + dispatchRangeInputChangedEvent(); + }, + op: 'slide' + _opSuffix, + createRecordArgs: function () { + return [+this.value]; + }, + prepareReplay: function (recordArgs) { + _rangeInputEl.value = recordArgs[0]; + return { + this: _rangeInputEl, + arguments: [] + }; } - sliderInputEl.value = state.value; - updateText(state.value); - }, - getState: function () { - return {value: +sliderInputEl.value}; - }, - disable: function (disabled) { - sliderInputEl.disabled = disabled; - resetWrapperCSS(disabled); + })); + _rangeInputEl.setAttribute('min', _minVal); + _rangeInputEl.setAttribute('max', _maxVal); + _rangeInputEl.setAttribute('value', _currVal); + _rangeInputEl.setAttribute('step', _step); + _rangeInputWrapperEl.appendChild(_rangeInputEl); + + _rangeInputValueEl = document.createElement('span'); + _rangeInputValueEl.className = 'test-inputs-slider-value'; + _rangeInputWrapperEl.appendChild(_rangeInputValueEl); + + updateRangeInputViewValue(_currVal); + resetRangeInputDisabled(inputDefine); + } + + function updateRangeInputViewValue(newVal) { + _rangeInputEl.value = +newVal; + _rangeInputValueEl.innerHTML = encodeHTML(newVal + '' + (inputDefine.suffix || '')); + } + function resetRangeInputWrapperCSS(wrapperEl, disabled) { + wrapperEl.className = 'test-inputs-slider' + + (internallyForceDef ? ' test-inputs-slider-sub' : '') + + (disabled ? ' test-inputs-slider-disabled' : ''); + + (_noDeltaButtons ? ' test-inputs-slider-no-delta-buttons' : ''); + } + function setRangeInputState(state) { + if (!isObject(state)) { + console.error( + errMsgPrefix + ' Range input state must be object rather than ' + printObject(state) + + ' May caused by test case change.' + ); + return; } - }; - } + var newVal = +state.value; + if (!isFinite(newVal)) { + console.error( + errMsgPrefix + ' Range input state.value must be number rather than ' + printObject(state) + + ' May caused by test case change.' + ); + return; + } + _currVal = newVal; + resetRangeInputDisabled({disabled: state.disabled}); + updateRangeInputViewValue(_currVal); + } + function getRangeInputState() { + return { + value: _currVal, + disabled: _disabled, + }; + } + function resetRangeInputDisabled(opt) { + _disabled = !!opt.disabled; + _rangeInputEl.disabled = _disabled; + resetRangeInputWrapperCSS(_rangeInputWrapperEl, _disabled); + } + function dispatchRangeInputChangedEvent() { + if (_disabled) { return; } + var target = {value: _currVal}; + _rangeInputListener.call(target, {target: target}); + } + } // End of createRangeInput - function createSelectInput(inputDefine) { + function createSelectInput(inputDefine, inputRecorder) { var selectCtx = { _optionList: [], + _selectWrapperEl: null, _selectEl: null, _optionIdxToSubInput: [], - _elList: [] + _el: null, + _disabled: false, }; - createElementsForSelect(); + var _SAMPLE_SELECT_DEFINITION = [ + '{', + ' type: "select",', + ' text?: "my select:",', + ' options: [', + ' {text?: string, value: any},', + ' {text?: string, input: {type: "range", ...}},', + ' {text?: string, id: "some_option_id", input: {type: "range", ...}},', + ' ...,', + ' ],', + ' onchange() { ... },', + '}' + ].join('\n'); + + createSelectInputElements(); var _selectListener = getBtnEventListener(inputDefine, NAMES_ON_INPUT_CHANGE); - if (!_selectListener) { - throw new Error(errMsgPrefix + 'No listener (either ' + NAMES_ON_INPUT_CHANGE.join(', ') + ') specified for select.'); - } - - initOptionsForSelect(inputDefine); - - selectCtx._selectEl.addEventListener('change', function () { - var optionIdx = getOptionIndex(selectCtx._selectEl); - disableSubInputs(optionIdx); - handleSelectChange(getValueByOptionIndex(optionIdx)); - }); + assert( + _selectListener, + errMsgPrefix + ' No listener specified for select. Should have either one of ' + + NAMES_ON_INPUT_CHANGE.join(', ') + '.' + ); + + initSelectInputOptions(inputDefine); + + selectCtx._selectEl.addEventListener('change', inputRecorder.wrapUserInputListener({ + listener: function dispatchSelectInputChangedEvent() { + if (selectCtx._disabled) { return; } + resetSelectInputSubInputsDisabled(); + triggerUserSelectChangedEvent(); + }, + op: 'select', + createRecordArgs: function () { + return [getSelectInputOptionIndex()]; + }, + prepareReplay: function (recordArgs) { + var optionIndex = recordArgs[0]; + validateOptionIndex(optionIndex); + selectCtx._selectEl.value = optionIndex; + return { + this: selectCtx._selectEl, + arguments: [] + }; + } + })); - setInitValue(inputDefine); + setSelectInputInitValue(inputDefine); + resetSelectInputDisabled(inputDefine); return { - elList: selectCtx._elList, - getState: getStateForSelect, - setState: setStateForSelect + elList: [selectCtx._el], + disable: resetSelectInputDisabled, + getState: getSelectInputState, + setState: setSelectInputState, }; - function createElementsForSelect() { + function createSelectInputElements() { var selectWrapperEl = document.createElement('span'); - selectWrapperEl.className = 'test-inputs-select'; + selectCtx._selectWrapperEl = selectWrapperEl; + resetSelectInputWrapperCSS(selectWrapperEl, false); var textEl = document.createElement('span'); textEl.className = 'test-inputs-select-text'; - textEl.innerHTML = getBtnTextHTML(inputDefine, ''); + textEl.innerHTML = getInputsTextHTML(inputDefine, ''); selectWrapperEl.appendChild(textEl); var selectEl = document.createElement('select'); selectEl.className = 'test-inputs-select-select'; selectWrapperEl.appendChild(selectEl); - selectCtx._elList.push(selectWrapperEl); + selectCtx._el = selectWrapperEl; selectCtx._selectEl = selectEl; } - function initOptionsForSelect(inputDefine) { + function resetSelectInputWrapperCSS(selectWrapperEl, disabled) { + selectWrapperEl.className = 'test-inputs-select' + + (disabled ? ' test-inputs-select-disabled' : ''); + } + + function initSelectInputOptions(inputDefine) { // optionDef can be {text, value} or just value // (value can be null/undefined/array/object/... everything). // Convinient but might cause ambiguity when a value happens to be {text, value}, but rarely happen. if (inputDefine.options) { + var innerInputCount = 0; for (var optionIdx = 0; optionIdx < inputDefine.options.length; optionIdx++) { var optionDef = inputDefine.options[optionIdx]; - if ( - !isObject(optionDef) - || ( - !optionDef.hasOwnProperty('value') - && !isObject(optionDef.input) - ) - ) { - throw new Error( - 'Can only be {type: "select", options: {value: any, text?: string, input?: SubInput}[]}' - ); - } + assert(isObject(optionDef), [ + errMsgPrefix + ' Select option definition should be an object, such as,', + _SAMPLE_SELECT_DEFINITION + ].join('\n')); + assert(optionDef.hasOwnProperty('value') || isObject(optionDef.input), [ + errMsgPrefix + ' Select option definition should contain prop' + + ' either `value` or `option`, such as,', + _SAMPLE_SELECT_DEFINITION + ].join('\n')); var text = getType(optionDef.text) === 'string' ? optionDef.text - : makeTextByValue(optionDef); + : makeSelectInputTextByValue(optionDef); selectCtx._optionList.push({ value: optionDef.value, input: optionDef.input, + id: optionDef.id, text: text }); + if (optionDef.input) { + innerInputCount++; + } + assert(innerInputCount < 2 || optionDef.id != null, [ + errMsgPrefix + ' If more than one inner input in a select,' + + ' option id must be specified. ', + _SAMPLE_SELECT_DEFINITION + ].join('\n')); } } else if (inputDefine.values) { @@ -563,19 +1174,19 @@ var value = inputDefine.values[optionIdx]; selectCtx._optionList.push({ value: value, - text: makeTextByValue({value: value}) + text: makeSelectInputTextByValue({value: value}) }); } } if (!selectCtx._optionList.length) { - throw new Error(errMsgPrefix + 'No options specified for select.'); + throw new Error(errMsgPrefix + ' No options specified for select.'); } for (var optionIdx = 0; optionIdx < selectCtx._optionList.length; optionIdx++) { var optionDef = selectCtx._optionList[optionIdx]; selectCtx._optionList[optionIdx] = optionDef; var optionEl = document.createElement('option'); - optionEl.innerHTML = testHelper.encodeHTML(optionDef.text); + optionEl.innerHTML = encodeHTML(optionDef.text); // HTML select.value is always string. But it would be more convenient to // convert it to user's raw input value type. // (The input raw value can be null/undefined/array/object/... everything). @@ -584,106 +1195,161 @@ if (optionDef.input) { if (arrayIndexOf(NAMES_TYPE_RANGE, optionDef.input.type) < 0) { - throw new Error(errMsgPrefix + 'Sub input only supported for range input.'); + throw new Error(errMsgPrefix + ' Sub input only supported for range input.'); } - var createdRangeInput = createRangeInput(optionDef.input, { + var rangeInputCreated = createRangeInput(optionDef.input, { text: '', + id: optionDef.id, onchange: function () { - handleSelectChange(this.value) + if (selectCtx._disabled) { return; } + triggerUserSelectChangedEvent(); } - }); - selectCtx._elList.push(createdRangeInput.el); - selectCtx._optionIdxToSubInput[optionIdx] = createdRangeInput; + }, inputRecorder); + for (var idx = 0; idx < rangeInputCreated.elList.length; idx++) { + selectCtx._el.appendChild(rangeInputCreated.elList[idx]); + } + selectCtx._optionIdxToSubInput[optionIdx] = rangeInputCreated; } } } - function getStateForSelect() { - var subInputState = {}; - for (var optionIdx = 0; optionIdx < selectCtx._optionIdxToSubInput.length; optionIdx++) { - if (selectCtx._optionIdxToSubInput[optionIdx]) { - subInputState[optionIdx] = selectCtx._optionIdxToSubInput[optionIdx].getState(); + function resetSelectInputDisabled(opt) { + selectCtx._disabled = !!opt.disabled; + selectCtx._selectEl.disabled = selectCtx._disabled; + resetSelectInputWrapperCSS(selectCtx._selectWrapperEl, selectCtx._disabled); + resetSelectInputSubInputsDisabled(); + } + + function getSelectInputState() { + var optionIndex = getSelectInputOptionIndex(); + var state = {}; + state.optionIndex = optionIndex; + state.disabled = selectCtx._disabled; + if (selectCtx._optionIdxToSubInput.length) { // Make literal state short to save space. + state.optionStateMap = {}; + for (var optionIdx = 0; optionIdx < selectCtx._optionIdxToSubInput.length; optionIdx++) { + if (selectCtx._optionIdxToSubInput[optionIdx]) { + state.optionStateMap[optionIdx] = selectCtx._optionIdxToSubInput[optionIdx].getState(); + } } } - return { - valueIndex: getOptionIndex(selectCtx._selectEl), - subInputState: subInputState - }; + return state; } - function setStateForSelect(state) { - if (state == null - || getType(state.valueIndex) !== 'number' - || !isObject(state.subInputState) - ) { - throw new Error(errMsgPrefix + 'Invalid state: ' + printObject(state) + ' for select'); + function setSelectInputState(state) { + if (!isObject(state)) { + console.error( + errMsgPrefix + ' Invalid select input state: ' + printObject(state) + + ' May caused by test case change.' + ); + return; + } + if (!validateOptionIndex(state.optionIndex)) { + return; } - resetOptionIndex(state.valueIndex); - for (var optionIdx in state.subInputState) { - if (state.subInputState.hasOwnProperty(optionIdx)) { + + var optionStateMap = state.optionStateMap || {}; + for (var optionIdx in optionStateMap) { + if (state.optionStateMap.hasOwnProperty(optionIdx)) { var subInput = selectCtx._optionIdxToSubInput[optionIdx]; - if (subInput) { - subInput.setState(state.subInputState[optionIdx]); + if (!subInput) { + console.error( + errMsgPrefix + ' Invalid select input state: ' + printObject(state) + + ' Can not find a sub-input by optionIndex: ' + optionIdx + '.' + + ' May caused by test case change.' + ); + return; } } } + for (var optionIdx in optionStateMap) { + if (state.optionStateMap.hasOwnProperty(optionIdx)) { + var subInput = selectCtx._optionIdxToSubInput[optionIdx]; + subInput.setState(state.optionStateMap[optionIdx]); + } + } + resetSelectInputDisabled({disabled: state.disabled}); + resetSelectInputOptionIndex(state.optionIndex); + } + + function validateOptionIndex(optionIndex) { + if (getType(optionIndex) !== 'number' + || optionIndex < 0 + || optionIndex >= selectCtx._optionList.length + ) { + console.error( + errMsgPrefix + ' Invalid select, optionIndex: ' + optionIndex + ' is out if range.' + + ' May caused by test case change.' + ); + return false; + } + return true; } - function setInitValue(inputDefine) { + function setSelectInputInitValue(inputDefine) { var initOptionIdx = 0; - if (inputDefine.hasOwnProperty('valueIndex')) { - var valueIndex = inputDefine.valueIndex; - if (valueIndex < 0 || valueIndex >= selectCtx._optionList.length) { - throw new Error(errMsgPrefix + 'Invalid valueIndex: ' + valueIndex); + var initOptionIdxOpt = retrieveValue(inputDefine.optionIndex, inputDefine.valueIndex, undefined); + if (initOptionIdxOpt != null) { + if (initOptionIdxOpt < 0 || initOptionIdxOpt >= selectCtx._optionList.length) { + throw new Error(errMsgPrefix + ' Invalid optionIndex: ' + initOptionIdxOpt); } - selectCtx._selectEl.value = selectCtx._optionList[valueIndex].value; - initOptionIdx = valueIndex; + selectCtx._selectEl.value = selectCtx._optionList[initOptionIdxOpt].value; + initOptionIdx = initOptionIdxOpt; } else if (inputDefine.hasOwnProperty('value')) { var found = false; for (var idx = 0; idx < selectCtx._optionList.length; idx++) { - if (!selectCtx._optionList[idx].input && selectCtx._optionList[idx].value === inputDefine.value) { + if (!selectCtx._optionList[idx].input + && selectCtx._optionList[idx].value === inputDefine.value + ) { found = true; initOptionIdx = idx; } } if (!found) { - throw new Error(errMsgPrefix + 'Value not found in select options: ' + inputDefine.value); + throw new Error(errMsgPrefix + ' Value not found in select options: ' + inputDefine.value); } } - resetOptionIndex(initOptionIdx); + resetSelectInputOptionIndex(initOptionIdx); } - function resetOptionIndex(optionIdx) { - disableSubInputs(optionIdx); + function resetSelectInputOptionIndex(optionIdx) { selectCtx._selectEl.value = optionIdx; + resetSelectInputSubInputsDisabled(); } - function getOptionIndex(optionIndexHost) { - return +optionIndexHost.value; + function getSelectInputOptionIndex() { + return +selectCtx._selectEl.value; } - function getValueByOptionIndex(optionIdx) { + function getSelectInputValueByOptionIndex(optionIdx) { return selectCtx._optionList[optionIdx].input ? selectCtx._optionIdxToSubInput[optionIdx].getState().value : selectCtx._optionList[optionIdx].value; } - function handleSelectChange(value) { - var target = {value: value}; + function triggerUserSelectChangedEvent() { + var optionIdx = getSelectInputOptionIndex(); + var value = getSelectInputValueByOptionIndex(optionIdx); + var optionId = selectCtx._optionList[optionIdx].id; + var target = {value: value, optionId: optionId}; _selectListener.call(target, {target: target}); } - function disableSubInputs(currOptionIdx) { + function resetSelectInputSubInputsDisabled() { + var optionIdx = getSelectInputOptionIndex(); for (var i = 0; i < selectCtx._optionIdxToSubInput.length; i++) { var subInput = selectCtx._optionIdxToSubInput[i]; if (subInput) { - subInput.disable(i !== currOptionIdx); + var disabled = selectCtx._disabled + ? true // Disable all options. + : i !== optionIdx // Disable all except current selected option. + subInput.disable({disabled: disabled}); } } } - function makeTextByValue(optionDef) { + function makeSelectInputTextByValue(optionDef) { if (optionDef.hasOwnProperty('value')) { return printObject(optionDef.value, { arrayLineBreak: false, objectLineBreak: false, indent: 0, lineBreak: '' @@ -693,19 +1359,198 @@ return 'range input'; } } - } + } // End of createSelectInput + + function createGroupSetInput(groupSetDefine) { + assert( + getType(groupSetDefine.inputsHeight) === 'number', + '`inputsHeight` is mandatory on groupSet to avoid height change' + + ' to affects visual testing when switching groups.' + ) + assert( + getType(groupSetDefine.groups) === 'array', + '.groups must be an array.' + ); + assert( + groupSetDefine.groups.length > 0, + 'groupset.group must have at least one group' + ); + + var groupSetEl = document.createElement('div'); + initInputsContainer(groupSetEl, groupSetDefine, { + ignoreInputsStyle: true, + className: 'test-inputs-groupset', + }); + var groupSetMarginBottomEl = document.createElement('div'); + groupSetMarginBottomEl.className = 'test-inputs-groupset-margin-bottom'; + + var groupSetTextEl = document.createElement('div'); + groupSetTextEl.className = 'test-inputs-groupset-text'; + groupSetEl.appendChild(groupSetTextEl); + + var groupSetCreated = { + currentGroupIndex: 0, + elList: [groupSetEl, groupSetMarginBottomEl], + children: [], + getState: getGroupSetInputState, + setState: setGroupSetInputState, + switchGroup: switchGroup + }; + + for (var groupIdx = 0; groupIdx < groupSetDefine.groups.length; groupIdx++) { + var groupDefine = groupSetDefine.groups[groupIdx]; + assert(groupDefine, 'groupset.group must not be undefined/null.'); + + var groupChildInputsContainer = document.createElement('div'); + initInputsContainer(groupChildInputsContainer, groupSetDefine, { + ignoreFixHeight: true, + className: 'test-inputs-groupset-group', + }); + groupSetEl.appendChild(groupChildInputsContainer); + + var groupChildId = retrieveId(groupDefine, 'id'); + if (groupChildId == null) { + throw new Error('In group child input, id must be specified.'); + } + + var groupCreated = { + groupParent: groupSetCreated, + inputsContainerEl: groupChildInputsContainer, + groupSetTextEl: groupSetTextEl, + groupDefine: groupDefine, + idList: null, + groupIndex: groupSetCreated.children.length + }; + groupSetCreated.children.push(groupCreated); + + storeToInputDict(groupDefine, groupCreated); + + var inputsDefineList = retrieveInputDefineList(groupDefine).slice(); + + // Cascade `disabled`. + for (var inputIdx = 0; inputIdx < inputsDefineList.length; inputIdx++) { + var inputDefine = inputsDefineList[inputIdx]; + if (!inputDefine) { + continue; + } + assert(isObject(inputDefine)); + inputsDefineList[inputIdx] = inputDefine = Object.assign({}, inputDefine); + inputDefine.disabled = retrieveValue( + inputDefine.disabled, groupDefine.disabled, groupSetDefine.disabled + ); + } + + groupCreated.idList = dealInitEachInput(inputsDefineList, groupChildInputsContainer); + + showHideGroupInGroupSet(groupCreated, false); + } + + showHideGroupInGroupSet(groupSetCreated.children[groupSetCreated.currentGroupIndex], true); + + return groupSetCreated; + + function switchGroup(groupId) { + var groupCreatedToShow = retrieveAndVerifyGroup(groupId); + if (groupCreatedToShow.groupIndex === groupCreatedToShow.groupParent.currentGroupIndex) { + return; + } + var groupCreatedToHide = groupCreatedToShow.groupParent.children[ + groupCreatedToShow.groupParent.currentGroupIndex + ]; + showHideGroupInGroupSet(groupCreatedToHide, false); + showHideGroupInGroupSet(groupCreatedToShow, true); + groupCreatedToShow.groupParent.currentGroupIndex = groupCreatedToShow.groupIndex; + } + + function getGroupSetInputState() { + var state = {currentGroupIndex: groupSetCreated.currentGroupIndex}; + return state; + } + + function setGroupSetInputState(state) { + if (!isObject(state)) { + console.error( + errMsgPrefix + ' Invalid group set state: ' + printObject(state) + + ' May caused by test case change.' + ); + return; + } + var currentGroupIndex = state.currentGroupIndex; + if (getType(currentGroupIndex) !== 'number' + || currentGroupIndex < 0 + || currentGroupIndex >= groupSetCreated.children.length + ) { + console.error( + errMsgPrefix + ' Invalid group set currentGroupIndex: ' + currentGroupIndex + + ' May caused by test case change.' + ); + return; + } + switchGroup(currentGroupIndex); + } + + } // End of createGroupSetInput + + function createButtonInput(inputDefine, inputRecorder) { + var _btnDisabled = false; + var btn = document.createElement('button'); + btn.innerHTML = getInputsTextHTML(inputDefine, 'button'); + var _btnListener = getBtnEventListener(inputDefine, NAMES_ON_CLICK); + assert(_btnListener, 'No button onclick provided.'); + btn.addEventListener('click', inputRecorder.wrapUserInputListener({ + listener: function () { + if (_btnDisabled) { return; } + return _btnListener.apply(this, arguments); + }, + op: 'click' + })); + resetButtonInputDisabled(inputDefine); + + return { + elList: [btn], + disable: resetButtonInputDisabled, + setState: setButtonInputState, + getState: getButtonInputState + }; + + function resetButtonInputDisabled(opt) { + _btnDisabled = !!opt.disabled; + btn.disabled = _btnDisabled; + } + function getButtonInputState() { + return {disabled: _btnDisabled}; + } + function setButtonInputState(state) { + if (!isObject(state)) { + console.error( + errMsgPrefix + ' Button input state must be object rather than ' + printObject(state) + + ' May caused by test case change.' + ); + return; + } + resetButtonInputDisabled(state); + } + } // End of createButtonInput function createBr(inputDefine) { - return document.createElement('br'); + return {elList: [document.createElement('br')]}; } - function createButtonInput(inputDefine) { - var btn = document.createElement('button'); - btn.innerHTML = getBtnTextHTML(inputDefine, 'button'); - btn.addEventListener('click', getBtnEventListener(inputDefine, NAMES_ON_CLICK)); - return btn; + function createHr(inputDefine) { + var _hrWrapperEl = document.createElement('div'); + _hrWrapperEl.className = 'test-inputs-hr' + var textEl = document.createElement('span'); + textEl.className = 'test-inputs-hr-text'; + _hrWrapperEl.appendChild(textEl); + var text = textEl.innerHTML = getInputsTextHTML(inputDefine, ''); + textEl.style.display = text ? 'block' : 'none'; + + return { + elList: [_hrWrapperEl] + }; } - } + + } // End of initInputs function initRecordCanvas(opt, chart, recordCanvasContainer) { if (!opt.recordCanvas) { @@ -761,6 +1606,156 @@ } } + /** + * @param {EChartsInstance} chart + * @param {Parameter['boundingRect']} opt.boundingRect + */ + function initShowBoundingRects(chart, echarts, opt, boundingRectsContainer) { + assert(chart.__testHelper); + + var _bRectZr; + var _bRectGroup; + // @type Parameter['boundingRect'] + var _currBoundingRectOpt = false; + + chart.__testHelper.updateBoundingRects + = chart.__testHelper.updateBoundingRect + = chart.__testHelper.boundingRect + = chart.__testHelper.boundingRects + = updateBoundingRects; + + updateBoundingRects(opt.boundingRect); + + return; + + function updateBoundingRects(opt) { + if (arguments.length > 0) { + _currBoundingRectOpt = opt; + } // If no opt, keep the last one. + + _currBoundingRectOpt + ? buildBoundingRects(_currBoundingRectOpt) + : disableBoundingRects(); + } + + function ensureBoundingRectsFacilities() { + // zr requires size non-zero. + boundingRectsContainer.style.width = chart.getWidth() + 'px'; + boundingRectsContainer.style.height = chart.getHeight() + 'px'; + + if (_bRectZr) { + _bRectZr.resize(); + return; + } + + _bRectGroup = new echarts.graphic.Group(); + _bRectGroup.__testHelperBoundingRectsRoot = true; + _bRectGroup.on('click', function (event) { + var target = event.target; + if (!target || !target.__testHelperBoundingRectTarget) { + return; + } + var wrapper = { + boundingRect: target, + rawElement: target.__testHelperBoundingRectTarget + }; + console.log('boundingRect:', wrapper.boundingRect); + console.log('rawElement:', wrapper.rawElement); + window.$0 = wrapper; + }); + _bRectZr = echarts.zrender.init(boundingRectsContainer); + _bRectZr.add(_bRectGroup); + } + + function disableBoundingRects() { + chart.off('finished', updateBoundingRects); + boundingRectsContainer.style.display = 'none'; + if (_bRectGroup) { + _bRectGroup.removeAll(); + } + } + + function buildBoundingRects(boundingRectOpt) { + ensureBoundingRectsFacilities(); + boundingRectOpt = isObject(boundingRectOpt) ? boundingRectOpt : {}; + + boundingRectsContainer.style.display = 'block'; + _bRectGroup.removeAll(); + + var strokeColor = boundingRectOpt.color || 'rgba(0,0,255,0.5)'; + var silent = boundingRectOpt.silent != null ? boundingRectOpt.silent : false; + + boundingRectsContainer.style.pointerEvent = silent ? 'none' : 'auto'; + + var roots = chart.getZr().storage.getRoots(); + for (var rootIdx = 0; rootIdx < roots.length; rootIdx++) { + travelGroupAndBuildRects(roots[rootIdx], _bRectGroup); + } + + // Follow chart update and resize. + chart.on('finished', updateBoundingRects); + + return; + + function travelGroupAndBuildRects(group, visualRectGroupParent) { + var visualRectGroup = createVisualRectGroup(group, visualRectGroupParent) + group.eachChild(function (child) { + if (child.isGroup) { + travelGroupAndBuildRects(child, visualRectGroup); + return; + } + + createRectForDisplayable(child, visualRectGroup); + + var textContent = child.getTextContent(); + var textGuildLine = child.getTextGuideLine(); + if (textContent || textGuildLine) { + textContent && createRectForDisplayable(textContent, _bRectGroup, true); + textGuildLine && createRectForDisplayable(textGuildLine, _bRectGroup, true); + } + }); + + function createVisualRectGroup(fromEl, visualRectGroupParent) { + var visualRectGroup = new echarts.graphic.Group(); + copyTransformAttrs(visualRectGroup, fromEl); + visualRectGroupParent.add(visualRectGroup); + return visualRectGroup; + } + + function createRectForDisplayable(el, visualRectGroup, useInnerTransformable) { + var elRawRect = el.getBoundingRect(); + var visualRect = new echarts.graphic.Rect({ + shape: {x: elRawRect.x, y: elRawRect.y, width: elRawRect.width, height: elRawRect.height}, + style: {fill: null, stroke: strokeColor, lineWidth: 1, strokeNoScale: true}, + silent: silent, + z: Number.MAX_SAFE_INTEGER + }); + visualRect.__testHelperBoundingRectTarget = el; + var transAttrSource = el; + if (useInnerTransformable && el.innerTransformable) { + transAttrSource = el.innerTransformable; + } + copyTransformAttrs(visualRect, transAttrSource); + visualRectGroup.add(visualRect); + } + } + + function copyTransformAttrs(target, source) { + target.x = source.x; + target.y = source.y; + target.rotation = source.rotation; + target.scaleX = source.scaleX; + target.scaleY = source.scaleY; + target.originX = source.originX; + target.originY = source.originY; + target.skewX = source.skewX; + target.skewY = source.skewY; + target.anchorX = source.anchorX; + target.anchorY = source.anchorY; + } + } + } + testHelper.createRecordVideo = function (chart, recordVideoContainer) { var button = document.createElement('button'); button.innerHTML = 'Start Recording'; @@ -816,7 +1811,7 @@ theme = window.__ECHARTS__DEFAULT__THEME__; } if (theme) { - require([`theme/${theme}`]); + require(['theme/' + theme]); } var chart = echarts.init(dom, theme, { @@ -828,11 +1823,11 @@ if (opt.draggable) { if (!window.draggable) { throw new Error( - errMsgPrefix + 'Pleasse add the script in HTML: \n' + errMsgPrefix + ' Pleasse add the script in HTML: \n' + '' ); } - window.draggable.init(dom, chart, {throttle: 70}); + window.draggable.init(dom, chart, {throttle: 70, onResize: opt.onResize}); } option && chart.setOption(option, { @@ -842,7 +1837,7 @@ var isAutoResize = opt.autoResize == null ? true : opt.autoResize; if (isAutoResize) { - testHelper.resizable(chart); + testHelper.resizable(chart, {onResize: opt.onResize}); } return chart; @@ -901,9 +1896,10 @@ resultDom.style.cssText = [ 'position: absolute;', 'left: 20px;', + 'pointer-events: none;', 'font-size: ' + fontSize + 'px;', 'z-index: ' + (failErr ? 99999 : 88888) + ';', - 'color: ' + (failErr ? 'red' : 'green') + ';', + 'color: ' + (failErr ? 'rgba(150,0,0,0.8)' : 'rgba(0,150,0,0.8)') + ';', ].join(''); printAssertRecord.push(resultDom); hostDOMEl.appendChild(resultDom); @@ -1015,7 +2011,8 @@ } } - testHelper.resizable = function (chart) { + testHelper.resizable = function (chart, opt) { + opt = opt || {}; var dom = chart.getDom(); var width = dom.clientWidth; var height = dom.clientHeight; @@ -1024,13 +2021,20 @@ var newHeight = dom.clientHeight; if (width !== newWidth || height !== newHeight) { chart.resize(); + if (chart.__testHelper && chart.__testHelper.updateBoundingRects) { + chart.__testHelper.updateBoundingRects(); + } width = newWidth; height = newHeight; + + if (opt.onResize) { + opt.onResize(); + } } } if (window.attachEvent) { // Use builtin resize in IE - window.attachEvent('onresize', chart.resize); + window.attachEvent('onresize', resize); } else if (window.addEventListener) { window.addEventListener('resize', resize, false); @@ -1086,7 +2090,7 @@ return '/' + resolvedPath; }; - testHelper.encodeHTML = function (source) { + var encodeHTML = testHelper.encodeHTML = function (source) { return String(source) .replace(/&/g, '&') .replace(/ 3) { + // `3` is an arbitrary value, considering a path array: + // [ + // [1,2], [3,4], [5,6], + // [7,8], [9,10] + // ] + preventParentArrayPartiallyBreak = true; + } + if (!forceObjectLineBreak && maxColumnWithoutLineBreak > lineBreakMaxColumn) { + hasLineBreak = true; } var tail = hasLineBreak ? lineBreak : ''; - var delimiter = ',' + (hasLineBreak ? (lineBreak + subCodeIndent) : ' '); var subPre = hasLineBreak ? subCodeIndent : ''; var endPre = hasLineBreak ? codeIndent : ''; - str = '' - + preStr + '[' + tail - + subPre + childBuilder.join(delimiter) + tail - + endPre + ']'; + var delimiterInline = ', '; + var delimiterBreak = ',' + lineBreak + subCodeIndent; + if (!childBuilder.length) { + str = preStr + '[]'; + } + else { + var subContentStr = ''; + var subContentMaxColumn = 0; + if (canPartiallyBreak && hasLineBreak) { + for (var idx = 0; idx < childBuilder.length; idx++) { + var childStr = childBuilder[idx]; + subContentMaxColumn += childStr.length + delimiterInline.length; + if (idx === childBuilder.length - 1) { + subContentStr += childStr; + } + else if (subContentMaxColumn > lineBreakMaxColumn) { + subContentStr += childStr + delimiterBreak; + subContentMaxColumn = 0; + } + else { + subContentStr += childStr + delimiterInline; + } + } + } + else { + subContentStr = childBuilder.join(hasLineBreak ? delimiterBreak : delimiterInline); + } + str = '' + + preStr + '[' + tail + + subPre + subContentStr + tail + + endPre + ']'; + } break; case 'object': - hasLineBreak = opt.objectLineBreak != null ? opt.objectLineBreak : true; + if (forceObjectLineBreak) { + hasLineBreak = !!opt.objectLineBreak; + } var childBuilder = []; + var maxColumnWithoutLineBreak = preStr.length; + var keyCount = 0; for (var i in obj) { if (obj.hasOwnProperty(i)) { + keyCount++; var subResult = doPrint(obj[i], i, depth + 1); childBuilder.push(subResult.str); + if (subResult.hasLineBreak) { hasLineBreak = true; } + else { + maxColumnWithoutLineBreak += subResult.str.length + 2; // `2` is ', '.length + } + + if (subResult.preventParentArrayPartiallyBreak) { + preventParentArrayPartiallyBreak = true; + } } } - str = '' - + preStr + '{' + (hasLineBreak ? lineBreak : '') - + (childBuilder.length - ? (hasLineBreak ? subCodeIndent : '') + childBuilder.join(',' + (hasLineBreak ? lineBreak + subCodeIndent: ' ')) + (hasLineBreak ? lineBreak: '') - : '' - ) - + (hasLineBreak ? codeIndent : '') + '}'; + if (keyCount > 1) { + // `3` is an arbitrary value, considering case like: + // [ + // {name: 'xx'}, {name: 'yy'}, {name: 'zz'}, + // {name: 'aa'}, {name: 'bb'} + // ] + preventParentArrayPartiallyBreak = true; + } + if (!forceObjectLineBreak && maxColumnWithoutLineBreak > lineBreakMaxColumn) { + hasLineBreak = true; + } + if (!childBuilder.length) { + str = preStr + '{}'; + } + else { + str = '' + + preStr + '{' + (hasLineBreak ? lineBreak : '') + + (hasLineBreak ? subCodeIndent : '') + + childBuilder.join(',' + (hasLineBreak ? lineBreak + subCodeIndent: ' ')) + + (hasLineBreak ? lineBreak: '') + + (hasLineBreak ? codeIndent : '') + '}'; + } break; case 'boolean': case 'number': str = preStr + obj + ''; break; case 'string': - str = JSON.stringify(obj); // escapse \n\r or others. - str = preStr + quotationMark + str.slice(1, str.length - 1) + quotationMark; + str = preStr + convertStringToJSLiteral(obj, quotationMark); break; default: str = preStr + obj + ''; + preventParentArrayPartiallyBreak = true; } return { str: str, - hasLineBreak: hasLineBreak + hasLineBreak: hasLineBreak, + isMethodShorthand: isMethodShorthand, + preventParentArrayPartiallyBreak: preventParentArrayPartiallyBreak }; } + + /** + * Simple implementation for detecting method shorthand, such as, + * ({abc() { return 1; }}).abc is a method shorthand and needs to + * be serialized as `{abc() { return 1; }}` rather than `{abc: abc() { return 1; }}`. + * Those cases can be detected: + * ({abc() { console.log('=>'); return 1; }}).abc expected: IS_SHORTHAND + * ({abc(x, y = 5) { return 1; }}).abc expected: IS_SHORTHAND + * ({$ab_c() { return 1; }}).$ab_c expected: IS_SHORTHAND + * ({*abc() { return 1; }}).abc expected: IS_SHORTHAND + * ({* abc() { return 1; }}).abc expected: IS_SHORTHAND + * ({async abc() { return 1; }}).abc expected: IS_SHORTHAND + * ({*abc() { yield 1; }}).abc expected: IS_SHORTHAND + * ({abc(x, y) { return x + y; }}).abc expected: IS_SHORTHAND + * ({abc: function abc() { return 1; }}).abc expected: NOT_SHORTHAND + * ({abc: function def() { return 1; }}).abc expected: NOT_SHORTHAND + * ({abc: function() { return 1; }}).abc expected: NOT_SHORTHAND + * ({abc: function* () { return 1; }}).abc expected: NOT_SHORTHAND + * ({abc: function (aa, bb) { return 1; }}).abc expected: NOT_SHORTHAND + * ({abc: function (aa, bb = 5) { return 1; }}).abc expected: NOT_SHORTHAND + * ({abc: async () => { return 1; }}).abc expected: NOT_SHORTHAND + * ({abc: () => { return 1; }}).abc expected: NOT_SHORTHAND + * ({abc: (aa, bb = 5) => { return 1; }}).abc expected: NOT_SHORTHAND + * FIXME: fail at some rare cases, such as: + * Literal string involved, like: + * ({"ab-() ' =>c"() { return 1; }})["ab-() ' =>c"] expected: IS_SHORTHAND + * ({async "ab-c"() { return 1; }})["ab-c"] expected: IS_SHORTHAND + * Computed property name involved, like: + * ({[some]() { return 1; }})[some] expected: IS_SHORTHAND + */ + function isMethodShorthandNotAccurate(fnStr, fnName, objKey) { + // Assert fnStr, fnName, objKey is a string. + if (fnName !== objKey) { + return false; + } + var matched = fnStr.match(/^\s*(async\s+)?(function\s*)?(\*\s*)?([a-zA-Z$_][a-zA-Z0-9$_]*)?\s*\(/); + if (!matched) { + return false; + } + if (matched[2]) { // match 'function' + return false; + } + // May enhanced by /(['"])(?:(?=(\\?))\2.)*?\1/; to match literal string, + // such as "ab-c", "a\nc". But this simple impl does not cover it. + if (!matched[4] || matched[4] !== objKey) { // match "maybe function name" + return false; + } + return true; + } + }; /** @@ -1278,7 +2449,7 @@ * @param {function} [opt.filter] print a subtree only if any satisfied node exists. * param: el, return: boolean */ - testHelper.stringifyElements = function (chart, opt) { + var stringifyElements = testHelper.stringifyElements = function (chart, opt) { if (!chart) { return; } @@ -1368,7 +2539,7 @@ * * @see `stringifyElements`. */ - testHelper.printElements = function (chart, opt) { + var printElements = testHelper.printElements = function (chart, opt) { var elsStr = testHelper.stringifyElements(chart, opt); console.log(elsStr); }; @@ -1388,7 +2559,7 @@ * param: el, return: boolean * @return {Array.} */ - testHelper.retrieveElements = function (chart, opt) { + var retrieveElements = testHelper.retrieveElements = function (chart, opt) { if (!chart) { return; } @@ -1457,6 +2628,20 @@ document.body.appendChild(canvas); }; + function initDataTables(opt, dataTableContainer) { + var dataTables = opt.dataTables; + if (!dataTables && opt.dataTable) { + dataTables = [opt.dataTable]; + } + if (dataTables) { + var tableHTML = []; + for (var i = 0; i < dataTables.length; i++) { + tableHTML.push(createDataTableHTML(dataTables[i], opt)); + } + dataTableContainer.innerHTML = tableHTML.join(''); + } + } + function createDataTableHTML(data, opt) { var sourceFormat = detectSourceFormat(data); var dataTableLimit = opt.dataTableLimit || DEFAULT_DATA_TABLE_LIMIT; @@ -1473,7 +2658,7 @@ var htmlLine = ['']; for (var j = 0; j < line.length; j++) { var val = i === dataTableLimit ? '...' : line[j]; - htmlLine.push('' + testHelper.encodeHTML(val) + ''); + htmlLine.push('' + encodeHTML(val) + ''); } htmlLine.push(''); html.push(htmlLine.join('')); @@ -1486,9 +2671,9 @@ for (var key in line) { if (line.hasOwnProperty(key)) { var keyText = i === dataTableLimit ? '...' : key; - htmlLine.push('' + testHelper.encodeHTML(keyText) + ''); + htmlLine.push('' + encodeHTML(keyText) + ''); var val = i === dataTableLimit ? '...' : line[key]; - htmlLine.push('' + testHelper.encodeHTML(val) + ''); + htmlLine.push('' + encodeHTML(val) + ''); } } htmlLine.push(''); @@ -1498,12 +2683,12 @@ else if (sourceFormat === 'keyedColumns') { for (var key in data) { var htmlLine = ['']; - htmlLine.push('' + testHelper.encodeHTML(key) + ''); + htmlLine.push('' + encodeHTML(key) + ''); if (data.hasOwnProperty(key)) { var col = data[key] || []; for (var i = 0; i < col.length && i <= dataTableLimit; i++) { var val = i === dataTableLimit ? '...' : col[i]; - htmlLine.push('' + testHelper.encodeHTML(val) + ''); + htmlLine.push('' + encodeHTML(val) + ''); } } htmlLine.push(''); @@ -1539,7 +2724,7 @@ function createObjectHTML(obj, key) { var html = isObject(obj) - ? testHelper.encodeHTML(printObject(obj, key)) + ? encodeHTML(printObject(obj, key)) : obj ? obj.toString() : ''; @@ -1608,6 +2793,12 @@ return -1; } + var assert = testHelper.assert = function (cond, msg) { + if (!cond) { + throw new Error(msg || 'Assertion failed.'); + } + } + function makeFlexibleNames(dashedNames) { var nameMap = {}; for (var i = 0; i < dashedNames.length; i++) { @@ -1634,6 +2825,71 @@ return names; } + /** + * Copied from src/util/number.ts + */ + function getPrecision(val) { + val = +val; + if (isNaN(val)) { + return 0; + } + + // It is much faster than methods converting number to string as follows + // let tmp = val.toString(); + // return tmp.length - 1 - tmp.indexOf('.'); + // especially when precision is low + // Notice: + // (1) If the loop count is over about 20, it is slower than `getPrecisionSafe`. + // (see https://jsbench.me/2vkpcekkvw/1) + // (2) If the val is less than for example 1e-15, the result may be incorrect. + // (see test/ut/spec/util/number.test.ts `getPrecision_equal_random`) + if (val > 1e-14) { + var e = 1; + for (var i = 0; i < 15; i++, e *= 10) { + if (Math.round(val * e) / e === val) { + return i; + } + } + } + + return getPrecisionSafe(val); + } + + /** + * Copied from src/util/number.ts + * Get precision with slow but safe method + */ + function getPrecisionSafe(val) { + // toLowerCase for: '3.4E-12' + var str = val.toString().toLowerCase(); + + // Consider scientific notation: '3.4e-12' '3.4e+12' + var eIndex = str.indexOf('e'); + var exp = eIndex > 0 ? +str.slice(eIndex + 1) : 0; + var significandPartLen = eIndex > 0 ? eIndex : str.length; + var dotIndex = str.indexOf('.'); + var decimalPartLen = dotIndex < 0 ? 0 : significandPartLen - 1 - dotIndex; + return Math.max(0, decimalPartLen - exp); + } + + /** + * Copied from src/util/number.ts + */ + function round(x, precision, returnStr) { + if (precision == null) { + precision = 10; + } + // Avoid range error + precision = Math.min(Math.max(0, precision), ROUND_SUPPORTED_PRECISION_MAX); + // PENDING: 1.005.toFixed(2) is '1.00' rather than '1.01' + x = (+x).toFixed(precision); + return (returnStr ? x : +x); + } + // Although chrome already enlarge this number to 100 for `toFixed`, but + // we sill follow the spec for compatibility. + var ROUND_SUPPORTED_PRECISION_MAX = 20; + + function objectNoOtherNotNullUndefinedPropExcept(obj, exceptProps) { if (!obj) { return false; @@ -1646,9 +2902,94 @@ return true; } + var copyToClipboard = function (text) { + if (typeof navigator === 'undefined' || !navigator.clipboard || !navigator.clipboard.writeText) { + console.error('[clipboard] Can not copy to clipboard.'); + return; + } + return navigator.clipboard.writeText(text).then(function () { + console.log('[clipboard] Text copied to clipboard.'); + }).catch(function (err) { + console.error('[clipboard] Failed to copy text: ', err); // Just print for easy to use. + return err; + }); + }; + + /** + * A shortcut for both stringify and copy to clipboard. + * + * @param {any} val Any val to stringify and copy to clipboard. + * @param {Object?} printObjectOpt Optional. + */ + testHelper.clipboard = function (val, printObjectOpt) { + var literal = testHelper.printObject(val, printObjectOpt); + if (document.hasFocus()) { + copyToClipboard(literal); + } + else { + // Handle the error: + // NotAllowedError: Failed to execute 'writeText' on 'Clipboard': Document is not focused. + ensureClipboardButton(); + updateClipboardButton(literal) + console.log( + '⚠️ [clipboard] Please click the new button that appears on the top-left corner of the screen' + + ' to copy to clipboard.' + ); + } + + function updateClipboardButton(text) { + var button = __tmpClipboardButttonWrapper.button; + button.innerHTML = 'Click me to copy to clipboard'; + button.style.display = 'block'; + __tmpClipboardButttonWrapper.text = text; + } + + function ensureClipboardButton() { + var button = __tmpClipboardButttonWrapper.button; + if (button != null) { + return; + } + __tmpClipboardButttonWrapper.button = button = document.createElement('div'); + button.style.cssText = [ + 'height: 80px;', + 'line-height: 80px;', + 'padding: 10px 20px;', + 'margin: 5px;', + 'text-align: center;', + 'position: fixed;', + 'top: 10px;', + 'left: 10px;', + 'z-index: 9999;', + 'cursor: pointer;', + 'color: #fff;', + 'background-color: #333;', + 'border: 2px solid #eee;', + 'border-radius: 5px;', + 'font-size: 18px;', + 'font-weight: bold;', + 'font-family: sans-serif;', + 'box-shadow: 0 4px 10px rgba(0, 0, 0, 0.8);' + ].join(''); + document.body.appendChild(button); + button.addEventListener('click', function () { + copyToClipboard(__tmpClipboardButttonWrapper.text).then(function (err) { + if (!err) { + button.style.display = 'none'; + } + else { + button.innerHTML = 'error, see console log.'; + } + }); + }); + } + // Do not return the text, because it may be too long for a console.log. + }; + var __tmpClipboardButttonWrapper = {}; + + // It may be changed by test case changing. Do not use it as a persistent id. var _idBase = 1; - function generateId(prefix) { - return prefix + '' + (_idBase++); + function generateNonPersistentId(prefix) { + return (prefix || '') + '' + (_idBase++); } function VideoRecorder(chart) { @@ -1715,4 +3056,4 @@ context.testHelper = testHelper; -})(window); \ No newline at end of file +})(window); diff --git a/test/matrix.html b/test/matrix.html new file mode 100644 index 0000000000..ba553194f5 --- /dev/null +++ b/test/matrix.html @@ -0,0 +1,602 @@ + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/matrix2.html b/test/matrix2.html new file mode 100644 index 0000000000..dd3049f992 --- /dev/null +++ b/test/matrix2.html @@ -0,0 +1,1803 @@ + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/matrix3.html b/test/matrix3.html new file mode 100644 index 0000000000..ba219bed2e --- /dev/null +++ b/test/matrix3.html @@ -0,0 +1,1319 @@ + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/matrix_application.html b/test/matrix_application.html new file mode 100644 index 0000000000..0f6ea81a5f --- /dev/null +++ b/test/matrix_application.html @@ -0,0 +1,1381 @@ + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/matrix_application2.html b/test/matrix_application2.html new file mode 100644 index 0000000000..2bc8047be9 --- /dev/null +++ b/test/matrix_application2.html @@ -0,0 +1,801 @@ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + diff --git a/test/pie-coordinate-system.html b/test/pie-coordinate-system.html index 387d4c7c75..cdc8bf0073 100644 --- a/test/pie-coordinate-system.html +++ b/test/pie-coordinate-system.html @@ -274,7 +274,21 @@ radius: 20, center: ['Tue', 230], data: [ - { value: 1048, name: 'A' }, + { value: 1048, name: 'use_center' }, + { value: 735, name: 'B' }, + { value: 580, name: 'C' }, + { value: 484, name: 'D' }, + { value: 300, name: 'E' } + ] + }, + { + type: 'pie', + name: 'Cartesian2D(category axis & value axis)', + coordinateSystem: 'cartesian2d', + radius: 20, + coord: ['Fri', 130], + data: [ + { value: 1048, name: 'use_coord' }, { value: 735, name: 'B' }, { value: 580, name: 'C' }, { value: 484, name: 'D' }, diff --git a/test/runTest/actions/__meta__.json b/test/runTest/actions/__meta__.json index 397679110f..1350aa339b 100644 --- a/test/runTest/actions/__meta__.json +++ b/test/runTest/actions/__meta__.json @@ -110,6 +110,7 @@ "graph-case": 3, "graph-grid": 1, "graph-simple": 2, + "graph-thumbnail": 4, "graphic-animation": 1, "graphic-animation-wave": 1, "graphic-cases": 2, @@ -150,6 +151,10 @@ "mapWorld": 1, "markArea": 3, "marker-case": 1, + "matrix_application": 4, + "matrix_application2": 2, + "matrix2": 5, + "matrix3": 3, "media-dataZoom": 1, "media-finance": 2, "media-pie": 1, diff --git a/test/runTest/actions/graph-thumbnail.json b/test/runTest/actions/graph-thumbnail.json new file mode 100644 index 0000000000..181861398d --- /dev/null +++ b/test/runTest/actions/graph-thumbnail.json @@ -0,0 +1 @@ +[{"name":"Action 1","ops":[{"type":"mousemove","time":30,"x":690,"y":368},{"type":"mousemove","time":231,"x":410,"y":385},{"type":"mousemove","time":437,"x":378,"y":383},{"type":"mousewheel","time":814,"x":378,"y":383,"deltaY":-1},{"type":"mousewheel","time":835,"x":378,"y":383,"deltaY":-4},{"type":"mousewheel","time":854,"x":378,"y":383,"deltaY":-9},{"type":"mousewheel","time":873,"x":378,"y":383,"deltaY":-11},{"type":"mousewheel","time":893,"x":378,"y":383,"deltaY":-10},{"type":"mousewheel","time":913,"x":378,"y":383,"deltaY":-16},{"type":"mousewheel","time":933,"x":378,"y":383,"deltaY":-6},{"type":"mousewheel","time":954,"x":378,"y":383,"deltaY":-4},{"type":"mousewheel","time":975,"x":378,"y":383,"deltaY":-3},{"type":"mousewheel","time":997,"x":378,"y":383,"deltaY":-3},{"type":"mousewheel","time":1024,"x":378,"y":383,"deltaY":-4},{"type":"mousewheel","time":1045,"x":378,"y":383,"deltaY":-2},{"type":"mousewheel","time":1064,"x":378,"y":383,"deltaY":-4},{"type":"mousewheel","time":1086,"x":378,"y":383,"deltaY":-1},{"type":"mousewheel","time":1109,"x":378,"y":383,"deltaY":-1},{"type":"mousewheel","time":1132,"x":378,"y":383,"deltaY":-2},{"type":"mousewheel","time":1229,"x":378,"y":383,"deltaY":1},{"type":"mousewheel","time":1248,"x":378,"y":383,"deltaY":1},{"type":"mousewheel","time":1267,"x":378,"y":383,"deltaY":2},{"type":"mousewheel","time":1289,"x":378,"y":383,"deltaY":3},{"type":"mousewheel","time":1310,"x":378,"y":383,"deltaY":5},{"type":"mousewheel","time":1329,"x":378,"y":383,"deltaY":10},{"type":"mousewheel","time":1350,"x":378,"y":383,"deltaY":5},{"type":"mousewheel","time":1373,"x":378,"y":383,"deltaY":4},{"type":"mousewheel","time":1392,"x":378,"y":383,"deltaY":3},{"type":"mousewheel","time":1411,"x":378,"y":383,"deltaY":4},{"type":"mousewheel","time":1429,"x":378,"y":383,"deltaY":5},{"type":"mousewheel","time":1448,"x":378,"y":383,"deltaY":2},{"type":"mousewheel","time":1468,"x":378,"y":383,"deltaY":1},{"type":"mousewheel","time":1488,"x":378,"y":383,"deltaY":1},{"type":"mousewheel","time":1509,"x":378,"y":383,"deltaY":1},{"type":"mousewheel","time":1531,"x":378,"y":383,"deltaY":1},{"type":"mousewheel","time":1846,"x":378,"y":383,"deltaY":-1},{"type":"mousewheel","time":1862,"x":378,"y":383,"deltaY":-4},{"type":"mousewheel","time":1879,"x":378,"y":383,"deltaY":-8},{"type":"mousewheel","time":1896,"x":378,"y":383,"deltaY":-17},{"type":"mousewheel","time":1929,"x":378,"y":383,"deltaY":-39},{"type":"mousewheel","time":1946,"x":378,"y":383,"deltaY":-79},{"type":"mousewheel","time":1964,"x":378,"y":383,"deltaY":-31},{"type":"mousewheel","time":1982,"x":378,"y":383,"deltaY":-47},{"type":"mousewheel","time":2000,"x":378,"y":383,"deltaY":-54},{"type":"mousewheel","time":2018,"x":378,"y":383,"deltaY":-58},{"type":"mousewheel","time":2035,"x":378,"y":383,"deltaY":-58},{"type":"mousewheel","time":2054,"x":378,"y":383,"deltaY":-56},{"type":"mousewheel","time":2070,"x":378,"y":383,"deltaY":-114},{"type":"mousewheel","time":2088,"x":378,"y":383,"deltaY":-56},{"type":"mousewheel","time":2105,"x":378,"y":383,"deltaY":-53},{"type":"mousewheel","time":2122,"x":378,"y":383,"deltaY":-49},{"type":"mousewheel","time":2138,"x":378,"y":383,"deltaY":-45},{"type":"mousewheel","time":2155,"x":378,"y":383,"deltaY":-41},{"type":"mousewheel","time":2172,"x":378,"y":383,"deltaY":-38},{"type":"mousewheel","time":2189,"x":378,"y":383,"deltaY":-35},{"type":"mousewheel","time":2209,"x":378,"y":383,"deltaY":-32},{"type":"mousewheel","time":2229,"x":378,"y":383,"deltaY":-30},{"type":"mousewheel","time":2246,"x":378,"y":383,"deltaY":-27},{"type":"mousewheel","time":2263,"x":378,"y":383,"deltaY":-24},{"type":"mousewheel","time":2280,"x":378,"y":383,"deltaY":-22},{"type":"mousewheel","time":2297,"x":378,"y":383,"deltaY":-20},{"type":"mousewheel","time":2315,"x":378,"y":383,"deltaY":-19},{"type":"mousewheel","time":2335,"x":378,"y":383,"deltaY":-17},{"type":"mousewheel","time":2356,"x":378,"y":383,"deltaY":-16},{"type":"mousewheel","time":2374,"x":378,"y":383,"deltaY":-27},{"type":"mousewheel","time":2394,"x":378,"y":383,"deltaY":-12},{"type":"mousewheel","time":2496,"x":378,"y":383,"deltaY":1},{"type":"mousewheel","time":2513,"x":378,"y":383,"deltaY":5},{"type":"mousewheel","time":2530,"x":378,"y":383,"deltaY":13},{"type":"mousewheel","time":2549,"x":378,"y":383,"deltaY":20},{"type":"mousewheel","time":2567,"x":378,"y":383,"deltaY":26},{"type":"mousewheel","time":2585,"x":378,"y":383,"deltaY":29},{"type":"mousewheel","time":2605,"x":378,"y":383,"deltaY":21},{"type":"mousewheel","time":2623,"x":378,"y":383,"deltaY":21},{"type":"mousewheel","time":2642,"x":378,"y":383,"deltaY":16},{"type":"mousewheel","time":2661,"x":378,"y":383,"deltaY":28},{"type":"mousewheel","time":2679,"x":378,"y":383,"deltaY":97},{"type":"mousewheel","time":2699,"x":378,"y":383,"deltaY":33},{"type":"mousewheel","time":2717,"x":378,"y":383,"deltaY":32},{"type":"mousewheel","time":2735,"x":378,"y":383,"deltaY":36},{"type":"mousewheel","time":2752,"x":378,"y":383,"deltaY":35},{"type":"mousewheel","time":2769,"x":378,"y":383,"deltaY":32},{"type":"mousewheel","time":2787,"x":378,"y":383,"deltaY":29},{"type":"mousewheel","time":2805,"x":378,"y":383,"deltaY":26},{"type":"mousewheel","time":2822,"x":378,"y":383,"deltaY":24},{"type":"mousewheel","time":2841,"x":378,"y":383,"deltaY":22},{"type":"mousewheel","time":2860,"x":378,"y":383,"deltaY":20},{"type":"mousewheel","time":2879,"x":378,"y":383,"deltaY":36},{"type":"mousewheel","time":2906,"x":378,"y":383,"deltaY":16},{"type":"mousewheel","time":2925,"x":378,"y":383,"deltaY":14},{"type":"mousewheel","time":2945,"x":378,"y":383,"deltaY":25},{"type":"mousewheel","time":2966,"x":378,"y":383,"deltaY":11},{"type":"mousewheel","time":2988,"x":378,"y":383,"deltaY":10},{"type":"mousewheel","time":3007,"x":378,"y":383,"deltaY":9},{"type":"mousewheel","time":3027,"x":378,"y":383,"deltaY":8},{"type":"mousewheel","time":3045,"x":378,"y":383,"deltaY":15},{"type":"mousewheel","time":3062,"x":378,"y":383,"deltaY":7},{"type":"mousewheel","time":3129,"x":378,"y":383,"deltaY":1},{"type":"mousewheel","time":3147,"x":378,"y":383,"deltaY":4},{"type":"mousewheel","time":3166,"x":378,"y":383,"deltaY":7},{"type":"mousewheel","time":3188,"x":378,"y":383,"deltaY":12},{"type":"mousewheel","time":3210,"x":378,"y":383,"deltaY":20},{"type":"mousewheel","time":3231,"x":378,"y":383,"deltaY":47},{"type":"mousewheel","time":3249,"x":378,"y":383,"deltaY":37},{"type":"mousewheel","time":3267,"x":378,"y":383,"deltaY":86},{"type":"mousewheel","time":3285,"x":378,"y":383,"deltaY":43},{"type":"mousewheel","time":3304,"x":378,"y":383,"deltaY":42},{"type":"mousewheel","time":3321,"x":378,"y":383,"deltaY":41},{"type":"mousewheel","time":3339,"x":378,"y":383,"deltaY":45},{"type":"mousewheel","time":3356,"x":378,"y":383,"deltaY":43},{"type":"mousewheel","time":3373,"x":378,"y":383,"deltaY":39},{"type":"mousewheel","time":3391,"x":378,"y":383,"deltaY":37},{"type":"mousewheel","time":3409,"x":378,"y":383,"deltaY":33},{"type":"mousewheel","time":3428,"x":378,"y":383,"deltaY":31},{"type":"mousewheel","time":3448,"x":378,"y":383,"deltaY":28},{"type":"mousewheel","time":3471,"x":378,"y":383,"deltaY":50},{"type":"mousewheel","time":3493,"x":378,"y":383,"deltaY":22},{"type":"mousewheel","time":3546,"x":378,"y":383,"deltaY":2},{"type":"mousewheel","time":3565,"x":378,"y":383,"deltaY":9},{"type":"mousewheel","time":3585,"x":378,"y":383,"deltaY":8},{"type":"mousewheel","time":3604,"x":378,"y":383,"deltaY":10},{"type":"mousewheel","time":3622,"x":378,"y":383,"deltaY":14},{"type":"mousewheel","time":3641,"x":378,"y":383,"deltaY":13},{"type":"mousewheel","time":3659,"x":378,"y":383,"deltaY":12},{"type":"mousewheel","time":3678,"x":378,"y":383,"deltaY":21},{"type":"mousewheel","time":3697,"x":378,"y":383,"deltaY":6},{"type":"mousewheel","time":3717,"x":378,"y":383,"deltaY":4},{"type":"mousewheel","time":3736,"x":378,"y":383,"deltaY":2},{"type":"mousewheel","time":3754,"x":378,"y":383,"deltaY":1},{"type":"mousewheel","time":3773,"x":378,"y":383,"deltaY":1},{"type":"mousewheel","time":3846,"x":378,"y":383,"deltaY":-1},{"type":"mousewheel","time":3862,"x":378,"y":383,"deltaY":-1},{"type":"mousewheel","time":3879,"x":378,"y":383,"deltaY":-4},{"type":"mousewheel","time":3896,"x":378,"y":383,"deltaY":-9},{"type":"mousewheel","time":3914,"x":378,"y":383,"deltaY":-17},{"type":"mousewheel","time":3932,"x":378,"y":383,"deltaY":-20},{"type":"mousewheel","time":3952,"x":378,"y":383,"deltaY":-28},{"type":"mousewheel","time":3972,"x":378,"y":383,"deltaY":-28},{"type":"mousewheel","time":3991,"x":378,"y":383,"deltaY":-31},{"type":"mousewheel","time":4010,"x":378,"y":383,"deltaY":-69},{"type":"mousewheel","time":4031,"x":378,"y":383,"deltaY":-34},{"type":"mousewheel","time":4052,"x":378,"y":383,"deltaY":-35},{"type":"mousewheel","time":4072,"x":378,"y":383,"deltaY":-33},{"type":"mousewheel","time":4091,"x":378,"y":383,"deltaY":-36},{"type":"mousewheel","time":4116,"x":378,"y":383,"deltaY":-67},{"type":"mousewheel","time":4135,"x":378,"y":383,"deltaY":-29},{"type":"mousewheel","time":4155,"x":378,"y":383,"deltaY":-26},{"type":"mousewheel","time":4175,"x":378,"y":383,"deltaY":-24},{"type":"mousewheel","time":4196,"x":378,"y":383,"deltaY":-42},{"type":"mousewheel","time":4218,"x":378,"y":383,"deltaY":-19},{"type":"mousewheel","time":4238,"x":378,"y":383,"deltaY":-17},{"type":"mousewheel","time":4257,"x":378,"y":383,"deltaY":-16},{"type":"mousewheel","time":4278,"x":378,"y":383,"deltaY":-14},{"type":"mousewheel","time":4297,"x":378,"y":383,"deltaY":-25},{"type":"mousewheel","time":4317,"x":378,"y":383,"deltaY":-11},{"type":"mousewheel","time":4336,"x":378,"y":383,"deltaY":-10},{"type":"mousewheel","time":4355,"x":378,"y":383,"deltaY":-9},{"type":"mousewheel","time":4375,"x":378,"y":383,"deltaY":-8},{"type":"mousewheel","time":4395,"x":378,"y":383,"deltaY":-15},{"type":"mousewheel","time":4419,"x":378,"y":383,"deltaY":-7},{"type":"mousewheel","time":4438,"x":378,"y":383,"deltaY":-6},{"type":"mousewheel","time":4458,"x":378,"y":383,"deltaY":-5},{"type":"mousewheel","time":4477,"x":378,"y":383,"deltaY":-10},{"type":"mousewheel","time":4496,"x":378,"y":383,"deltaY":-5},{"type":"mousewheel","time":4515,"x":378,"y":383,"deltaY":-4},{"type":"mousewheel","time":4535,"x":378,"y":383,"deltaY":-4},{"type":"mousewheel","time":4562,"x":378,"y":383,"deltaY":-4},{"type":"mousewheel","time":4584,"x":378,"y":383,"deltaY":-6},{"type":"mousewheel","time":4607,"x":378,"y":383,"deltaY":-3},{"type":"mousewheel","time":4680,"x":378,"y":383,"deltaY":1},{"type":"mousewheel","time":4702,"x":378,"y":383,"deltaY":2},{"type":"mousewheel","time":4725,"x":378,"y":383,"deltaY":3},{"type":"mousewheel","time":4748,"x":378,"y":383,"deltaY":9},{"type":"mousewheel","time":4778,"x":378,"y":383,"deltaY":7},{"type":"mousewheel","time":4799,"x":378,"y":383,"deltaY":13},{"type":"mousewheel","time":4821,"x":378,"y":383,"deltaY":5},{"type":"mousewheel","time":4843,"x":378,"y":383,"deltaY":5},{"type":"mousewheel","time":4868,"x":378,"y":383,"deltaY":7},{"type":"mousewheel","time":4890,"x":378,"y":383,"deltaY":3},{"type":"mousewheel","time":4915,"x":378,"y":383,"deltaY":3},{"type":"mousewheel","time":4939,"x":378,"y":383,"deltaY":7},{"type":"mousewheel","time":4971,"x":378,"y":383,"deltaY":5},{"type":"mousewheel","time":4993,"x":378,"y":383,"deltaY":10},{"type":"mousewheel","time":5015,"x":378,"y":383,"deltaY":6},{"type":"mousewheel","time":5044,"x":378,"y":383,"deltaY":1},{"type":"mousewheel","time":5069,"x":378,"y":383,"deltaY":2},{"type":"mousewheel","time":5105,"x":378,"y":383,"deltaY":-1},{"type":"mousewheel","time":5129,"x":378,"y":383,"deltaY":-1},{"type":"mousewheel","time":5164,"x":378,"y":383,"deltaY":-1},{"type":"mousewheel","time":5193,"x":378,"y":383,"deltaY":-2},{"type":"mousewheel","time":5223,"x":378,"y":383,"deltaY":-1},{"type":"mousemove","time":5436,"x":385,"y":392},{"type":"mousemove","time":5645,"x":479,"y":512},{"type":"mousemove","time":5858,"x":489,"y":511},{"type":"mousemove","time":6180,"x":491,"y":513},{"type":"mousemove","time":6396,"x":519,"y":536},{"type":"mousewheel","time":6541,"x":519,"y":536,"deltaY":-1},{"type":"mousewheel","time":6574,"x":519,"y":536,"deltaY":-4},{"type":"mousewheel","time":6615,"x":519,"y":536,"deltaY":-7},{"type":"mousewheel","time":6663,"x":519,"y":536,"deltaY":-19},{"type":"mousewheel","time":6708,"x":519,"y":536,"deltaY":-13},{"type":"mousewheel","time":6754,"x":519,"y":536,"deltaY":-11},{"type":"mousewheel","time":6802,"x":519,"y":536,"deltaY":-19},{"type":"mousewheel","time":6843,"x":519,"y":536,"deltaY":-23},{"type":"mousewheel","time":6880,"x":519,"y":536,"deltaY":-10},{"type":"mousewheel","time":6917,"x":519,"y":536,"deltaY":-41},{"type":"mousewheel","time":6957,"x":519,"y":536,"deltaY":-30},{"type":"mousewheel","time":6984,"x":519,"y":536,"deltaY":-17},{"type":"mousewheel","time":7008,"x":519,"y":536,"deltaY":-15},{"type":"mousewheel","time":7048,"x":519,"y":536,"deltaY":-6},{"type":"mousewheel","time":7096,"x":519,"y":536,"deltaY":-6},{"type":"mousewheel","time":7188,"x":519,"y":536,"deltaY":-1},{"type":"mousewheel","time":7218,"x":519,"y":536,"deltaY":-2},{"type":"mousewheel","time":7249,"x":519,"y":536,"deltaY":-3},{"type":"mousewheel","time":7281,"x":519,"y":536,"deltaY":-6},{"type":"mousewheel","time":7318,"x":519,"y":536,"deltaY":-4},{"type":"mousewheel","time":7351,"x":519,"y":536,"deltaY":-4},{"type":"mousewheel","time":7381,"x":519,"y":536,"deltaY":-6},{"type":"mousewheel","time":7719,"x":519,"y":536,"deltaY":1},{"type":"mousewheel","time":7740,"x":519,"y":536,"deltaY":2},{"type":"mousewheel","time":7769,"x":519,"y":536,"deltaY":5},{"type":"mousewheel","time":7797,"x":519,"y":536,"deltaY":9},{"type":"mousewheel","time":7828,"x":519,"y":536,"deltaY":27},{"type":"mousewheel","time":7860,"x":519,"y":536,"deltaY":32},{"type":"mousewheel","time":7886,"x":519,"y":536,"deltaY":26},{"type":"mousewheel","time":7917,"x":519,"y":536,"deltaY":19},{"type":"mousewheel","time":7952,"x":519,"y":536,"deltaY":42},{"type":"mousewheel","time":7979,"x":519,"y":536,"deltaY":42},{"type":"mousewheel","time":8011,"x":519,"y":536,"deltaY":43},{"type":"mousewheel","time":8037,"x":519,"y":536,"deltaY":42},{"type":"mousewheel","time":8058,"x":519,"y":536,"deltaY":19},{"type":"mousewheel","time":8087,"x":519,"y":536,"deltaY":17},{"type":"mousewheel","time":8116,"x":519,"y":536,"deltaY":30},{"type":"mousewheel","time":8149,"x":519,"y":536,"deltaY":25},{"type":"mousewheel","time":8184,"x":519,"y":536,"deltaY":21},{"type":"mousewheel","time":8218,"x":519,"y":536,"deltaY":17},{"type":"mousewheel","time":8245,"x":519,"y":536,"deltaY":15},{"type":"mousewheel","time":8277,"x":519,"y":536,"deltaY":12},{"type":"mousewheel","time":8306,"x":519,"y":536,"deltaY":10},{"type":"mousewheel","time":8338,"x":519,"y":536,"deltaY":9},{"type":"mousewheel","time":8360,"x":519,"y":536,"deltaY":4},{"type":"mousewheel","time":8388,"x":519,"y":536,"deltaY":7},{"type":"mousewheel","time":8421,"x":519,"y":536,"deltaY":3},{"type":"mousedown","time":8943,"x":519,"y":536},{"type":"mouseup","time":8953,"x":519,"y":536},{"time":8954,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":9082,"x":522,"y":535},{"type":"mousemove","time":9290,"x":540,"y":519},{"type":"mousedown","time":9473,"x":540,"y":519},{"type":"mousemove","time":9483,"x":540,"y":519},{"type":"mousemove","time":9683,"x":464,"y":545},{"type":"mousemove","time":9891,"x":448,"y":554},{"type":"mousemove","time":10107,"x":504,"y":537},{"type":"mouseup","time":10257,"x":504,"y":537},{"time":10258,"delay":400,"type":"screenshot-auto"},{"type":"mousewheel","time":10796,"x":504,"y":537,"deltaY":-1},{"type":"mousewheel","time":10815,"x":504,"y":537,"deltaY":-5},{"type":"mousewheel","time":10836,"x":504,"y":537,"deltaY":-7},{"type":"mousewheel","time":10857,"x":504,"y":537,"deltaY":-7},{"type":"mousewheel","time":10879,"x":504,"y":537,"deltaY":-11},{"type":"mousewheel","time":10900,"x":504,"y":537,"deltaY":-22},{"type":"mousewheel","time":10924,"x":504,"y":537,"deltaY":-10},{"type":"mousewheel","time":10946,"x":504,"y":537,"deltaY":-7},{"type":"mousewheel","time":10969,"x":504,"y":537,"deltaY":-9},{"type":"mousewheel","time":10998,"x":504,"y":537,"deltaY":-3},{"type":"mousewheel","time":11022,"x":504,"y":537,"deltaY":-6},{"type":"mousewheel","time":11042,"x":504,"y":537,"deltaY":-18},{"type":"mousewheel","time":11065,"x":504,"y":537,"deltaY":-6},{"type":"mousewheel","time":11086,"x":504,"y":537,"deltaY":-6},{"type":"mousewheel","time":11109,"x":504,"y":537,"deltaY":-12},{"type":"mousewheel","time":11131,"x":504,"y":537,"deltaY":-5},{"type":"mousewheel","time":11151,"x":504,"y":537,"deltaY":-5},{"type":"mousewheel","time":11172,"x":504,"y":537,"deltaY":-5},{"type":"mousewheel","time":11196,"x":504,"y":537,"deltaY":-8},{"type":"mousewheel","time":11217,"x":504,"y":537,"deltaY":-4},{"type":"mousewheel","time":11241,"x":504,"y":537,"deltaY":-3},{"type":"mousewheel","time":11270,"x":504,"y":537,"deltaY":-6},{"type":"mousedown","time":11592,"x":504,"y":537},{"type":"mousemove","time":11602,"x":505,"y":537},{"type":"mousemove","time":11809,"x":538,"y":512},{"type":"mousemove","time":12012,"x":575,"y":486},{"type":"mousemove","time":12212,"x":566,"y":497},{"type":"mousemove","time":12421,"x":556,"y":507},{"type":"mouseup","time":12542,"x":556,"y":507},{"time":12543,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":12553,"x":538,"y":524},{"type":"mousemove","time":12756,"x":491,"y":561},{"type":"mousedown","time":12957,"x":491,"y":561},{"type":"mousemove","time":12967,"x":492,"y":561},{"type":"mousemove","time":13175,"x":546,"y":544},{"type":"mousemove","time":13379,"x":512,"y":559},{"type":"mousemove","time":13589,"x":484,"y":572},{"type":"mousemove","time":13812,"x":484,"y":572},{"type":"mousemove","time":14012,"x":493,"y":557},{"type":"mousemove","time":14212,"x":524,"y":542},{"type":"mousemove","time":14422,"x":490,"y":564},{"type":"mouseup","time":14623,"x":490,"y":564},{"time":14624,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":14632,"x":496,"y":560},{"type":"mousemove","time":14839,"x":512,"y":550},{"type":"mousedown","time":15073,"x":512,"y":550},{"type":"mousemove","time":15084,"x":512,"y":550},{"type":"mousemove","time":15287,"x":495,"y":553},{"type":"mouseup","time":15424,"x":495,"y":553},{"time":15425,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":15435,"x":467,"y":552},{"type":"mousemove","time":15643,"x":322,"y":551},{"type":"mousedown","time":15824,"x":315,"y":553},{"type":"mousemove","time":15873,"x":315,"y":553},{"type":"mouseup","time":15922,"x":315,"y":553},{"time":15923,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":16229,"x":315,"y":554},{"type":"mousemove","time":16440,"x":319,"y":557},{"type":"mousedown","time":16639,"x":319,"y":557},{"type":"mousemove","time":16656,"x":319,"y":557},{"type":"mouseup","time":16739,"x":319,"y":557},{"time":16740,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":16946,"x":321,"y":557},{"type":"mousemove","time":17158,"x":520,"y":536},{"type":"mousemove","time":17372,"x":523,"y":536},{"type":"mousewheel","time":17481,"x":523,"y":536,"deltaY":-1},{"type":"mousewheel","time":17501,"x":523,"y":536,"deltaY":-3},{"type":"mousewheel","time":17521,"x":523,"y":536,"deltaY":-8},{"type":"mousewheel","time":17542,"x":523,"y":536,"deltaY":-10},{"type":"mousewheel","time":17563,"x":523,"y":536,"deltaY":-20},{"type":"mousewheel","time":17584,"x":523,"y":536,"deltaY":-7},{"type":"mousewheel","time":17604,"x":523,"y":536,"deltaY":-6},{"type":"mousewheel","time":17625,"x":523,"y":536,"deltaY":-3},{"type":"mousewheel","time":17649,"x":523,"y":536,"deltaY":-3},{"type":"mousewheel","time":17670,"x":523,"y":536,"deltaY":-1},{"type":"mousewheel","time":17692,"x":523,"y":536,"deltaY":-1},{"type":"mousewheel","time":17765,"x":523,"y":536,"deltaY":0},{"type":"mousewheel","time":17786,"x":523,"y":536,"deltaY":-1},{"type":"mousewheel","time":17807,"x":523,"y":536,"deltaY":-1},{"type":"mousewheel","time":17827,"x":523,"y":536,"deltaY":-1},{"type":"mousewheel","time":17848,"x":523,"y":536,"deltaY":-2},{"type":"mousewheel","time":17869,"x":523,"y":536,"deltaY":-1},{"type":"mousewheel","time":17895,"x":523,"y":536,"deltaY":-1},{"type":"mousewheel","time":17919,"x":523,"y":536,"deltaY":4},{"type":"mousewheel","time":17940,"x":523,"y":536,"deltaY":4},{"type":"mousewheel","time":17962,"x":523,"y":536,"deltaY":5},{"type":"mousewheel","time":17984,"x":523,"y":536,"deltaY":12},{"type":"mousewheel","time":18007,"x":523,"y":536,"deltaY":6},{"type":"mousewheel","time":18028,"x":523,"y":536,"deltaY":5},{"type":"mousewheel","time":18049,"x":523,"y":536,"deltaY":11},{"type":"mousewheel","time":18072,"x":523,"y":536,"deltaY":5},{"type":"mousewheel","time":18097,"x":523,"y":536,"deltaY":9},{"type":"mousewheel","time":18120,"x":523,"y":536,"deltaY":4},{"type":"mousewheel","time":18141,"x":523,"y":536,"deltaY":4},{"type":"mousewheel","time":18162,"x":523,"y":536,"deltaY":8},{"type":"mousewheel","time":18184,"x":523,"y":536,"deltaY":5},{"type":"mousewheel","time":18205,"x":523,"y":536,"deltaY":6},{"type":"mousewheel","time":18227,"x":523,"y":536,"deltaY":6},{"type":"mousewheel","time":18250,"x":523,"y":536,"deltaY":14},{"type":"mousewheel","time":18272,"x":523,"y":536,"deltaY":7},{"type":"mousewheel","time":18295,"x":523,"y":536,"deltaY":8},{"type":"mousewheel","time":18316,"x":523,"y":536,"deltaY":14},{"type":"mousewheel","time":18345,"x":523,"y":536,"deltaY":7},{"type":"mousewheel","time":18366,"x":523,"y":536,"deltaY":15},{"type":"mousewheel","time":18387,"x":523,"y":536,"deltaY":7},{"type":"mousewheel","time":18409,"x":523,"y":536,"deltaY":6},{"type":"mousewheel","time":18431,"x":523,"y":536,"deltaY":11},{"type":"mousewheel","time":18454,"x":523,"y":536,"deltaY":5},{"type":"mousewheel","time":18476,"x":523,"y":536,"deltaY":5},{"type":"mousewheel","time":18500,"x":523,"y":536,"deltaY":8},{"type":"mousewheel","time":18524,"x":523,"y":536,"deltaY":4},{"type":"mousewheel","time":18549,"x":523,"y":536,"deltaY":7},{"type":"mousewheel","time":18571,"x":523,"y":536,"deltaY":3},{"type":"mousewheel","time":18593,"x":523,"y":536,"deltaY":3},{"type":"mousedown","time":18724,"x":523,"y":536},{"type":"mouseup","time":18822,"x":523,"y":536},{"time":18823,"delay":400,"type":"screenshot-auto"},{"type":"mousedown","time":19473,"x":523,"y":536},{"type":"mousemove","time":19483,"x":523,"y":536},{"type":"mousemove","time":19691,"x":510,"y":567},{"type":"mousemove","time":19908,"x":506,"y":578},{"type":"mouseup","time":20057,"x":506,"y":578},{"time":20058,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":20113,"x":522,"y":535},{"type":"mousemove","time":20323,"x":531,"y":507},{"type":"mousedown","time":20441,"x":531,"y":507},{"type":"mousemove","time":20452,"x":530,"y":509},{"type":"mousemove","time":20659,"x":506,"y":542},{"type":"mousemove","time":20874,"x":506,"y":544},{"type":"mouseup","time":20924,"x":506,"y":544},{"time":20925,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":21141,"x":334,"y":478},{"type":"mousedown","time":21224,"x":334,"y":478},{"type":"mousemove","time":21235,"x":331,"y":475},{"type":"mousemove","time":21445,"x":339,"y":448},{"type":"mousemove","time":21659,"x":340,"y":448},{"type":"mouseup","time":21742,"x":340,"y":448},{"time":21743,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":21754,"x":278,"y":365},{"type":"mousemove","time":21957,"x":109,"y":179},{"type":"mousemove","time":22164,"x":126,"y":173},{"type":"mousemove","time":22375,"x":130,"y":168},{"type":"valuechange","selector":"#main0>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select>select.test-inputs-select-select","value":"1","time":23369,"target":"select"},{"time":23370,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":23451,"x":128,"y":158},{"type":"mousemove","time":23661,"x":128,"y":158},{"type":"valuechange","selector":"#main0>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select>select.test-inputs-select-select","value":"0","time":24745,"target":"select"},{"time":24746,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":24766,"x":211,"y":148},{"type":"mousemove","time":24974,"x":232,"y":163},{"type":"valuechange","selector":"#main0>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(2)>select.test-inputs-select-select","value":"1","time":26203,"target":"select"},{"time":26204,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":26233,"x":204,"y":210},{"type":"mousemove","time":26441,"x":148,"y":277},{"type":"mousedown","time":26512,"x":148,"y":277},{"type":"mousemove","time":26525,"x":156,"y":275},{"type":"mousemove","time":26725,"x":241,"y":287},{"type":"mousemove","time":26939,"x":193,"y":303},{"type":"mouseup","time":27041,"x":193,"y":303},{"time":27042,"delay":400,"type":"screenshot-auto"},{"type":"mousedown","time":27721,"x":523,"y":519},{"type":"mousemove","time":27731,"x":523,"y":519},{"type":"mousemove","time":27939,"x":487,"y":512},{"type":"mousemove","time":28156,"x":504,"y":513},{"type":"mouseup","time":28207,"x":504,"y":513},{"time":28208,"delay":400,"type":"screenshot-auto"},{"type":"valuechange","selector":"#main0>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(2)>select.test-inputs-select-select","value":"2","time":30247,"target":"select"},{"time":30248,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":30280,"x":226,"y":267},{"type":"mousemove","time":30488,"x":206,"y":371},{"type":"mousedown","time":31856,"x":206,"y":371},{"type":"mousemove","time":31866,"x":206,"y":371},{"type":"mousemove","time":32074,"x":226,"y":361},{"type":"mousemove","time":32290,"x":212,"y":369},{"type":"mouseup","time":32523,"x":212,"y":369},{"time":32524,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":32534,"x":196,"y":388},{"type":"mousemove","time":32739,"x":177,"y":402},{"type":"mousewheel","time":32996,"x":177,"y":402,"deltaY":1},{"type":"mousewheel","time":33018,"x":177,"y":402,"deltaY":4},{"type":"mousewheel","time":33040,"x":177,"y":402,"deltaY":2},{"type":"mousewheel","time":33062,"x":177,"y":402,"deltaY":2},{"type":"mousewheel","time":33085,"x":177,"y":402,"deltaY":2},{"type":"mousewheel","time":33107,"x":177,"y":402,"deltaY":1},{"type":"mousewheel","time":33128,"x":177,"y":402,"deltaY":1},{"type":"mousewheel","time":33330,"x":177,"y":402,"deltaY":-1},{"type":"mousewheel","time":33352,"x":177,"y":402,"deltaY":-1},{"type":"mousewheel","time":33374,"x":177,"y":402,"deltaY":-3},{"type":"mousewheel","time":33397,"x":177,"y":402,"deltaY":-5},{"type":"mousewheel","time":33420,"x":177,"y":402,"deltaY":-2},{"type":"mousewheel","time":33442,"x":177,"y":402,"deltaY":-1},{"type":"mousewheel","time":33464,"x":177,"y":402,"deltaY":-1},{"type":"mousemove","time":33680,"x":179,"y":396},{"type":"mousemove","time":33880,"x":243,"y":213},{"type":"mousemove","time":34090,"x":241,"y":178},{"type":"mousemove","time":34305,"x":247,"y":158},{"type":"mousemove","time":35046,"x":250,"y":158},{"type":"mousemove","time":35246,"x":401,"y":325},{"type":"mousemove","time":35455,"x":455,"y":413},{"type":"mousemove","time":35663,"x":514,"y":512},{"type":"mousemove","time":35872,"x":514,"y":513},{"type":"mousedown","time":36188,"x":514,"y":513},{"type":"mousemove","time":36198,"x":514,"y":513},{"type":"mousemove","time":36406,"x":518,"y":513},{"type":"mousemove","time":36622,"x":514,"y":516},{"type":"mousewheel","time":36889,"x":514,"y":516,"deltaY":1},{"type":"mouseup","time":36899,"x":514,"y":516},{"time":36900,"delay":400,"type":"screenshot-auto"},{"type":"mousewheel","time":36909,"x":514,"y":516,"deltaY":1},{"type":"mousewheel","time":36930,"x":514,"y":516,"deltaY":3},{"type":"mousewheel","time":36954,"x":514,"y":516,"deltaY":4},{"type":"mousewheel","time":36978,"x":514,"y":516,"deltaY":4},{"type":"mousewheel","time":37001,"x":514,"y":516,"deltaY":5},{"type":"mousewheel","time":37026,"x":514,"y":516,"deltaY":1},{"type":"mousewheel","time":37052,"x":514,"y":516,"deltaY":2},{"type":"mousewheel","time":37130,"x":514,"y":516,"deltaY":-1},{"type":"mousewheel","time":37152,"x":514,"y":516,"deltaY":-1},{"type":"mousewheel","time":37176,"x":514,"y":516,"deltaY":-2},{"type":"mousewheel","time":37200,"x":514,"y":516,"deltaY":-4},{"type":"mousewheel","time":37223,"x":514,"y":516,"deltaY":-2},{"type":"mousewheel","time":37248,"x":514,"y":516,"deltaY":-2},{"type":"mousewheel","time":37274,"x":514,"y":516,"deltaY":-3},{"type":"mousewheel","time":37298,"x":514,"y":516,"deltaY":-2},{"type":"mousewheel","time":37323,"x":514,"y":516,"deltaY":-1},{"type":"mousewheel","time":37346,"x":514,"y":516,"deltaY":-1},{"type":"mousewheel","time":37369,"x":514,"y":516,"deltaY":-1},{"type":"mousemove","time":37696,"x":514,"y":514},{"type":"mousemove","time":37906,"x":248,"y":214},{"type":"mousemove","time":38107,"x":254,"y":171},{"type":"mousemove","time":38315,"x":252,"y":168},{"type":"mousemove","time":38524,"x":251,"y":168},{"type":"valuechange","selector":"#main0>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(2)>select.test-inputs-select-select","value":"3","time":39319,"target":"select"},{"time":39320,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":39343,"x":248,"y":208},{"type":"mousemove","time":39543,"x":244,"y":389},{"type":"mousedown","time":40260,"x":244,"y":389},{"type":"mousemove","time":40275,"x":249,"y":386},{"type":"mousemove","time":40476,"x":307,"y":371},{"type":"mousemove","time":40692,"x":259,"y":397},{"type":"mousemove","time":40908,"x":259,"y":397},{"type":"mouseup","time":41008,"x":259,"y":397},{"time":41009,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":41021,"x":275,"y":402},{"type":"mousemove","time":41224,"x":579,"y":534},{"type":"mousemove","time":41440,"x":561,"y":551},{"type":"mousedown","time":41641,"x":561,"y":551},{"type":"mousemove","time":41651,"x":561,"y":551},{"type":"mousemove","time":41859,"x":575,"y":547},{"type":"mousemove","time":42062,"x":564,"y":550},{"type":"mouseup","time":42273,"x":563,"y":550},{"time":42274,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":42284,"x":549,"y":531},{"type":"mousemove","time":42490,"x":328,"y":341},{"type":"mousewheel","time":42779,"x":328,"y":341,"deltaY":1},{"type":"mousewheel","time":42799,"x":328,"y":341,"deltaY":2},{"type":"mousewheel","time":42820,"x":328,"y":341,"deltaY":6},{"type":"mousewheel","time":42841,"x":328,"y":341,"deltaY":2},{"type":"mousewheel","time":42863,"x":328,"y":341,"deltaY":1},{"type":"mousewheel","time":42883,"x":328,"y":341,"deltaY":1},{"type":"mousewheel","time":42904,"x":328,"y":341,"deltaY":2},{"type":"mousemove","time":43597,"x":331,"y":343},{"type":"mousemove","time":43807,"x":494,"y":529},{"type":"mousemove","time":44024,"x":503,"y":538},{"type":"mousewheel","time":44279,"x":503,"y":538,"deltaY":2},{"type":"mousewheel","time":44300,"x":503,"y":538,"deltaY":3},{"type":"mousewheel","time":44321,"x":503,"y":538,"deltaY":7},{"type":"mousewheel","time":44345,"x":503,"y":538,"deltaY":3},{"type":"mousewheel","time":44366,"x":503,"y":537,"deltaY":2},{"type":"mousewheel","time":44389,"x":503,"y":537,"deltaY":3},{"type":"mousewheel","time":44413,"x":503,"y":537,"deltaY":1},{"type":"mousewheel","time":44436,"x":503,"y":537,"deltaY":2},{"type":"mousemove","time":44764,"x":501,"y":533},{"type":"mousemove","time":44978,"x":209,"y":170},{"type":"mousemove","time":45213,"x":210,"y":169},{"type":"mousemove","time":45414,"x":241,"y":162},{"type":"mousemove","time":45623,"x":242,"y":162},{"type":"valuechange","selector":"#main0>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(2)>select.test-inputs-select-select","value":"0","time":46770,"target":"select"},{"time":46771,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":46809,"x":322,"y":130},{"type":"mousemove","time":47009,"x":368,"y":165},{"type":"mousemove","time":47241,"x":368,"y":165},{"type":"valuechange","selector":"#main0>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(3)>select.test-inputs-select-select","value":"1","time":48278,"target":"select"},{"time":48279,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":48315,"x":367,"y":179},{"type":"mousemove","time":48525,"x":373,"y":164},{"type":"mousemove","time":48740,"x":374,"y":162},{"type":"valuechange","selector":"#main0>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(3)>select.test-inputs-select-select","value":"0","time":49951,"target":"select"},{"time":49952,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":50013,"x":409,"y":181},{"type":"mousemove","time":50238,"x":314,"y":300},{"type":"mousemove","time":50442,"x":306,"y":317},{"type":"mousewheel","time":50813,"x":306,"y":317,"deltaY":-1},{"type":"mousewheel","time":50834,"x":306,"y":317,"deltaY":-2},{"type":"mousewheel","time":50856,"x":306,"y":317,"deltaY":-3},{"type":"mousewheel","time":50877,"x":306,"y":317,"deltaY":-2},{"type":"mousewheel","time":50900,"x":306,"y":317,"deltaY":-2},{"type":"mousewheel","time":50922,"x":306,"y":317,"deltaY":-1},{"type":"mousemove","time":51280,"x":308,"y":317},{"type":"mousemove","time":51480,"x":517,"y":204},{"type":"mousemove","time":51684,"x":537,"y":183},{"type":"mousemove","time":51892,"x":541,"y":156},{"type":"valuechange","selector":"#main0>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(4)>select.test-inputs-select-select","value":"1","time":52905,"target":"select"},{"time":52906,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":52930,"x":534,"y":181},{"type":"mousemove","time":53143,"x":419,"y":266},{"type":"mousemove","time":53349,"x":331,"y":344},{"type":"mousemove","time":53963,"x":333,"y":344},{"type":"mousemove","time":54164,"x":361,"y":439},{"type":"mousemove","time":54375,"x":341,"y":506},{"type":"mousemove","time":54913,"x":341,"y":506},{"type":"mousemove","time":55114,"x":327,"y":522},{"type":"mousemove","time":55326,"x":310,"y":543},{"type":"mousemove","time":55526,"x":310,"y":545},{"type":"mousemove","time":55735,"x":316,"y":547},{"type":"mousemove","time":55935,"x":341,"y":504},{"type":"mousedown","time":62623,"x":341,"y":504},{"type":"mousemove","time":62633,"x":341,"y":504},{"type":"mousemove","time":62840,"x":321,"y":500},{"type":"mousemove","time":63046,"x":311,"y":498},{"type":"mousemove","time":63256,"x":309,"y":498},{"type":"mouseup","time":63275,"x":309,"y":498},{"time":63276,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":63286,"x":327,"y":437},{"type":"mousemove","time":63491,"x":328,"y":338},{"type":"mousemove","time":63706,"x":321,"y":347},{"type":"mousedown","time":63856,"x":321,"y":347},{"type":"mousemove","time":63866,"x":321,"y":347},{"type":"mousemove","time":64073,"x":274,"y":371},{"type":"mouseup","time":64306,"x":274,"y":371},{"time":64307,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":64796,"x":272,"y":376},{"type":"mousemove","time":65007,"x":255,"y":410},{"type":"mousewheel","time":65230,"x":255,"y":410,"deltaY":1},{"type":"mousewheel","time":65251,"x":255,"y":410,"deltaY":4},{"type":"mousewheel","time":65273,"x":255,"y":410,"deltaY":6},{"type":"mousewheel","time":65297,"x":255,"y":410,"deltaY":11},{"type":"mousewheel","time":65320,"x":255,"y":410,"deltaY":4},{"type":"mousewheel","time":65342,"x":255,"y":410,"deltaY":3},{"type":"mousewheel","time":65365,"x":255,"y":410,"deltaY":3},{"type":"mousemove","time":65680,"x":255,"y":411},{"type":"mousemove","time":65880,"x":249,"y":435},{"type":"mousewheel","time":66030,"x":248,"y":438,"deltaY":1},{"type":"mousewheel","time":66054,"x":248,"y":438,"deltaY":1},{"type":"mousewheel","time":66079,"x":248,"y":438,"deltaY":2},{"type":"mousewheel","time":66106,"x":248,"y":438,"deltaY":4},{"type":"mousewheel","time":66131,"x":248,"y":438,"deltaY":3},{"type":"mousewheel","time":66156,"x":248,"y":438,"deltaY":6},{"type":"mousewheel","time":66186,"x":248,"y":438,"deltaY":4},{"type":"mousewheel","time":66210,"x":248,"y":438,"deltaY":2},{"type":"mousemove","time":66235,"x":248,"y":438},{"type":"mousewheel","time":66247,"x":248,"y":438,"deltaY":2},{"type":"mousewheel","time":66272,"x":248,"y":438,"deltaY":1},{"type":"mousemove","time":66446,"x":248,"y":437},{"type":"mousemove","time":66647,"x":300,"y":346},{"type":"mousedown","time":66907,"x":300,"y":346},{"type":"mousemove","time":66919,"x":299,"y":349},{"type":"mousemove","time":67127,"x":261,"y":491},{"type":"mouseup","time":67340,"x":261,"y":491},{"time":67341,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":67352,"x":289,"y":485},{"type":"mousemove","time":67562,"x":528,"y":518},{"type":"mousemove","time":67779,"x":540,"y":523},{"type":"mousewheel","time":67913,"x":540,"y":523,"deltaY":-1},{"type":"mousewheel","time":67937,"x":540,"y":523,"deltaY":-4},{"type":"mousewheel","time":67963,"x":540,"y":523,"deltaY":-5},{"type":"mousewheel","time":67989,"x":540,"y":523,"deltaY":-16},{"type":"mousewheel","time":68015,"x":540,"y":523,"deltaY":-16},{"type":"mousewheel","time":68041,"x":540,"y":523,"deltaY":-6},{"type":"mousewheel","time":68073,"x":540,"y":523,"deltaY":-7},{"type":"mousewheel","time":68101,"x":540,"y":523,"deltaY":-4},{"type":"mousewheel","time":68127,"x":540,"y":523,"deltaY":-1},{"type":"mousewheel","time":68151,"x":540,"y":523,"deltaY":-3},{"type":"mousewheel","time":68176,"x":540,"y":523,"deltaY":-2},{"type":"mousewheel","time":68201,"x":540,"y":523,"deltaY":-6},{"type":"mousewheel","time":68226,"x":540,"y":523,"deltaY":-4},{"type":"mousewheel","time":68252,"x":540,"y":523,"deltaY":-6},{"type":"mousewheel","time":68276,"x":540,"y":523,"deltaY":-2},{"type":"mousewheel","time":68305,"x":540,"y":523,"deltaY":-2},{"type":"mousewheel","time":68530,"x":540,"y":523,"deltaY":-1},{"type":"mousewheel","time":68680,"x":540,"y":523,"deltaY":1},{"type":"mousewheel","time":68706,"x":540,"y":523,"deltaY":1},{"type":"mousewheel","time":68731,"x":540,"y":523,"deltaY":1},{"type":"mousewheel","time":68757,"x":540,"y":523,"deltaY":4},{"type":"mousewheel","time":68782,"x":540,"y":523,"deltaY":4},{"type":"mousewheel","time":68808,"x":540,"y":523,"deltaY":1},{"type":"mousewheel","time":68836,"x":540,"y":523,"deltaY":1},{"type":"mousemove","time":69030,"x":534,"y":523},{"type":"mousemove","time":69232,"x":360,"y":578},{"type":"mousemove","time":69442,"x":348,"y":569},{"type":"mousedown","time":69626,"x":332,"y":560},{"type":"mousemove","time":69675,"x":332,"y":560},{"type":"mouseup","time":69759,"x":332,"y":560},{"time":69760,"delay":400,"type":"screenshot-auto"},{"type":"mousedown","time":70210,"x":332,"y":560},{"type":"mouseup","time":70275,"x":332,"y":560},{"time":70276,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":70432,"x":341,"y":558},{"type":"mousemove","time":70632,"x":518,"y":538},{"type":"mousemove","time":70845,"x":553,"y":528},{"type":"mousewheel","time":71497,"x":553,"y":528,"deltaY":-1},{"type":"mousewheel","time":71522,"x":553,"y":528,"deltaY":-4},{"type":"mousewheel","time":71547,"x":553,"y":528,"deltaY":-3},{"type":"mousewheel","time":71572,"x":553,"y":528,"deltaY":-6},{"type":"mousewheel","time":71598,"x":553,"y":528,"deltaY":-2},{"type":"mousewheel","time":71622,"x":553,"y":528,"deltaY":-2},{"type":"mousewheel","time":71648,"x":553,"y":528,"deltaY":0},{"type":"mousewheel","time":71676,"x":553,"y":528,"deltaY":-1},{"type":"mousewheel","time":71730,"x":553,"y":528,"deltaY":-1},{"type":"mousewheel","time":71754,"x":553,"y":528,"deltaY":-1},{"type":"mousewheel","time":71779,"x":553,"y":528,"deltaY":-2},{"type":"mousewheel","time":71804,"x":553,"y":528,"deltaY":-4},{"type":"mousewheel","time":71830,"x":553,"y":528,"deltaY":-2},{"type":"mousewheel","time":71856,"x":553,"y":528,"deltaY":-2},{"type":"mousewheel","time":71887,"x":553,"y":528,"deltaY":0},{"type":"mousewheel","time":71930,"x":553,"y":528,"deltaY":1},{"type":"mousewheel","time":71955,"x":553,"y":528,"deltaY":2},{"type":"mousewheel","time":71982,"x":553,"y":528,"deltaY":2},{"type":"mousewheel","time":72010,"x":553,"y":528,"deltaY":3},{"type":"mousewheel","time":72038,"x":553,"y":528,"deltaY":2},{"type":"mousewheel","time":72065,"x":553,"y":528,"deltaY":1},{"type":"mousewheel","time":72123,"x":553,"y":528,"deltaY":1},{"type":"mousewheel","time":72147,"x":553,"y":528,"deltaY":1},{"type":"mousewheel","time":72189,"x":553,"y":528,"deltaY":1},{"type":"mousewheel","time":72221,"x":553,"y":528,"deltaY":1},{"type":"mousewheel","time":72247,"x":553,"y":528,"deltaY":1},{"type":"mousewheel","time":72273,"x":553,"y":528,"deltaY":4},{"type":"mousewheel","time":72300,"x":553,"y":528,"deltaY":2},{"type":"mousewheel","time":72328,"x":553,"y":528,"deltaY":1},{"type":"mousewheel","time":72354,"x":553,"y":528,"deltaY":1},{"type":"mousemove","time":72630,"x":552,"y":528},{"type":"mousemove","time":72845,"x":308,"y":416},{"type":"mousedown","time":73014,"x":308,"y":416},{"type":"mousemove","time":73029,"x":311,"y":415},{"type":"mousemove","time":73247,"x":407,"y":393},{"type":"mousemove","time":73453,"x":364,"y":399},{"type":"mousemove","time":73661,"x":360,"y":399},{"type":"mouseup","time":73859,"x":360,"y":399},{"time":73860,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":73873,"x":372,"y":397},{"type":"mousemove","time":74079,"x":447,"y":368},{"type":"mousedown","time":74096,"x":448,"y":368},{"type":"mouseup","time":74171,"x":448,"y":368},{"time":74172,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":74296,"x":448,"y":368}],"scrollY":0,"scrollX":0,"timestamp":1750343219896},{"name":"Action 2","ops":[{"type":"mousewheel","time":777,"x":315,"y":326,"deltaY":-1},{"type":"mousewheel","time":806,"x":315,"y":326,"deltaY":-5},{"type":"mousewheel","time":835,"x":315,"y":326,"deltaY":-11},{"type":"mousewheel","time":869,"x":315,"y":326,"deltaY":-10},{"type":"mousewheel","time":897,"x":315,"y":326,"deltaY":-9},{"type":"mousewheel","time":926,"x":315,"y":326,"deltaY":-3},{"type":"mousewheel","time":954,"x":315,"y":326,"deltaY":-4},{"type":"mousewheel","time":984,"x":315,"y":326,"deltaY":-2},{"type":"mousewheel","time":1011,"x":315,"y":326,"deltaY":0},{"type":"mousewheel","time":1040,"x":315,"y":326,"deltaY":0},{"type":"mousewheel","time":1160,"x":315,"y":326,"deltaY":-1},{"type":"mousewheel","time":1187,"x":315,"y":326,"deltaY":-1},{"type":"mousewheel","time":1213,"x":315,"y":326,"deltaY":-4},{"type":"mousewheel","time":1239,"x":315,"y":326,"deltaY":-2},{"type":"mousewheel","time":1266,"x":315,"y":326,"deltaY":-4},{"type":"mousewheel","time":1291,"x":315,"y":326,"deltaY":-2},{"type":"mousewheel","time":1317,"x":315,"y":326,"deltaY":-4},{"type":"mousewheel","time":1346,"x":315,"y":326,"deltaY":-2},{"type":"mousewheel","time":1372,"x":315,"y":326,"deltaY":-2},{"type":"mousewheel","time":1398,"x":315,"y":326,"deltaY":-2},{"type":"mousewheel","time":1527,"x":315,"y":326,"deltaY":1},{"type":"mousewheel","time":1556,"x":315,"y":326,"deltaY":1},{"type":"mousewheel","time":1590,"x":315,"y":326,"deltaY":6},{"type":"mousewheel","time":1616,"x":315,"y":326,"deltaY":6},{"type":"mousewheel","time":1641,"x":315,"y":326,"deltaY":2},{"type":"mousewheel","time":1667,"x":315,"y":326,"deltaY":4},{"type":"mousewheel","time":1692,"x":315,"y":326,"deltaY":3},{"type":"mousewheel","time":1717,"x":315,"y":326,"deltaY":6},{"type":"mousewheel","time":1743,"x":315,"y":326,"deltaY":2},{"type":"mousewheel","time":1767,"x":315,"y":326,"deltaY":2},{"type":"mousewheel","time":1792,"x":315,"y":326,"deltaY":1},{"type":"mousewheel","time":1817,"x":315,"y":326,"deltaY":2},{"type":"mousewheel","time":1843,"x":315,"y":326,"deltaY":1},{"type":"mousewheel","time":1870,"x":315,"y":326,"deltaY":4},{"type":"mousewheel","time":1895,"x":315,"y":326,"deltaY":4},{"type":"mousewheel","time":1920,"x":315,"y":326,"deltaY":1},{"type":"mousewheel","time":1944,"x":315,"y":326,"deltaY":2},{"type":"mousewheel","time":1969,"x":315,"y":326,"deltaY":1},{"type":"mousewheel","time":1993,"x":315,"y":326,"deltaY":1},{"type":"mousewheel","time":2018,"x":315,"y":326,"deltaY":3},{"type":"mousewheel","time":2042,"x":315,"y":326,"deltaY":2},{"type":"mousewheel","time":2072,"x":315,"y":326,"deltaY":4},{"type":"mousewheel","time":2097,"x":315,"y":326,"deltaY":2},{"type":"mousewheel","time":2123,"x":315,"y":326,"deltaY":1},{"type":"mousewheel","time":2149,"x":315,"y":326,"deltaY":3},{"type":"mousewheel","time":2174,"x":315,"y":326,"deltaY":2},{"type":"mousewheel","time":2199,"x":315,"y":326,"deltaY":6},{"type":"mousewheel","time":2224,"x":315,"y":326,"deltaY":2},{"type":"mousewheel","time":2248,"x":315,"y":326,"deltaY":2},{"type":"mousewheel","time":2272,"x":315,"y":326,"deltaY":1},{"type":"mousewheel","time":2360,"x":315,"y":326,"deltaY":-1},{"type":"mousewheel","time":2383,"x":315,"y":326,"deltaY":-1},{"type":"mousewheel","time":2419,"x":315,"y":326,"deltaY":1},{"type":"mousewheel","time":2442,"x":315,"y":326,"deltaY":2},{"type":"mousewheel","time":2465,"x":315,"y":326,"deltaY":2},{"type":"mousewheel","time":2488,"x":315,"y":326,"deltaY":2},{"type":"mousewheel","time":2516,"x":315,"y":326,"deltaY":2},{"type":"mousewheel","time":2553,"x":315,"y":326,"deltaY":-1},{"type":"mousewheel","time":2577,"x":315,"y":326,"deltaY":-1},{"type":"mousewheel","time":2604,"x":315,"y":326,"deltaY":-1},{"type":"mousewheel","time":2631,"x":315,"y":326,"deltaY":-4},{"type":"mousewheel","time":2657,"x":315,"y":326,"deltaY":-2},{"type":"mousewheel","time":2684,"x":315,"y":326,"deltaY":-3},{"type":"mousewheel","time":2712,"x":315,"y":326,"deltaY":-1},{"type":"mousewheel","time":2746,"x":315,"y":326,"deltaY":-1},{"type":"mousewheel","time":2774,"x":315,"y":326,"deltaY":0},{"type":"mousewheel","time":2802,"x":315,"y":326,"deltaY":0},{"type":"mousewheel","time":2833,"x":315,"y":326,"deltaY":-1},{"type":"mousewheel","time":2863,"x":315,"y":326,"deltaY":-1},{"type":"mousewheel","time":2891,"x":315,"y":326,"deltaY":-1},{"type":"mousedown","time":3106,"x":315,"y":326},{"type":"mousemove","time":3119,"x":318,"y":325},{"type":"mousemove","time":3321,"x":446,"y":309},{"type":"mousemove","time":3526,"x":323,"y":322},{"type":"mouseup","time":3738,"x":315,"y":322},{"time":3739,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":3751,"x":352,"y":317},{"type":"mousemove","time":3954,"x":629,"y":338},{"type":"mousemove","time":4160,"x":566,"y":341},{"type":"mousemove","time":4370,"x":556,"y":329},{"type":"mousewheel","time":4527,"x":556,"y":329,"deltaY":1},{"type":"mousewheel","time":4551,"x":556,"y":329,"deltaY":7},{"type":"mousewheel","time":4577,"x":556,"y":329,"deltaY":10},{"type":"mousewheel","time":4604,"x":556,"y":329,"deltaY":27},{"type":"mousewheel","time":4631,"x":556,"y":329,"deltaY":23},{"type":"mousewheel","time":4658,"x":556,"y":329,"deltaY":7},{"type":"mousewheel","time":4691,"x":556,"y":329,"deltaY":9},{"type":"mousewheel","time":4718,"x":556,"y":329,"deltaY":4},{"type":"mousewheel","time":4744,"x":556,"y":329,"deltaY":2},{"type":"mousewheel","time":4771,"x":556,"y":329,"deltaY":6},{"type":"mousewheel","time":4798,"x":556,"y":329,"deltaY":4},{"type":"mousewheel","time":4824,"x":556,"y":329,"deltaY":2},{"type":"mousewheel","time":4850,"x":556,"y":329,"deltaY":4},{"type":"mousewheel","time":4876,"x":556,"y":329,"deltaY":1},{"type":"mousewheel","time":4901,"x":556,"y":329,"deltaY":2},{"type":"mousewheel","time":4993,"x":556,"y":329,"deltaY":-1},{"type":"mousewheel","time":5019,"x":556,"y":329,"deltaY":-1},{"type":"mousewheel","time":5045,"x":556,"y":329,"deltaY":-5},{"type":"mousewheel","time":5070,"x":556,"y":329,"deltaY":-3},{"type":"mousewheel","time":5097,"x":556,"y":329,"deltaY":-8},{"type":"mousewheel","time":5123,"x":556,"y":329,"deltaY":-4},{"type":"mousewheel","time":5150,"x":556,"y":329,"deltaY":-8},{"type":"mousewheel","time":5176,"x":556,"y":329,"deltaY":-3},{"type":"mousewheel","time":5203,"x":556,"y":329,"deltaY":-3},{"type":"mousewheel","time":5228,"x":556,"y":329,"deltaY":-2},{"type":"mousewheel","time":5269,"x":556,"y":329,"deltaY":-1},{"type":"mousewheel","time":5294,"x":556,"y":329,"deltaY":-1},{"type":"mousemove","time":5593,"x":558,"y":329},{"type":"mousemove","time":5806,"x":660,"y":311},{"type":"mousedown","time":6023,"x":676,"y":308},{"type":"mousemove","time":6054,"x":676,"y":308},{"type":"mouseup","time":6122,"x":676,"y":308},{"time":6123,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":7659,"x":670,"y":308},{"type":"mousemove","time":7866,"x":291,"y":417},{"type":"mousemove","time":8078,"x":286,"y":412},{"type":"mousedown","time":8227,"x":296,"y":407},{"type":"mousemove","time":8302,"x":296,"y":407},{"type":"mouseup","time":8356,"x":296,"y":407},{"time":8357,"delay":400,"type":"screenshot-auto"},{"type":"mousedown","time":8609,"x":296,"y":407},{"type":"mouseup","time":8724,"x":296,"y":407},{"time":8725,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":8828,"x":300,"y":407},{"type":"mousemove","time":9040,"x":337,"y":393},{"type":"mousemove","time":9259,"x":310,"y":409},{"type":"mousemove","time":9478,"x":469,"y":349},{"type":"mousemove","time":9691,"x":526,"y":330},{"type":"mousemove","time":9877,"x":526,"y":330}],"scrollY":472,"scrollX":0,"timestamp":1750343303266},{"name":"Action 3","ops":[{"type":"mousemove","time":291,"x":608,"y":281},{"type":"mousemove","time":507,"x":346,"y":292},{"type":"mousemove","time":716,"x":342,"y":294},{"type":"mousewheel","time":919,"x":342,"y":294,"deltaY":-1},{"type":"mousewheel","time":948,"x":342,"y":294,"deltaY":-4},{"type":"mousewheel","time":978,"x":342,"y":294,"deltaY":-15},{"type":"mousewheel","time":1009,"x":342,"y":294,"deltaY":-13},{"type":"mousewheel","time":1044,"x":342,"y":294,"deltaY":-5},{"type":"mousewheel","time":1080,"x":342,"y":294,"deltaY":-9},{"type":"mousewheel","time":1116,"x":342,"y":294,"deltaY":-5},{"type":"mousewheel","time":1158,"x":342,"y":294,"deltaY":-9},{"type":"mousewheel","time":1188,"x":342,"y":294,"deltaY":-15},{"type":"mousewheel","time":1215,"x":342,"y":294,"deltaY":-19},{"type":"mousewheel","time":1251,"x":342,"y":294,"deltaY":-18},{"type":"mousewheel","time":1286,"x":342,"y":294,"deltaY":-11},{"type":"mousewheel","time":1321,"x":342,"y":294,"deltaY":-48},{"type":"mousewheel","time":1357,"x":342,"y":294,"deltaY":-23},{"type":"mousewheel","time":1393,"x":342,"y":294,"deltaY":-23},{"type":"mousewheel","time":1434,"x":342,"y":294,"deltaY":-27},{"type":"mousewheel","time":1467,"x":342,"y":294,"deltaY":-15},{"type":"mousewheel","time":1510,"x":342,"y":294,"deltaY":-3},{"type":"mousewheel","time":1538,"x":342,"y":294,"deltaY":-7},{"type":"mousewheel","time":1567,"x":342,"y":294,"deltaY":-62},{"type":"mousewheel","time":1596,"x":342,"y":294,"deltaY":-131},{"type":"mousewheel","time":1623,"x":342,"y":294,"deltaY":-105},{"type":"mousewheel","time":1650,"x":342,"y":294,"deltaY":-331},{"type":"mousewheel","time":1683,"x":342,"y":294,"deltaY":-204},{"type":"mousewheel","time":1710,"x":342,"y":294,"deltaY":-188},{"type":"mousewheel","time":1739,"x":342,"y":294,"deltaY":-87},{"type":"mousewheel","time":1768,"x":342,"y":294,"deltaY":-159},{"type":"mousewheel","time":1795,"x":342,"y":294,"deltaY":-142},{"type":"mousewheel","time":1822,"x":342,"y":294,"deltaY":-64},{"type":"mousewheel","time":1850,"x":342,"y":294,"deltaY":-115},{"type":"mousewheel","time":1876,"x":342,"y":294,"deltaY":-98},{"type":"mousewheel","time":1925,"x":342,"y":294,"deltaY":-2},{"type":"mousewheel","time":1953,"x":342,"y":294,"deltaY":-1},{"type":"mousewheel","time":1981,"x":342,"y":294,"deltaY":-12},{"type":"mousewheel","time":2011,"x":342,"y":294,"deltaY":-25},{"type":"mousewheel","time":2040,"x":342,"y":294,"deltaY":-12},{"type":"mousewheel","time":2067,"x":342,"y":294,"deltaY":-17},{"type":"mousewheel","time":2094,"x":342,"y":294,"deltaY":-77},{"type":"mousewheel","time":2121,"x":342,"y":294,"deltaY":-18},{"type":"mousewheel","time":2153,"x":342,"y":294,"deltaY":-40},{"type":"mousewheel","time":2179,"x":342,"y":294,"deltaY":-34},{"type":"mousewheel","time":2206,"x":342,"y":294,"deltaY":-14},{"type":"mousewheel","time":2247,"x":342,"y":294,"deltaY":2},{"type":"mousewheel","time":2277,"x":342,"y":294,"deltaY":29},{"type":"mousewheel","time":2308,"x":342,"y":294,"deltaY":67},{"type":"mousewheel","time":2336,"x":342,"y":294,"deltaY":32},{"type":"mousewheel","time":2363,"x":342,"y":294,"deltaY":53},{"type":"mousewheel","time":2398,"x":342,"y":294,"deltaY":187},{"type":"mousewheel","time":2425,"x":342,"y":294,"deltaY":120},{"type":"mousewheel","time":2453,"x":342,"y":294,"deltaY":61},{"type":"mousewheel","time":2487,"x":342,"y":294,"deltaY":113},{"type":"mousewheel","time":2514,"x":342,"y":294,"deltaY":98},{"type":"mousewheel","time":2541,"x":342,"y":294,"deltaY":83},{"type":"mousewheel","time":2566,"x":342,"y":294,"deltaY":37},{"type":"mousewheel","time":2592,"x":342,"y":294,"deltaY":65},{"type":"mousewheel","time":2618,"x":342,"y":294,"deltaY":28},{"type":"mousewheel","time":2644,"x":342,"y":294,"deltaY":26},{"type":"mousewheel","time":2686,"x":342,"y":294,"deltaY":7},{"type":"mousewheel","time":2712,"x":342,"y":294,"deltaY":62},{"type":"mousewheel","time":2741,"x":342,"y":294,"deltaY":132},{"type":"mousewheel","time":2769,"x":342,"y":294,"deltaY":103},{"type":"mousewheel","time":2796,"x":342,"y":294,"deltaY":330},{"type":"mousewheel","time":2824,"x":342,"y":294,"deltaY":103},{"type":"mousewheel","time":2853,"x":342,"y":294,"deltaY":196},{"type":"mousewheel","time":2883,"x":342,"y":294,"deltaY":179},{"type":"mousewheel","time":2918,"x":342,"y":294,"deltaY":82},{"type":"mousewheel","time":2948,"x":342,"y":294,"deltaY":151},{"type":"mousewheel","time":2978,"x":342,"y":294,"deltaY":132},{"type":"mousewheel","time":3008,"x":342,"y":294,"deltaY":115},{"type":"mousewheel","time":3040,"x":342,"y":294,"deltaY":51},{"type":"mousemove","time":3055,"x":334,"y":292},{"type":"mousemove","time":3267,"x":65,"y":296},{"type":"mousemove","time":3481,"x":47,"y":301},{"type":"mousewheel","time":3619,"x":47,"y":301,"deltaY":-1},{"type":"mousewheel","time":3644,"x":47,"y":301,"deltaY":-5},{"type":"mousewheel","time":3669,"x":47,"y":301,"deltaY":-11},{"type":"mousewheel","time":3701,"x":47,"y":301,"deltaY":-71},{"type":"mousewheel","time":3734,"x":47,"y":301,"deltaY":-41},{"type":"mousewheel","time":3764,"x":47,"y":301,"deltaY":-59},{"type":"mousewheel","time":3792,"x":47,"y":301,"deltaY":-274},{"type":"mousewheel","time":3820,"x":47,"y":301,"deltaY":-132},{"type":"mousewheel","time":3847,"x":47,"y":301,"deltaY":-64},{"type":"mousewheel","time":3873,"x":47,"y":301,"deltaY":-114},{"type":"mousewheel","time":3900,"x":47,"y":301,"deltaY":-98},{"type":"mousewheel","time":3932,"x":47,"y":301,"deltaY":-45},{"type":"mousewheel","time":3959,"x":47,"y":301,"deltaY":-78},{"type":"mousewheel","time":3987,"x":47,"y":301,"deltaY":-67},{"type":"mousewheel","time":4021,"x":47,"y":301,"deltaY":-55},{"type":"mousewheel","time":4049,"x":47,"y":301,"deltaY":-24},{"type":"mousewheel","time":4077,"x":47,"y":301,"deltaY":-42},{"type":"mousewheel","time":4105,"x":47,"y":301,"deltaY":-36},{"type":"mousewheel","time":4146,"x":47,"y":301,"deltaY":-1},{"type":"mousewheel","time":4171,"x":47,"y":301,"deltaY":-3},{"type":"mousewheel","time":4197,"x":47,"y":301,"deltaY":-6},{"type":"mousewheel","time":4223,"x":47,"y":301,"deltaY":-9},{"type":"mousewheel","time":4250,"x":47,"y":301,"deltaY":-4},{"type":"mousewheel","time":4282,"x":47,"y":301,"deltaY":-5},{"type":"mousewheel","time":4311,"x":47,"y":301,"deltaY":-4},{"type":"mousewheel","time":4340,"x":47,"y":301,"deltaY":-3},{"type":"mousewheel","time":4367,"x":47,"y":301,"deltaY":-1},{"type":"mousewheel","time":4393,"x":47,"y":301,"deltaY":-1},{"type":"mousewheel","time":4420,"x":47,"y":301,"deltaY":0},{"type":"mousewheel","time":4619,"x":47,"y":301,"deltaY":-1},{"type":"mousewheel","time":4644,"x":47,"y":301,"deltaY":-2},{"type":"mousewheel","time":4670,"x":47,"y":301,"deltaY":-4},{"type":"mousewheel","time":4697,"x":47,"y":301,"deltaY":-7},{"type":"mousewheel","time":4730,"x":47,"y":301,"deltaY":-3},{"type":"mousewheel","time":4759,"x":47,"y":301,"deltaY":5},{"type":"mousewheel","time":4787,"x":47,"y":301,"deltaY":9},{"type":"mousewheel","time":4816,"x":47,"y":301,"deltaY":30},{"type":"mousewheel","time":4850,"x":47,"y":301,"deltaY":19},{"type":"mousewheel","time":4880,"x":47,"y":301,"deltaY":7},{"type":"mousewheel","time":4912,"x":47,"y":301,"deltaY":-1},{"type":"mousewheel","time":4947,"x":47,"y":301,"deltaY":-22},{"type":"mousewheel","time":4986,"x":47,"y":301,"deltaY":-59},{"type":"mousewheel","time":5020,"x":47,"y":301,"deltaY":-64},{"type":"mousewheel","time":5064,"x":47,"y":301,"deltaY":-25},{"type":"mousewheel","time":5097,"x":47,"y":301,"deltaY":-8},{"type":"mousewheel","time":5129,"x":47,"y":301,"deltaY":-18},{"type":"mousewheel","time":5163,"x":47,"y":301,"deltaY":-102},{"type":"mousewheel","time":5198,"x":47,"y":301,"deltaY":-45},{"type":"mousewheel","time":5235,"x":47,"y":301,"deltaY":-39},{"type":"mousewheel","time":5286,"x":47,"y":301,"deltaY":-47},{"type":"mousewheel","time":5339,"x":47,"y":301,"deltaY":-1},{"type":"mousewheel","time":5373,"x":47,"y":301,"deltaY":-18},{"type":"mousewheel","time":5408,"x":47,"y":301,"deltaY":-84},{"type":"mousewheel","time":5442,"x":47,"y":301,"deltaY":-67},{"type":"mousewheel","time":5485,"x":47,"y":301,"deltaY":-49},{"type":"mousewheel","time":5529,"x":47,"y":301,"deltaY":-240},{"type":"mousewheel","time":5573,"x":47,"y":301,"deltaY":-177},{"type":"mousewheel","time":5614,"x":47,"y":301,"deltaY":-153},{"type":"mousewheel","time":5653,"x":47,"y":301,"deltaY":-83},{"type":"mousewheel","time":5694,"x":47,"y":301,"deltaY":-71},{"type":"mousewheel","time":5762,"x":47,"y":301,"deltaY":-2},{"type":"mousewheel","time":5800,"x":47,"y":301,"deltaY":-88},{"type":"mousewheel","time":5838,"x":47,"y":301,"deltaY":-49},{"type":"mousewheel","time":5886,"x":47,"y":301,"deltaY":-39},{"type":"mousewheel","time":5927,"x":47,"y":301,"deltaY":-237},{"type":"mousewheel","time":5964,"x":47,"y":301,"deltaY":-99},{"type":"mousewheel","time":6005,"x":47,"y":301,"deltaY":-86},{"type":"mousewheel","time":6043,"x":47,"y":301,"deltaY":-105},{"type":"mousewheel","time":6083,"x":47,"y":301,"deltaY":-57},{"type":"mousewheel","time":6132,"x":47,"y":301,"deltaY":-46},{"type":"mousewheel","time":6217,"x":47,"y":301,"deltaY":1},{"type":"mousewheel","time":6251,"x":47,"y":301,"deltaY":5},{"type":"mousewheel","time":6295,"x":47,"y":301,"deltaY":42},{"type":"mousewheel","time":6330,"x":47,"y":301,"deltaY":95},{"type":"mousewheel","time":6370,"x":47,"y":301,"deltaY":41},{"type":"mousewheel","time":6403,"x":47,"y":301,"deltaY":147},{"type":"mousewheel","time":6436,"x":47,"y":301,"deltaY":95},{"type":"mousewheel","time":6470,"x":47,"y":301,"deltaY":98},{"type":"mousewheel","time":6510,"x":47,"y":301,"deltaY":86},{"type":"mousewheel","time":6546,"x":47,"y":301,"deltaY":105},{"type":"mousewheel","time":6586,"x":47,"y":301,"deltaY":57},{"type":"mousewheel","time":6620,"x":47,"y":301,"deltaY":46},{"type":"mousewheel","time":6661,"x":47,"y":301,"deltaY":39},{"type":"mousewheel","time":6696,"x":47,"y":301,"deltaY":47},{"type":"mousewheel","time":6733,"x":47,"y":301,"deltaY":25},{"type":"mousewheel","time":6775,"x":47,"y":301,"deltaY":11},{"type":"mousewheel","time":6835,"x":47,"y":301,"deltaY":2},{"type":"mousewheel","time":6871,"x":47,"y":301,"deltaY":66},{"type":"mousewheel","time":6913,"x":47,"y":301,"deltaY":127},{"type":"mousewheel","time":6949,"x":47,"y":301,"deltaY":46},{"type":"mousewheel","time":6985,"x":47,"y":301,"deltaY":73},{"type":"mousewheel","time":7022,"x":47,"y":301,"deltaY":413},{"type":"mousewheel","time":7068,"x":47,"y":301,"deltaY":157},{"type":"mousewheel","time":7105,"x":47,"y":301,"deltaY":200},{"type":"mousewheel","time":7146,"x":47,"y":301,"deltaY":113},{"type":"mousewheel","time":7180,"x":47,"y":301,"deltaY":141},{"type":"mousewheel","time":7215,"x":47,"y":301,"deltaY":77},{"type":"mousewheel","time":7251,"x":47,"y":301,"deltaY":65},{"type":"mousewheel","time":7295,"x":47,"y":301,"deltaY":54},{"type":"mousewheel","time":7333,"x":47,"y":301,"deltaY":66},{"type":"mousewheel","time":7381,"x":47,"y":301,"deltaY":36},{"type":"mousewheel","time":7418,"x":47,"y":301,"deltaY":30},{"type":"mousedown","time":7499,"x":47,"y":301},{"type":"mousemove","time":7513,"x":47,"y":308},{"type":"mousemove","time":7738,"x":40,"y":372},{"type":"mousemove","time":8035,"x":43,"y":366},{"type":"mouseup","time":8252,"x":43,"y":366},{"time":8253,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":8267,"x":50,"y":328},{"type":"mousemove","time":8479,"x":52,"y":290},{"type":"mousewheel","time":8641,"x":52,"y":290,"deltaY":-1},{"type":"mousewheel","time":8674,"x":52,"y":290,"deltaY":-14},{"type":"mousewheel","time":8709,"x":52,"y":290,"deltaY":-60},{"type":"mousewheel","time":8748,"x":52,"y":290,"deltaY":-87},{"type":"mousewheel","time":8784,"x":52,"y":290,"deltaY":-63},{"type":"mousewheel","time":8820,"x":52,"y":290,"deltaY":-291},{"type":"mousewheel","time":8858,"x":52,"y":290,"deltaY":-203},{"type":"mousewheel","time":8891,"x":52,"y":290,"deltaY":-120},{"type":"mousewheel","time":8928,"x":52,"y":290,"deltaY":-104},{"type":"mousewheel","time":8970,"x":52,"y":290,"deltaY":-90},{"type":"mousewheel","time":9014,"x":52,"y":290,"deltaY":-110},{"type":"mousewheel","time":9048,"x":52,"y":290,"deltaY":-59},{"type":"mousewheel","time":9086,"x":52,"y":290,"deltaY":-50},{"type":"mousewheel","time":9125,"x":52,"y":290,"deltaY":-41},{"type":"mousewheel","time":9189,"x":52,"y":290,"deltaY":-1},{"type":"mousewheel","time":9228,"x":52,"y":290,"deltaY":-119},{"type":"mousewheel","time":9269,"x":52,"y":290,"deltaY":-68},{"type":"mousewheel","time":9319,"x":52,"y":290,"deltaY":-105},{"type":"mousewheel","time":9355,"x":52,"y":290,"deltaY":-722},{"type":"mousewheel","time":9397,"x":52,"y":290,"deltaY":-169},{"type":"mousewheel","time":9437,"x":52,"y":290,"deltaY":-151},{"type":"mousewheel","time":9477,"x":52,"y":290,"deltaY":-192},{"type":"mousewheel","time":9515,"x":52,"y":290,"deltaY":-106},{"type":"mousewheel","time":9562,"x":52,"y":290,"deltaY":-133},{"type":"mousewheel","time":9605,"x":52,"y":290,"deltaY":-73},{"type":"mousemove","time":9622,"x":60,"y":290},{"type":"mousemove","time":9831,"x":290,"y":393},{"type":"mousemove","time":10050,"x":305,"y":406},{"type":"mousedown","time":10249,"x":303,"y":414},{"type":"mousemove","time":10264,"x":303,"y":414},{"type":"mouseup","time":10440,"x":303,"y":414},{"time":10441,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":10689,"x":303,"y":414},{"type":"mousemove","time":10901,"x":28,"y":317},{"type":"mousemove","time":11111,"x":42,"y":294},{"type":"mousewheel","time":11289,"x":43,"y":292,"deltaY":-1},{"type":"mousemove","time":11319,"x":43,"y":292},{"type":"mousewheel","time":11335,"x":43,"y":292,"deltaY":-4},{"type":"mousewheel","time":11362,"x":43,"y":292,"deltaY":-45},{"type":"mousewheel","time":11393,"x":43,"y":292,"deltaY":-28},{"type":"mousewheel","time":11427,"x":43,"y":292,"deltaY":-39},{"type":"mousewheel","time":11457,"x":43,"y":292,"deltaY":-184},{"type":"mousewheel","time":11486,"x":43,"y":292,"deltaY":-44},{"type":"mousewheel","time":11513,"x":43,"y":292,"deltaY":-92},{"type":"mousewheel","time":11545,"x":43,"y":292,"deltaY":-80},{"type":"mousewheel","time":11578,"x":43,"y":292,"deltaY":-35},{"type":"mousewheel","time":11627,"x":43,"y":292,"deltaY":-2},{"type":"mousewheel","time":11660,"x":43,"y":292,"deltaY":-21},{"type":"mousewheel","time":11693,"x":43,"y":292,"deltaY":-17},{"type":"mousewheel","time":11735,"x":43,"y":292,"deltaY":-34},{"type":"mousewheel","time":11767,"x":43,"y":292,"deltaY":-15},{"type":"mousewheel","time":11799,"x":43,"y":292,"deltaY":-66},{"type":"mousewheel","time":11829,"x":43,"y":292,"deltaY":-16},{"type":"mousewheel","time":11864,"x":43,"y":292,"deltaY":-35},{"type":"mousemove","time":11900,"x":52,"y":292},{"type":"mousewheel","time":11914,"x":43,"y":292,"deltaY":-16},{"type":"mousemove","time":12113,"x":315,"y":409},{"type":"mousemove","time":12322,"x":314,"y":416},{"type":"mousemove","time":12527,"x":301,"y":423},{"type":"mousemove","time":12739,"x":293,"y":412},{"type":"mousedown","time":12838,"x":291,"y":411},{"type":"mouseup","time":12961,"x":291,"y":411},{"time":12962,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":13007,"x":291,"y":411},{"type":"mousemove","time":13410,"x":291,"y":410},{"type":"mousemove","time":13631,"x":287,"y":323},{"type":"mousewheel","time":13909,"x":287,"y":323,"deltaY":-1},{"type":"mousewheel","time":13947,"x":287,"y":323,"deltaY":-3},{"type":"mousewheel","time":13986,"x":287,"y":323,"deltaY":-9},{"type":"mousewheel","time":14026,"x":287,"y":323,"deltaY":-7},{"type":"mousewheel","time":14075,"x":287,"y":323,"deltaY":-3},{"type":"mousewheel","time":14114,"x":287,"y":323,"deltaY":-1}],"scrollY":890,"scrollX":0,"timestamp":1750343958054},{"name":"Action 4","ops":[{"type":"mousemove","time":790,"x":270,"y":380},{"type":"mousemove","time":991,"x":165,"y":385},{"type":"mousemove","time":1199,"x":136,"y":198},{"type":"mousemove","time":1403,"x":137,"y":178},{"type":"mousemove","time":1620,"x":139,"y":173},{"type":"valuechange","selector":"#main_in_matrix>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select>select.test-inputs-select-select","value":"1","time":2606,"target":"select"},{"time":2607,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":2641,"x":141,"y":155},{"type":"mousemove","time":2843,"x":140,"y":163},{"type":"mousemove","time":3058,"x":138,"y":166},{"type":"mousemove","time":3287,"x":138,"y":167},{"type":"mousemove","time":5524,"x":229,"y":432},{"type":"mousewheel","time":5584,"x":229,"y":432,"deltaY":1},{"type":"mousewheel","time":5624,"x":229,"y":432,"deltaY":3},{"type":"mousewheel","time":5659,"x":229,"y":432,"deltaY":13},{"type":"mousewheel","time":5695,"x":229,"y":432,"deltaY":18},{"type":"mousewheel","time":5728,"x":229,"y":432,"deltaY":21},{"type":"mousewheel","time":5763,"x":229,"y":432,"deltaY":16},{"type":"mousewheel","time":5796,"x":229,"y":432,"deltaY":9},{"type":"mousewheel","time":5829,"x":229,"y":432,"deltaY":3},{"type":"mousewheel","time":5865,"x":229,"y":432,"deltaY":2},{"type":"mousewheel","time":5896,"x":229,"y":432,"deltaY":0},{"type":"mousewheel","time":5927,"x":229,"y":432,"deltaY":1},{"type":"mousewheel","time":5960,"x":229,"y":432,"deltaY":1},{"type":"mousewheel","time":5991,"x":229,"y":432,"deltaY":2},{"type":"mousewheel","time":6022,"x":229,"y":432,"deltaY":2},{"type":"mousewheel","time":6053,"x":229,"y":432,"deltaY":2},{"type":"mousewheel","time":6089,"x":229,"y":432,"deltaY":1},{"type":"mousewheel","time":6166,"x":229,"y":432,"deltaY":-1},{"type":"mousewheel","time":6199,"x":229,"y":432,"deltaY":-1},{"type":"mousewheel","time":6231,"x":229,"y":432,"deltaY":-14},{"type":"mousewheel","time":6264,"x":229,"y":432,"deltaY":-35},{"type":"mousewheel","time":6300,"x":229,"y":432,"deltaY":-20},{"type":"mousewheel","time":6332,"x":229,"y":432,"deltaY":-34},{"type":"mousewheel","time":6364,"x":229,"y":432,"deltaY":-8},{"type":"mousewheel","time":6397,"x":229,"y":432,"deltaY":-3},{"type":"mousewheel","time":6429,"x":229,"y":432,"deltaY":-4},{"type":"mousewheel","time":6462,"x":229,"y":432,"deltaY":-3},{"type":"mousewheel","time":6495,"x":229,"y":432,"deltaY":-2},{"type":"mousewheel","time":6534,"x":229,"y":432,"deltaY":-1},{"type":"mousemove","time":6774,"x":235,"y":432},{"type":"mousemove","time":6978,"x":540,"y":511},{"type":"mousemove","time":7195,"x":536,"y":515},{"type":"mousedown","time":7438,"x":536,"y":515},{"type":"mousemove","time":7453,"x":531,"y":515},{"type":"mousemove","time":7673,"x":426,"y":525},{"type":"mousemove","time":7878,"x":437,"y":555},{"type":"mousemove","time":8091,"x":522,"y":533},{"type":"mousemove","time":8310,"x":522,"y":529},{"type":"mousewheel","time":8476,"x":522,"y":529,"deltaY":1},{"type":"mouseup","time":8493,"x":522,"y":529},{"time":8494,"delay":400,"type":"screenshot-auto"},{"type":"mousewheel","time":8509,"x":522,"y":529,"deltaY":4},{"type":"mousewheel","time":8541,"x":522,"y":529,"deltaY":11},{"type":"mousewheel","time":8575,"x":522,"y":529,"deltaY":12},{"type":"mousewheel","time":8612,"x":522,"y":529,"deltaY":8},{"type":"mousewheel","time":8647,"x":522,"y":529,"deltaY":8},{"type":"mousewheel","time":8681,"x":522,"y":529,"deltaY":7},{"type":"mousewheel","time":8717,"x":522,"y":529,"deltaY":5},{"type":"mousewheel","time":8751,"x":522,"y":529,"deltaY":1},{"type":"mousewheel","time":8835,"x":522,"y":529,"deltaY":-1},{"type":"mousewheel","time":8867,"x":522,"y":529,"deltaY":-1},{"type":"mousewheel","time":8901,"x":522,"y":529,"deltaY":-10},{"type":"mousewheel","time":8935,"x":522,"y":529,"deltaY":-36},{"type":"mousewheel","time":8969,"x":522,"y":529,"deltaY":-53},{"type":"mousewheel","time":9002,"x":522,"y":529,"deltaY":-28},{"type":"mousewheel","time":9035,"x":522,"y":529,"deltaY":-14},{"type":"mousewheel","time":9074,"x":522,"y":529,"deltaY":-3},{"type":"mousemove","time":9207,"x":516,"y":529},{"type":"mousemove","time":9408,"x":341,"y":563},{"type":"mousedown","time":9619,"x":324,"y":565},{"type":"mousemove","time":9652,"x":324,"y":565},{"type":"mouseup","time":9733,"x":324,"y":565},{"time":9734,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":10042,"x":324,"y":565},{"type":"mousedown","time":10102,"x":326,"y":562},{"type":"mouseup","time":10203,"x":327,"y":562},{"time":10204,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":10265,"x":327,"y":562},{"type":"mousemove","time":10393,"x":329,"y":562},{"type":"mousemove","time":10609,"x":519,"y":437},{"type":"mousemove","time":10812,"x":547,"y":418},{"type":"mousedown","time":10954,"x":553,"y":411},{"type":"mousemove","time":11043,"x":553,"y":411},{"type":"mouseup","time":11058,"x":553,"y":411},{"time":11059,"delay":400,"type":"screenshot-auto"}],"scrollY":1245,"scrollX":0,"timestamp":1750343977599}] \ No newline at end of file diff --git a/test/runTest/actions/matrix2.json b/test/runTest/actions/matrix2.json new file mode 100644 index 0000000000..b3eecf24ba --- /dev/null +++ b/test/runTest/actions/matrix2.json @@ -0,0 +1 @@ +[{"name":"Action 1","ops":[{"type":"mousemove","time":543,"x":612,"y":165},{"type":"mousemove","time":743,"x":318,"y":198},{"type":"mousemove","time":950,"x":260,"y":218},{"type":"mousemove","time":1227,"x":259,"y":218},{"type":"mousemove","time":1427,"x":134,"y":108},{"type":"mousemove","time":1633,"x":121,"y":64},{"type":"mousemove","time":1849,"x":122,"y":54},{"type":"valuechange","selector":"#main_layout>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select>select.test-inputs-select-select","value":"1","time":2734,"target":"select"},{"time":2735,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":2757,"x":262,"y":55},{"type":"mousemove","time":2958,"x":277,"y":57},{"type":"valuechange","selector":"#main_layout>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(2)>select.test-inputs-select-select","value":"1","time":3935,"target":"select"},{"time":3936,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":3956,"x":277,"y":73},{"type":"mousemove","time":4158,"x":284,"y":56},{"type":"mousemove","time":4371,"x":285,"y":54},{"type":"mousemove","time":4643,"x":285,"y":54},{"type":"mousemove","time":4854,"x":115,"y":74},{"type":"mousedown","time":4983,"x":99,"y":71},{"type":"mousemove","time":5066,"x":98,"y":71},{"type":"mouseup","time":5092,"x":98,"y":71},{"time":5093,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":5193,"x":101,"y":69},{"type":"mousemove","time":5400,"x":110,"y":62},{"type":"valuechange","selector":"#main_layout>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select>select.test-inputs-select-select","value":"0","time":6588,"target":"select"},{"time":6589,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":6633,"x":262,"y":54},{"type":"mousemove","time":6836,"x":282,"y":56},{"type":"valuechange","selector":"#main_layout>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(2)>select.test-inputs-select-select","value":"0","time":8051,"target":"select"},{"time":8052,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":8069,"x":285,"y":38},{"type":"mousemove","time":8270,"x":297,"y":41},{"type":"mousemove","time":8477,"x":317,"y":47},{"type":"mousemove","time":8677,"x":388,"y":57},{"type":"mousemove","time":8878,"x":409,"y":125},{"type":"mousemove","time":9085,"x":354,"y":144},{"type":"mousemove","time":9293,"x":324,"y":142},{"type":"mousedown","time":9450,"x":347,"y":123},{"type":"mousemove","time":9493,"x":347,"y":123},{"type":"mouseup","time":9542,"x":347,"y":123},{"time":9543,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":9793,"x":347,"y":126},{"type":"mousemove","time":9994,"x":371,"y":153},{"type":"mousemove","time":10201,"x":429,"y":133},{"type":"mousemove","time":10410,"x":574,"y":116},{"type":"mousedown","time":10575,"x":577,"y":114},{"type":"mousemove","time":10616,"x":577,"y":114},{"type":"mouseup","time":10692,"x":577,"y":114},{"time":10693,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":10976,"x":582,"y":115},{"type":"mousedown","time":11187,"x":601,"y":121},{"type":"mousemove","time":11206,"x":601,"y":121},{"type":"mouseup","time":11309,"x":601,"y":121},{"time":11310,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":11559,"x":598,"y":121},{"type":"mousemove","time":11768,"x":552,"y":125},{"type":"mousedown","time":11866,"x":549,"y":121},{"type":"mousemove","time":11969,"x":549,"y":121},{"type":"mouseup","time":11993,"x":549,"y":121},{"time":11994,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":12176,"x":548,"y":155},{"type":"mousemove","time":12377,"x":590,"y":160},{"type":"mousedown","time":12560,"x":594,"y":150},{"type":"mousemove","time":12584,"x":594,"y":150},{"type":"mouseup","time":12692,"x":594,"y":150},{"time":12693,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":12927,"x":594,"y":152},{"type":"mousemove","time":13137,"x":600,"y":184},{"type":"mousemove","time":13343,"x":592,"y":184},{"type":"mousedown","time":13501,"x":619,"y":177},{"type":"mousemove","time":13551,"x":619,"y":177},{"type":"mouseup","time":13609,"x":619,"y":177},{"time":13610,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":13865,"x":618,"y":179},{"type":"mousemove","time":14071,"x":575,"y":216},{"type":"mousemove","time":14277,"x":575,"y":214},{"type":"mousedown","time":14401,"x":578,"y":201},{"type":"mousemove","time":14484,"x":578,"y":201},{"type":"mouseup","time":14510,"x":578,"y":201},{"time":14511,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":14711,"x":568,"y":200},{"type":"mousemove","time":14922,"x":207,"y":158},{"type":"mousedown","time":15117,"x":233,"y":154},{"type":"mousemove","time":15151,"x":233,"y":154},{"type":"mouseup","time":15225,"x":233,"y":154},{"time":15226,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":15476,"x":231,"y":154},{"type":"mousedown","time":15657,"x":178,"y":185},{"type":"mousemove","time":15693,"x":178,"y":185},{"type":"mouseup","time":15746,"x":178,"y":185},{"time":15747,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":15976,"x":175,"y":185},{"type":"mousemove","time":16176,"x":88,"y":164},{"type":"mousedown","time":16199,"x":88,"y":164},{"type":"mouseup","time":16325,"x":88,"y":164},{"time":16326,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":16377,"x":88,"y":164},{"type":"mousemove","time":16494,"x":88,"y":164},{"type":"mousemove","time":16706,"x":91,"y":119},{"type":"mousedown","time":16813,"x":90,"y":106},{"type":"mousemove","time":16907,"x":90,"y":106},{"type":"mouseup","time":16934,"x":90,"y":106},{"time":16935,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":17160,"x":90,"y":107},{"type":"mousemove","time":17360,"x":74,"y":444},{"type":"mousemove","time":17568,"x":91,"y":349},{"type":"mousedown","time":17743,"x":96,"y":296},{"type":"mousemove","time":17787,"x":96,"y":296},{"type":"mouseup","time":17861,"x":96,"y":296},{"time":17862,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":18093,"x":99,"y":296},{"type":"mousemove","time":18304,"x":122,"y":352},{"type":"mousemove","time":18505,"x":121,"y":575},{"type":"mousemove","time":18644,"x":87,"y":595},{"type":"mousemove","time":18844,"x":72,"y":569},{"type":"mousedown","time":18901,"x":72,"y":569},{"type":"mouseup","time":19027,"x":72,"y":569},{"time":19028,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":19069,"x":72,"y":569},{"type":"mousemove","time":19193,"x":72,"y":569},{"type":"mousemove","time":19393,"x":153,"y":583},{"type":"mousedown","time":19585,"x":128,"y":581},{"type":"mousemove","time":19601,"x":128,"y":581},{"type":"mouseup","time":19709,"x":128,"y":581},{"time":19710,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":19995,"x":129,"y":581},{"type":"mousedown","time":20163,"x":185,"y":571},{"type":"mousemove","time":20211,"x":186,"y":571},{"type":"mouseup","time":20260,"x":186,"y":571},{"time":20261,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":20494,"x":188,"y":571},{"type":"mousemove","time":20704,"x":425,"y":559},{"type":"mousedown","time":20772,"x":426,"y":560},{"type":"mouseup","time":20876,"x":426,"y":560},{"time":20877,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":20909,"x":424,"y":562},{"type":"mousemove","time":21119,"x":372,"y":563},{"type":"mousedown","time":21330,"x":315,"y":568},{"type":"mousemove","time":21339,"x":315,"y":568},{"type":"mouseup","time":21444,"x":315,"y":568},{"time":21445,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":21539,"x":315,"y":571},{"type":"mousemove","time":21743,"x":534,"y":568},{"type":"mousedown","time":21943,"x":571,"y":566},{"type":"mousemove","time":21976,"x":571,"y":566},{"type":"mouseup","time":22059,"x":571,"y":566},{"time":22060,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":22210,"x":571,"y":565},{"type":"mousemove","time":22411,"x":621,"y":556},{"type":"mousedown","time":22433,"x":621,"y":556},{"type":"mouseup","time":22562,"x":621,"y":556},{"time":22563,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":22618,"x":621,"y":536},{"type":"mousemove","time":22835,"x":416,"y":500},{"type":"mousedown","time":22974,"x":396,"y":501},{"type":"mousemove","time":23040,"x":396,"y":501},{"type":"mouseup","time":23097,"x":396,"y":501},{"time":23098,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":23526,"x":396,"y":501},{"type":"mousemove","time":23727,"x":376,"y":468},{"type":"mousedown","time":23744,"x":376,"y":468},{"type":"mouseup","time":23842,"x":375,"y":467},{"time":23843,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":23938,"x":375,"y":467},{"type":"mousemove","time":24094,"x":375,"y":467},{"type":"mousemove","time":24306,"x":372,"y":215},{"type":"mousemove","time":24506,"x":363,"y":197},{"type":"mousedown","time":24602,"x":374,"y":188},{"type":"mouseup","time":24709,"x":374,"y":188},{"time":24710,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":24756,"x":374,"y":188},{"type":"mousemove","time":25012,"x":370,"y":186},{"type":"mousemove","time":25216,"x":336,"y":160},{"type":"mousedown","time":25260,"x":335,"y":160},{"type":"mousemove","time":25417,"x":335,"y":160},{"type":"mouseup","time":25442,"x":335,"y":160},{"time":25443,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":25540,"x":335,"y":162},{"type":"mousemove","time":25760,"x":335,"y":194},{"type":"mousedown","time":25782,"x":335,"y":194},{"type":"mouseup","time":25895,"x":335,"y":194},{"time":25896,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":25960,"x":335,"y":194},{"type":"mousemove","time":26169,"x":295,"y":196},{"type":"mousedown","time":26193,"x":295,"y":196},{"type":"mouseup","time":26276,"x":295,"y":196},{"time":26277,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":26374,"x":295,"y":208},{"type":"mousedown","time":26579,"x":295,"y":284},{"type":"mousemove","time":26588,"x":295,"y":284},{"type":"mouseup","time":26692,"x":295,"y":284},{"time":26693,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":26993,"x":295,"y":286},{"type":"mousemove","time":27205,"x":361,"y":332},{"type":"mousemove","time":27408,"x":381,"y":324},{"type":"mousemove","time":27612,"x":411,"y":438},{"type":"mousemove","time":27817,"x":401,"y":376},{"type":"mousemove","time":28018,"x":400,"y":401},{"type":"mousedown","time":28140,"x":401,"y":390},{"type":"mousemove","time":28218,"x":401,"y":390},{"type":"mouseup","time":28261,"x":401,"y":390},{"time":28262,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":28445,"x":399,"y":388},{"type":"mousemove","time":28655,"x":230,"y":186},{"type":"mousemove","time":28862,"x":217,"y":170},{"type":"mousedown","time":28918,"x":217,"y":169},{"type":"mouseup","time":29010,"x":217,"y":169},{"time":29011,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":29071,"x":217,"y":169},{"type":"mousemove","time":29176,"x":217,"y":170},{"type":"mousemove","time":29377,"x":218,"y":240},{"type":"mousedown","time":29511,"x":215,"y":243},{"type":"mousemove","time":29585,"x":215,"y":243},{"type":"mouseup","time":29626,"x":215,"y":243},{"time":29627,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":29785,"x":187,"y":267},{"type":"mousemove","time":29991,"x":155,"y":269},{"type":"mousedown","time":30102,"x":148,"y":269},{"type":"mouseup","time":30192,"x":148,"y":269},{"time":30193,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":30239,"x":148,"y":269},{"type":"mousemove","time":30443,"x":148,"y":271},{"type":"mousemove","time":30643,"x":191,"y":496},{"type":"mousemove","time":30843,"x":209,"y":465},{"type":"mousedown","time":30942,"x":210,"y":461},{"type":"mouseup","time":31043,"x":210,"y":461},{"time":31044,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":31089,"x":210,"y":461},{"type":"mousemove","time":31585,"x":211,"y":456},{"type":"mousemove","time":31786,"x":387,"y":301},{"type":"mousemove","time":32003,"x":452,"y":204},{"type":"mousemove","time":32227,"x":452,"y":201},{"type":"mousemove","time":32436,"x":303,"y":75},{"type":"mousemove","time":32644,"x":288,"y":60},{"type":"mousemove","time":32844,"x":416,"y":73},{"type":"mousemove","time":33053,"x":532,"y":55},{"type":"mousemove","time":33268,"x":533,"y":54},{"type":"mousemove","time":33427,"x":533,"y":54}],"scrollY":102,"scrollX":0,"timestamp":1748966621272},{"name":"Action 2","ops":[{"type":"mousemove","time":512,"x":423,"y":296},{"type":"mousemove","time":716,"x":466,"y":169},{"type":"mousemove","time":920,"x":556,"y":49},{"type":"mousemove","time":1129,"x":525,"y":35},{"type":"mousemove","time":1336,"x":523,"y":43},{"type":"valuechange","selector":"#main_layout>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(3)>select.test-inputs-select-select","value":"1","time":2588,"target":"select"},{"time":2589,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":2629,"x":519,"y":63},{"type":"mousemove","time":3363,"x":518,"y":64},{"type":"mousemove","time":3571,"x":461,"y":107},{"type":"mousemove","time":3790,"x":336,"y":235},{"type":"mousedown","time":4006,"x":336,"y":235},{"type":"mousemove","time":4016,"x":339,"y":236},{"type":"mousemove","time":4219,"x":411,"y":287},{"type":"mousemove","time":4437,"x":412,"y":288},{"type":"mouseup","time":4478,"x":412,"y":288},{"time":4479,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":4762,"x":412,"y":288},{"type":"mousemove","time":4969,"x":382,"y":313},{"type":"mousemove","time":5230,"x":382,"y":313},{"type":"mousemove","time":5430,"x":382,"y":309},{"type":"mousemove","time":5637,"x":384,"y":304},{"type":"mousemove","time":5846,"x":408,"y":289},{"type":"mousedown","time":6037,"x":466,"y":264},{"type":"mousemove","time":6055,"x":466,"y":264},{"type":"mouseup","time":6194,"x":466,"y":264},{"time":6195,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":6312,"x":467,"y":264},{"type":"mousemove","time":6513,"x":586,"y":164},{"type":"mousedown","time":6561,"x":586,"y":164},{"type":"mouseup","time":6698,"x":586,"y":164},{"time":6699,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":6744,"x":586,"y":164},{"type":"mousemove","time":6878,"x":586,"y":164},{"type":"mousemove","time":7078,"x":591,"y":210},{"type":"mousedown","time":7130,"x":591,"y":213},{"type":"mouseup","time":7237,"x":591,"y":213},{"time":7238,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":7295,"x":591,"y":213},{"type":"mousemove","time":7579,"x":591,"y":214},{"type":"mousemove","time":7787,"x":585,"y":229},{"type":"mousedown","time":7811,"x":585,"y":229},{"type":"mouseup","time":7945,"x":585,"y":229},{"time":7946,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":8013,"x":585,"y":229},{"type":"mousemove","time":8081,"x":585,"y":229},{"type":"mousemove","time":8295,"x":587,"y":120},{"type":"mousedown","time":8392,"x":588,"y":103},{"type":"mouseup","time":8495,"x":588,"y":103},{"time":8496,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":8517,"x":588,"y":103},{"type":"mousemove","time":8778,"x":588,"y":103},{"type":"mousemove","time":8994,"x":549,"y":104},{"type":"mousedown","time":9112,"x":549,"y":104},{"type":"mousemove","time":9206,"x":549,"y":104},{"type":"mouseup","time":9244,"x":549,"y":104},{"time":9245,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":9433,"x":544,"y":104},{"type":"mousemove","time":9643,"x":345,"y":127},{"type":"mousedown","time":9701,"x":345,"y":127},{"type":"mouseup","time":9861,"x":345,"y":127},{"time":9862,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":9933,"x":345,"y":127},{"type":"mousemove","time":9958,"x":344,"y":127},{"type":"mousemove","time":10166,"x":297,"y":117},{"type":"mousemove","time":10369,"x":244,"y":109},{"type":"mousedown","time":10533,"x":214,"y":102},{"type":"mousemove","time":10571,"x":213,"y":102},{"type":"mouseup","time":10611,"x":213,"y":102},{"time":10612,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":10868,"x":209,"y":102},{"type":"mousemove","time":11083,"x":100,"y":111},{"type":"mousedown","time":11228,"x":90,"y":105},{"type":"mousemove","time":11287,"x":90,"y":105},{"type":"mouseup","time":11361,"x":90,"y":105},{"time":11362,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":11646,"x":90,"y":106},{"type":"mousedown","time":11833,"x":91,"y":166},{"type":"mousemove","time":11870,"x":91,"y":166},{"type":"mouseup","time":11945,"x":91,"y":166},{"time":11946,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":12162,"x":92,"y":166},{"type":"mousedown","time":12316,"x":168,"y":171},{"type":"mousemove","time":12377,"x":169,"y":171},{"type":"mouseup","time":12428,"x":169,"y":171},{"time":12429,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":12581,"x":283,"y":171},{"type":"mousedown","time":12762,"x":307,"y":171},{"type":"mousemove","time":12801,"x":307,"y":171},{"type":"mouseup","time":12851,"x":307,"y":171},{"time":12852,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":13045,"x":307,"y":175},{"type":"mousemove","time":13266,"x":212,"y":267},{"type":"mousedown","time":13324,"x":210,"y":268},{"type":"mouseup","time":13461,"x":209,"y":268},{"time":13462,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":13500,"x":209,"y":268},{"type":"mousemove","time":13711,"x":145,"y":281},{"type":"mousedown","time":13802,"x":145,"y":281},{"type":"mousemove","time":13925,"x":145,"y":281},{"type":"mouseup","time":13950,"x":145,"y":281},{"time":13951,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":14015,"x":145,"y":282},{"type":"mousemove","time":14232,"x":362,"y":454},{"type":"mousedown","time":14301,"x":363,"y":455},{"type":"mouseup","time":14436,"x":363,"y":455},{"time":14437,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":14458,"x":363,"y":455},{"type":"mousemove","time":14666,"x":366,"y":453},{"type":"mousemove","time":14871,"x":336,"y":506},{"type":"mousedown","time":15029,"x":336,"y":507},{"type":"mousemove","time":15084,"x":336,"y":507},{"type":"mouseup","time":15178,"x":336,"y":507},{"time":15179,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":15214,"x":336,"y":507},{"type":"mousedown","time":15435,"x":279,"y":551},{"type":"mousemove","time":15459,"x":279,"y":551},{"type":"mouseup","time":15548,"x":279,"y":551},{"time":15549,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":15670,"x":182,"y":551},{"type":"mousemove","time":15870,"x":75,"y":552},{"type":"mousedown","time":15952,"x":68,"y":550},{"type":"mouseup","time":16083,"x":68,"y":550},{"time":16084,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":16110,"x":81,"y":550},{"type":"mousemove","time":16326,"x":684,"y":550},{"type":"mousemove","time":16533,"x":597,"y":564},{"type":"mousedown","time":16599,"x":583,"y":561},{"type":"mouseup","time":16728,"x":582,"y":561},{"time":16729,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":16762,"x":582,"y":561},{"type":"mousedown","time":16954,"x":602,"y":504},{"type":"mousemove","time":16998,"x":602,"y":504},{"type":"mouseup","time":17045,"x":602,"y":504},{"time":17046,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":17234,"x":602,"y":504},{"type":"mousemove","time":17444,"x":569,"y":50},{"type":"mousemove","time":17646,"x":549,"y":51},{"type":"mousemove","time":17855,"x":549,"y":51},{"type":"valuechange","selector":"#main_layout>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(3)>select.test-inputs-select-select","value":"2","time":18940,"target":"select"},{"time":18941,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":18995,"x":444,"y":128},{"type":"mousemove","time":19195,"x":543,"y":83},{"type":"mousedown","time":19272,"x":547,"y":82},{"type":"mouseup","time":19380,"x":547,"y":82},{"time":19381,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":19426,"x":547,"y":82},{"type":"mousedown","time":19644,"x":601,"y":82},{"type":"mousemove","time":19682,"x":601,"y":82},{"type":"mouseup","time":19788,"x":601,"y":82},{"time":19789,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":19838,"x":601,"y":84},{"type":"mousemove","time":20052,"x":601,"y":132},{"type":"mousemove","time":20265,"x":601,"y":134},{"type":"mousedown","time":20323,"x":601,"y":134},{"type":"mouseup","time":20428,"x":601,"y":134},{"time":20429,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":20477,"x":601,"y":134},{"type":"mousemove","time":20645,"x":601,"y":136},{"type":"mousedown","time":20835,"x":594,"y":160},{"type":"mousemove","time":20875,"x":594,"y":160},{"type":"mouseup","time":20989,"x":594,"y":160},{"time":20990,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":21014,"x":586,"y":171},{"type":"mousemove","time":21225,"x":578,"y":258},{"type":"mousedown","time":21367,"x":582,"y":256},{"type":"mousemove","time":21477,"x":582,"y":256},{"type":"mouseup","time":21488,"x":582,"y":256},{"time":21489,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":21679,"x":582,"y":266},{"type":"mousemove","time":21902,"x":620,"y":509},{"type":"mousemove","time":22112,"x":607,"y":538},{"type":"mousemove","time":22312,"x":605,"y":523},{"type":"mousedown","time":22505,"x":603,"y":526},{"type":"mousemove","time":22515,"x":603,"y":526},{"type":"mouseup","time":22611,"x":603,"y":526},{"time":22612,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":22762,"x":601,"y":531},{"type":"mousedown","time":22925,"x":588,"y":546},{"type":"mousemove","time":22975,"x":588,"y":546},{"type":"mouseup","time":23028,"x":588,"y":546},{"time":23029,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":23179,"x":553,"y":546},{"type":"mousedown","time":23291,"x":538,"y":546},{"type":"mousemove","time":23392,"x":537,"y":546},{"type":"mouseup","time":23402,"x":537,"y":546},{"time":23403,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":23613,"x":536,"y":544},{"type":"mousedown","time":23770,"x":517,"y":516},{"type":"mousemove","time":23833,"x":517,"y":516},{"type":"mouseup","time":23885,"x":517,"y":516},{"time":23886,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":24040,"x":516,"y":513},{"type":"mousedown","time":24217,"x":481,"y":367},{"type":"mousemove","time":24257,"x":481,"y":366},{"type":"mouseup","time":24322,"x":481,"y":366},{"time":24323,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":24490,"x":477,"y":366},{"type":"mousemove","time":24693,"x":132,"y":222},{"type":"mousemove","time":24894,"x":173,"y":189},{"type":"mousedown","time":24963,"x":174,"y":186},{"type":"mouseup","time":25045,"x":174,"y":185},{"time":25046,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":25098,"x":182,"y":185},{"type":"mousemove","time":25316,"x":285,"y":138},{"type":"mousedown","time":25356,"x":285,"y":138},{"type":"mouseup","time":25464,"x":285,"y":138},{"time":25465,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":25505,"x":285,"y":141},{"type":"mousedown","time":25674,"x":285,"y":193},{"type":"mousemove","time":25731,"x":285,"y":193},{"type":"mouseup","time":25798,"x":285,"y":193},{"time":25799,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":25942,"x":180,"y":243},{"type":"mousemove","time":26151,"x":152,"y":267},{"type":"mousedown","time":26230,"x":152,"y":267},{"type":"mousemove","time":26358,"x":152,"y":267},{"type":"mouseup","time":26395,"x":152,"y":267},{"time":26396,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":26662,"x":152,"y":267},{"type":"mousemove","time":26876,"x":180,"y":555},{"type":"mousemove","time":27077,"x":158,"y":561},{"type":"mousedown","time":27088,"x":158,"y":561},{"type":"mouseup","time":27178,"x":158,"y":561},{"time":27179,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":27283,"x":109,"y":561},{"type":"mousedown","time":27426,"x":87,"y":561},{"type":"mousemove","time":27494,"x":86,"y":561},{"type":"mouseup","time":27533,"x":86,"y":561},{"time":27534,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":27700,"x":72,"y":497},{"type":"mousedown","time":27752,"x":72,"y":496},{"type":"mouseup","time":27886,"x":72,"y":496},{"time":27887,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":27938,"x":72,"y":496},{"type":"mousemove","time":28131,"x":72,"y":496},{"type":"mousemove","time":28338,"x":141,"y":252},{"type":"mousedown","time":28433,"x":137,"y":249},{"type":"mouseup","time":28531,"x":137,"y":249},{"time":28532,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":28567,"x":137,"y":249},{"type":"mousedown","time":28757,"x":87,"y":249},{"type":"mousemove","time":28768,"x":87,"y":249},{"type":"mouseup","time":28879,"x":87,"y":249},{"time":28880,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":29055,"x":88,"y":246},{"type":"mousemove","time":29273,"x":480,"y":66},{"type":"mousemove","time":29474,"x":485,"y":63},{"type":"mousemove","time":29688,"x":529,"y":23},{"type":"mousemove","time":29922,"x":517,"y":40},{"type":"valuechange","selector":"#main_layout>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(3)>select.test-inputs-select-select","value":"3","time":31105,"target":"select"},{"time":31106,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":31146,"x":517,"y":73},{"type":"mousemove","time":31356,"x":517,"y":87},{"type":"mousemove","time":31562,"x":573,"y":99},{"type":"mousemove","time":31762,"x":544,"y":95},{"type":"mousedown","time":31783,"x":544,"y":95},{"type":"mouseup","time":31929,"x":544,"y":95},{"time":31930,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":31982,"x":544,"y":95},{"type":"mousedown","time":32203,"x":578,"y":95},{"type":"mousemove","time":32229,"x":578,"y":95},{"type":"mouseup","time":32367,"x":578,"y":95},{"time":32368,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":32420,"x":578,"y":95},{"type":"mousemove","time":32627,"x":578,"y":104},{"type":"mousemove","time":32837,"x":443,"y":118},{"type":"mousemove","time":33040,"x":298,"y":117},{"type":"mousedown","time":33166,"x":237,"y":119},{"type":"mousemove","time":33255,"x":237,"y":119},{"type":"mouseup","time":33378,"x":237,"y":119},{"time":33379,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":33645,"x":236,"y":119},{"type":"mousedown","time":33822,"x":218,"y":189},{"type":"mousemove","time":33869,"x":218,"y":189},{"type":"mouseup","time":33945,"x":218,"y":189},{"time":33946,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":34078,"x":285,"y":158},{"type":"mousedown","time":34266,"x":308,"y":141},{"type":"mousemove","time":34278,"x":308,"y":140},{"type":"mouseup","time":34446,"x":308,"y":140},{"time":34447,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":34546,"x":308,"y":141},{"type":"mousemove","time":34752,"x":309,"y":182},{"type":"mousedown","time":34799,"x":309,"y":183},{"type":"mouseup","time":34962,"x":309,"y":183},{"time":34963,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":35001,"x":309,"y":183},{"type":"mousemove","time":35013,"x":306,"y":183},{"type":"mousedown","time":35193,"x":232,"y":241},{"type":"mousemove","time":35239,"x":232,"y":242},{"type":"mouseup","time":35312,"x":232,"y":242},{"time":35313,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":35579,"x":231,"y":242},{"type":"mousedown","time":35772,"x":185,"y":226},{"type":"mousemove","time":35784,"x":185,"y":225},{"type":"mouseup","time":35911,"x":185,"y":225},{"time":35912,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":36001,"x":184,"y":263},{"type":"mousedown","time":36179,"x":164,"y":374},{"type":"mousemove","time":36219,"x":164,"y":374},{"type":"mouseup","time":36283,"x":164,"y":374},{"time":36284,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":36502,"x":164,"y":375},{"type":"mousemove","time":36718,"x":38,"y":419},{"type":"mousedown","time":36747,"x":38,"y":419},{"type":"mouseup","time":36880,"x":38,"y":419},{"time":36881,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":36948,"x":38,"y":419},{"type":"mousemove","time":36962,"x":40,"y":423},{"type":"mousemove","time":37173,"x":55,"y":571},{"type":"mousedown","time":37201,"x":55,"y":571},{"type":"mouseup","time":37371,"x":55,"y":571},{"time":37372,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":37426,"x":55,"y":571},{"type":"mousemove","time":37567,"x":55,"y":571},{"type":"mousedown","time":37714,"x":139,"y":540},{"type":"mousemove","time":37787,"x":139,"y":540},{"type":"mouseup","time":37865,"x":139,"y":540},{"time":37866,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":37999,"x":177,"y":527},{"type":"mousedown","time":38138,"x":195,"y":449},{"type":"mousemove","time":38207,"x":195,"y":449},{"type":"mouseup","time":38262,"x":195,"y":449},{"time":38263,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":38479,"x":195,"y":451},{"type":"mousemove","time":38690,"x":235,"y":525},{"type":"mousemove","time":38891,"x":236,"y":530},{"type":"mousedown","time":39079,"x":215,"y":373},{"type":"mousemove","time":39123,"x":215,"y":373},{"type":"mouseup","time":39228,"x":215,"y":373},{"time":39229,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":39324,"x":215,"y":426},{"type":"mousemove","time":39543,"x":254,"y":576},{"type":"mousemove","time":39747,"x":260,"y":563},{"type":"mousedown","time":39874,"x":271,"y":559},{"type":"mousemove","time":39956,"x":272,"y":559},{"type":"mouseup","time":39967,"x":272,"y":559},{"time":39968,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":40173,"x":358,"y":543},{"type":"mousemove","time":40375,"x":563,"y":537},{"type":"mousedown","time":40413,"x":563,"y":538},{"type":"mouseup","time":40562,"x":563,"y":538},{"time":40563,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":40602,"x":565,"y":538},{"type":"mousedown","time":40801,"x":579,"y":538},{"type":"mousemove","time":40813,"x":579,"y":538},{"type":"mouseup","time":40947,"x":579,"y":538},{"time":40948,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":41019,"x":579,"y":501},{"type":"mousedown","time":41114,"x":579,"y":475},{"type":"mousemove","time":41251,"x":579,"y":475},{"type":"mouseup","time":41264,"x":579,"y":475},{"time":41265,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":41338,"x":579,"y":477},{"type":"mousemove","time":41538,"x":587,"y":563},{"type":"mousemove","time":41755,"x":636,"y":507},{"type":"mousemove","time":41966,"x":587,"y":310},{"type":"mousedown","time":42162,"x":532,"y":299},{"type":"mousemove","time":42173,"x":532,"y":299},{"type":"mouseup","time":42262,"x":532,"y":299},{"time":42263,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":42480,"x":531,"y":299},{"type":"mousedown","time":42693,"x":478,"y":317},{"type":"mousemove","time":42704,"x":478,"y":317},{"type":"mouseup","time":42788,"x":478,"y":317},{"time":42789,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":42931,"x":478,"y":316},{"type":"mousemove","time":43141,"x":594,"y":17},{"type":"mousemove","time":43351,"x":559,"y":36},{"type":"mousemove","time":43557,"x":548,"y":44},{"type":"valuechange","selector":"#main_layout>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(3)>select.test-inputs-select-select","value":"4","time":44724,"target":"select"},{"time":44725,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":44787,"x":487,"y":206},{"type":"mousemove","time":44989,"x":468,"y":335},{"type":"mousedown","time":45002,"x":468,"y":335},{"type":"mouseup","time":45128,"x":468,"y":335},{"time":45129,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":45226,"x":468,"y":335},{"type":"mousemove","time":45348,"x":459,"y":343},{"type":"mousedown","time":45521,"x":346,"y":450},{"type":"mousemove","time":45566,"x":346,"y":450},{"type":"mouseup","time":45634,"x":346,"y":450},{"time":45635,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":45850,"x":346,"y":449},{"type":"mousedown","time":46055,"x":322,"y":273},{"type":"mousemove","time":46067,"x":322,"y":273},{"type":"mouseup","time":46162,"x":322,"y":273},{"time":46163,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":46317,"x":325,"y":270},{"type":"mousedown","time":46517,"x":354,"y":156},{"type":"mousemove","time":46529,"x":354,"y":156},{"type":"mouseup","time":46669,"x":354,"y":156},{"time":46670,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":46731,"x":349,"y":156},{"type":"mousemove","time":46937,"x":300,"y":156},{"type":"mousedown","time":46969,"x":299,"y":156},{"type":"mouseup","time":47097,"x":299,"y":156},{"time":47098,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":47167,"x":299,"y":156},{"type":"mousemove","time":47241,"x":299,"y":156},{"type":"mousedown","time":47398,"x":281,"y":174},{"type":"mousemove","time":47458,"x":281,"y":174},{"type":"mouseup","time":47517,"x":281,"y":174},{"time":47518,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":47672,"x":281,"y":113},{"type":"mousedown","time":47756,"x":281,"y":109},{"type":"mouseup","time":47871,"x":281,"y":109},{"time":47872,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":47926,"x":281,"y":109},{"type":"mousemove","time":48014,"x":280,"y":109},{"type":"mousedown","time":48176,"x":185,"y":110},{"type":"mousemove","time":48233,"x":185,"y":110},{"type":"mouseup","time":48290,"x":185,"y":110},{"time":48291,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":48398,"x":182,"y":110},{"type":"mousemove","time":48601,"x":96,"y":110},{"type":"mousedown","time":48628,"x":96,"y":110},{"type":"mouseup","time":48764,"x":95,"y":110},{"time":48765,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":48802,"x":95,"y":110},{"type":"mousemove","time":49015,"x":164,"y":163},{"type":"mousedown","time":49118,"x":198,"y":149},{"type":"mouseup","time":49212,"x":198,"y":149},{"time":49213,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":49262,"x":198,"y":149},{"type":"mousemove","time":49436,"x":197,"y":149},{"type":"mousemove","time":49639,"x":157,"y":169},{"type":"mousedown","time":49780,"x":192,"y":176},{"type":"mousemove","time":49845,"x":192,"y":176},{"type":"mouseup","time":49968,"x":192,"y":176},{"time":49969,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":50021,"x":194,"y":176},{"type":"mousedown","time":50192,"x":219,"y":176},{"type":"mousemove","time":50250,"x":219,"y":176},{"type":"mouseup","time":50353,"x":219,"y":176},{"time":50354,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":50507,"x":219,"y":176},{"type":"mousemove","time":50714,"x":150,"y":140},{"type":"mousedown","time":50762,"x":149,"y":140},{"type":"mouseup","time":50912,"x":149,"y":140},{"time":50913,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":50935,"x":149,"y":140},{"type":"mousedown","time":51124,"x":72,"y":258},{"type":"mousemove","time":51138,"x":72,"y":258},{"type":"mouseup","time":51262,"x":72,"y":258},{"time":51263,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":51410,"x":72,"y":258},{"type":"mousemove","time":51627,"x":181,"y":328},{"type":"mousedown","time":51656,"x":181,"y":328},{"type":"mouseup","time":51798,"x":181,"y":328},{"time":51799,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":51854,"x":181,"y":328},{"type":"mousemove","time":52068,"x":276,"y":307},{"type":"mousedown","time":52137,"x":284,"y":307},{"type":"mouseup","time":52252,"x":284,"y":307},{"time":52253,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":52317,"x":284,"y":307},{"type":"mousemove","time":52382,"x":284,"y":307},{"type":"mousemove","time":52582,"x":244,"y":523},{"type":"mousemove","time":52788,"x":212,"y":551},{"type":"mousedown","time":52853,"x":208,"y":556},{"type":"mousemove","time":52991,"x":207,"y":557},{"type":"mouseup","time":53003,"x":207,"y":557},{"time":53004,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":53200,"x":56,"y":550},{"type":"mousedown","time":53293,"x":46,"y":550},{"type":"mouseup","time":53402,"x":46,"y":550},{"time":53403,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":53428,"x":46,"y":550},{"type":"mousemove","time":53630,"x":368,"y":545},{"type":"mousedown","time":53753,"x":400,"y":539},{"type":"mousemove","time":53875,"x":400,"y":539},{"type":"mouseup","time":53905,"x":400,"y":539},{"time":53906,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":54160,"x":400,"y":539},{"type":"mousemove","time":54176,"x":454,"y":408},{"type":"mousemove","time":54390,"x":543,"y":134},{"type":"mousemove","time":54593,"x":552,"y":45},{"type":"mousemove","time":54793,"x":552,"y":43},{"type":"valuechange","selector":"#main_layout>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(3)>select.test-inputs-select-select","value":"0","time":57076,"target":"select"},{"time":57077,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":57180,"x":551,"y":33},{"type":"mousemove","time":57390,"x":643,"y":295}],"scrollY":118,"scrollX":0,"timestamp":1748966697954},{"name":"Action 3","ops":[{"type":"mousemove","time":72,"x":683,"y":236},{"type":"mousemove","time":272,"x":753,"y":64},{"type":"mousemove","time":472,"x":707,"y":47},{"type":"mousemove","time":672,"x":699,"y":55},{"type":"valuechange","selector":"#main_layout>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(4)>select.test-inputs-select-select","value":"1","time":2146,"target":"select"},{"time":2147,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":2173,"x":699,"y":73},{"type":"mousemove","time":2387,"x":699,"y":72},{"type":"mousemove","time":2622,"x":701,"y":57},{"type":"mousemove","time":2839,"x":591,"y":127},{"type":"mousemove","time":3039,"x":497,"y":159},{"type":"mousemove","time":3249,"x":496,"y":146},{"type":"mousemove","time":3456,"x":499,"y":171},{"type":"mousemove","time":3682,"x":499,"y":177},{"type":"mousemove","time":4039,"x":499,"y":177},{"type":"mousemove","time":4249,"x":505,"y":151},{"type":"mousemove","time":4456,"x":535,"y":107},{"type":"mousemove","time":4665,"x":705,"y":33},{"type":"mousemove","time":4872,"x":706,"y":44},{"type":"mousemove","time":5083,"x":704,"y":49},{"type":"valuechange","selector":"#main_layout>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(4)>select.test-inputs-select-select","value":"2","time":6029,"target":"select"},{"time":6030,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":6042,"x":704,"y":65},{"type":"mousemove","time":6288,"x":700,"y":66},{"type":"mousemove","time":6507,"x":511,"y":153},{"type":"mousemove","time":6718,"x":494,"y":155},{"type":"mousemove","time":6933,"x":494,"y":145},{"type":"mousemove","time":7138,"x":498,"y":173},{"type":"mousemove","time":7349,"x":498,"y":174},{"type":"mousemove","time":7556,"x":533,"y":149},{"type":"mousemove","time":7756,"x":691,"y":47},{"type":"valuechange","selector":"#main_layout>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(4)>select.test-inputs-select-select","value":"0","time":9099,"target":"select"},{"time":9100,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":9129,"x":696,"y":10},{"type":"mousemove","time":9500,"x":695,"y":10},{"type":"mousemove","time":9700,"x":239,"y":105},{"type":"mousemove","time":9901,"x":236,"y":105},{"type":"mousemove","time":9973,"x":233,"y":104},{"type":"mousemove","time":10184,"x":170,"y":74},{"type":"mousemove","time":10399,"x":154,"y":71},{"type":"valuechange","selector":"#main_layout>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(5)>select.test-inputs-select-select","value":"1","time":11465,"target":"select"},{"time":11466,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":11495,"x":151,"y":96},{"type":"mousemove","time":11713,"x":146,"y":65},{"type":"valuechange","selector":"#main_layout>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(5)>select.test-inputs-select-select","value":"2","time":12630,"target":"select"},{"time":12631,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":12663,"x":252,"y":72},{"type":"mousemove","time":12877,"x":376,"y":57},{"type":"mousemove","time":13094,"x":411,"y":60},{"type":"mousemove","time":13298,"x":411,"y":60},{"type":"valuechange","selector":"#main_layout>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"1","time":14531,"target":"select"},{"time":14532,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":14545,"x":419,"y":90},{"type":"mousemove","time":14746,"x":440,"y":75},{"type":"mousemove","time":14976,"x":449,"y":64},{"type":"valuechange","selector":"#main_layout>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"2","time":16216,"target":"select"},{"time":16217,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":16249,"x":443,"y":76},{"type":"mousemove","time":16474,"x":442,"y":67},{"type":"valuechange","selector":"#main_layout>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"3","time":17414,"target":"select"},{"time":17415,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":17445,"x":442,"y":72},{"type":"mousemove","time":17664,"x":443,"y":72},{"type":"valuechange","selector":"#main_layout>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"4","time":18713,"target":"select"},{"time":18714,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":18743,"x":440,"y":69},{"type":"mousedown","time":18929,"x":448,"y":54},{"type":"mousemove","time":18973,"x":448,"y":54},{"type":"mouseup","time":19013,"x":448,"y":54},{"time":19014,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":19118,"x":448,"y":58},{"type":"mousemove","time":19318,"x":448,"y":101},{"type":"mousemove","time":19523,"x":444,"y":74},{"type":"mousemove","time":19733,"x":444,"y":71},{"type":"valuechange","selector":"#main_layout>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"5","time":20916,"target":"select"},{"time":20917,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":20950,"x":443,"y":58},{"type":"mousemove","time":21180,"x":443,"y":58},{"type":"valuechange","selector":"#main_layout>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"6","time":22198,"target":"select"},{"time":22199,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":22229,"x":449,"y":67},{"type":"mousemove","time":22438,"x":449,"y":67},{"type":"valuechange","selector":"#main_layout>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"7","time":23350,"target":"select"},{"time":23351,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":23382,"x":445,"y":76},{"type":"mousemove","time":23604,"x":444,"y":70},{"type":"valuechange","selector":"#main_layout>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"8","time":24615,"target":"select"},{"time":24616,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":24647,"x":445,"y":70},{"type":"mousemove","time":24864,"x":445,"y":68},{"type":"valuechange","selector":"#main_layout>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"9","time":25920,"target":"select"},{"time":25921,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":25953,"x":447,"y":67},{"type":"mousemove","time":26181,"x":447,"y":66},{"type":"valuechange","selector":"#main_layout>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"10","time":27080,"target":"select"},{"time":27081,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":27095,"x":447,"y":95},{"type":"mousemove","time":27297,"x":470,"y":61},{"type":"mousemove","time":27512,"x":511,"y":45},{"type":"valuechange","selector":"#main_layout>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(3)>select.test-inputs-select-select","value":"3","time":29013,"target":"select"},{"time":29014,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":29027,"x":478,"y":87},{"type":"mousemove","time":29238,"x":443,"y":56},{"type":"mousemove","time":29453,"x":443,"y":57},{"type":"mousemove","time":29654,"x":443,"y":64},{"type":"valuechange","selector":"#main_layout>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"9","time":31279,"target":"select"},{"time":31280,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":31295,"x":439,"y":57},{"type":"mousemove","time":31514,"x":436,"y":70},{"type":"valuechange","selector":"#main_layout>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"8","time":32798,"target":"select"},{"time":32799,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":32828,"x":429,"y":52},{"type":"mousemove","time":33030,"x":425,"y":65},{"type":"mousemove","time":33255,"x":425,"y":69},{"type":"valuechange","selector":"#main_layout>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"7","time":34114,"target":"select"},{"time":34115,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":34128,"x":429,"y":70},{"type":"valuechange","selector":"#main_layout>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"6","time":35462,"target":"select"},{"time":35463,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":35493,"x":428,"y":62},{"type":"mousemove","time":35710,"x":428,"y":68},{"type":"valuechange","selector":"#main_layout>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"5","time":37029,"target":"select"},{"time":37030,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":37060,"x":431,"y":51},{"type":"mousedown","time":37192,"x":432,"y":51},{"type":"mousemove","time":37272,"x":432,"y":51},{"type":"mouseup","time":37315,"x":432,"y":51},{"time":37316,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":37473,"x":432,"y":53},{"type":"mousemove","time":37684,"x":432,"y":64},{"type":"valuechange","selector":"#main_layout>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"4","time":38614,"target":"select"},{"time":38615,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":38647,"x":437,"y":63},{"type":"valuechange","selector":"#main_layout>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"3","time":39814,"target":"select"},{"time":39815,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":39846,"x":435,"y":60},{"type":"mousemove","time":40061,"x":435,"y":68},{"type":"valuechange","selector":"#main_layout>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"2","time":40964,"target":"select"},{"time":40965,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":40996,"x":435,"y":66},{"type":"mousemove","time":41222,"x":435,"y":71},{"type":"valuechange","selector":"#main_layout>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"1","time":42216,"target":"select"},{"time":42217,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":42250,"x":435,"y":58},{"type":"mousemove","time":42456,"x":434,"y":73},{"type":"mousemove","time":42642,"x":437,"y":72},{"type":"mousemove","time":42849,"x":526,"y":44},{"type":"mousemove","time":43081,"x":526,"y":44},{"type":"valuechange","selector":"#main_layout>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(3)>select.test-inputs-select-select","value":"2","time":45543,"target":"select"},{"time":45544,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":45560,"x":475,"y":38},{"type":"mousemove","time":45761,"x":428,"y":67},{"type":"mousemove","time":45977,"x":428,"y":68},{"type":"valuechange","selector":"#main_layout>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"2","time":47163,"target":"select"},{"time":47164,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":47178,"x":423,"y":98},{"type":"mousemove","time":47383,"x":436,"y":64},{"type":"mousemove","time":47588,"x":436,"y":64},{"type":"mousedown","time":48499,"x":436,"y":82},{"type":"mousemove","time":48511,"x":436,"y":82},{"type":"mouseup","time":48705,"x":436,"y":82},{"time":48706,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":48781,"x":436,"y":81},{"type":"mousemove","time":49004,"x":439,"y":71},{"type":"valuechange","selector":"#main_layout>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"3","time":49816,"target":"select"},{"time":49817,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":49846,"x":450,"y":71},{"type":"valuechange","selector":"#main_layout>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"4","time":50880,"target":"select"},{"time":50881,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":50910,"x":450,"y":72},{"type":"valuechange","selector":"#main_layout>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"5","time":51915,"target":"select"},{"time":51916,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":51948,"x":451,"y":72},{"type":"mousemove","time":52177,"x":451,"y":69},{"type":"valuechange","selector":"#main_layout>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"6","time":53060,"target":"select"},{"time":53061,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":53075,"x":451,"y":76},{"type":"mousemove","time":53307,"x":451,"y":74},{"type":"valuechange","selector":"#main_layout>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"7","time":54114,"target":"select"},{"time":54115,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":54145,"x":454,"y":62},{"type":"valuechange","selector":"#main_layout>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"8","time":55135,"target":"select"},{"time":55136,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":55167,"x":451,"y":69},{"type":"mousemove","time":55369,"x":451,"y":68},{"type":"valuechange","selector":"#main_layout>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"9","time":56130,"target":"select"},{"time":56131,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":56162,"x":451,"y":68},{"type":"mousemove","time":56390,"x":451,"y":66},{"type":"valuechange","selector":"#main_layout>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"10","time":57232,"target":"select"},{"time":57233,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":57264,"x":552,"y":116},{"type":"mousedown","time":57377,"x":630,"y":147},{"type":"mousemove","time":57472,"x":630,"y":147},{"type":"mouseup","time":57484,"x":630,"y":147},{"time":57485,"delay":400,"type":"screenshot-auto"}],"scrollY":118,"scrollX":0,"timestamp":1748966763745},{"name":"Action 4","ops":[{"type":"mousemove","time":107,"x":402,"y":476},{"type":"mousemove","time":307,"x":724,"y":490},{"type":"mousemove","time":524,"x":726,"y":490},{"type":"mousemove","time":725,"x":774,"y":511},{"type":"mousemove","time":936,"x":783,"y":526},{"type":"mousemove","time":1152,"x":783,"y":529},{"type":"mousedown","time":1388,"x":783,"y":529},{"type":"mousemove","time":1401,"x":783,"y":529},{"type":"mousemove","time":1604,"x":699,"y":511},{"type":"mousemove","time":1817,"x":698,"y":510},{"type":"mousemove","time":1924,"x":697,"y":510},{"type":"mousemove","time":2124,"x":582,"y":480},{"type":"mousemove","time":2337,"x":582,"y":480},{"type":"mousemove","time":2375,"x":582,"y":480},{"type":"mousemove","time":2586,"x":539,"y":476},{"type":"mousemove","time":2803,"x":501,"y":477},{"type":"mousemove","time":2958,"x":498,"y":476},{"type":"mousemove","time":3158,"x":504,"y":427},{"type":"mousemove","time":3358,"x":503,"y":384},{"type":"mousemove","time":3625,"x":503,"y":383},{"type":"mousemove","time":3825,"x":506,"y":380},{"type":"mousemove","time":4036,"x":511,"y":375},{"type":"mousemove","time":4241,"x":522,"y":360},{"type":"mousemove","time":4452,"x":525,"y":353},{"type":"mousemove","time":4658,"x":529,"y":338},{"type":"mousemove","time":4859,"x":534,"y":316},{"type":"mousemove","time":5071,"x":535,"y":306},{"type":"mousemove","time":5275,"x":546,"y":334},{"type":"mousemove","time":5478,"x":544,"y":351},{"type":"mousemove","time":5694,"x":528,"y":386},{"type":"mousemove","time":5907,"x":500,"y":408},{"type":"mousemove","time":6090,"x":499,"y":408},{"type":"mousemove","time":6291,"x":275,"y":417},{"type":"mousemove","time":6504,"x":274,"y":418},{"type":"mousemove","time":6561,"x":273,"y":419},{"type":"mousemove","time":6769,"x":241,"y":423},{"type":"mousemove","time":6975,"x":227,"y":424},{"type":"mousemove","time":7191,"x":215,"y":424},{"type":"mousemove","time":7397,"x":186,"y":425},{"type":"mousemove","time":7607,"x":155,"y":425},{"type":"mousemove","time":7810,"x":174,"y":438},{"type":"mousemove","time":8022,"x":190,"y":443},{"type":"mousemove","time":8228,"x":212,"y":445},{"type":"mousemove","time":8440,"x":273,"y":446},{"type":"mousemove","time":8655,"x":277,"y":445},{"type":"mousemove","time":8857,"x":478,"y":464},{"type":"mousemove","time":9069,"x":496,"y":465},{"type":"mousemove","time":9274,"x":696,"y":498},{"type":"mousemove","time":9476,"x":727,"y":500},{"type":"mousemove","time":9680,"x":744,"y":510},{"type":"mousemove","time":9891,"x":751,"y":516},{"type":"mousemove","time":10104,"x":753,"y":516},{"type":"mouseup","time":10142,"x":753,"y":516},{"time":10143,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":10156,"x":601,"y":430},{"type":"mousemove","time":10356,"x":145,"y":175},{"type":"mousemove","time":10525,"x":145,"y":175},{"type":"mousemove","time":10725,"x":135,"y":179},{"type":"mousemove","time":10938,"x":134,"y":179},{"type":"mousemove","time":11141,"x":179,"y":167},{"type":"mousemove","time":11355,"x":227,"y":167},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"1","time":12328,"target":"select"},{"time":12329,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":12375,"x":233,"y":176},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"2","time":13409,"target":"select"},{"time":13410,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":13468,"x":236,"y":181},{"type":"mousemove","time":13670,"x":236,"y":179},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"3","time":14461,"target":"select"},{"time":14462,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":14516,"x":242,"y":169},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"4","time":15676,"target":"select"},{"time":15677,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":15716,"x":235,"y":177},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"5","time":16708,"target":"select"},{"time":16709,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":16767,"x":247,"y":175},{"type":"mousemove","time":16970,"x":248,"y":174},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"6","time":17792,"target":"select"},{"time":17793,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":17846,"x":253,"y":175},{"type":"mousemove","time":18054,"x":254,"y":174},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"7","time":18874,"target":"select"},{"time":18875,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":18924,"x":257,"y":179},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"8","time":19944,"target":"select"},{"time":19945,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":19985,"x":258,"y":176},{"type":"mousemove","time":20185,"x":260,"y":174},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"9","time":21025,"target":"select"},{"time":21026,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":21062,"x":267,"y":176},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"10","time":22043,"target":"select"},{"time":22044,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":22081,"x":272,"y":170},{"type":"mousemove","time":22286,"x":272,"y":170},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"11","time":23157,"target":"select"},{"time":23158,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":23184,"x":136,"y":172},{"type":"mousemove","time":23387,"x":125,"y":163},{"type":"mousemove","time":23604,"x":125,"y":160},{"type":"mousemove","time":23821,"x":125,"y":158},{"type":"mousemove","time":24036,"x":124,"y":158},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select>select.test-inputs-select-select","value":"1","time":25766,"target":"select"},{"time":25767,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":25797,"x":226,"y":157},{"type":"mousemove","time":26011,"x":286,"y":151},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(2)>select.test-inputs-select-select","value":"1","time":26881,"target":"select"},{"time":26882,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":26910,"x":249,"y":176},{"type":"mousemove","time":27121,"x":249,"y":176},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"10","time":28010,"target":"select"},{"time":28011,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":28057,"x":244,"y":171},{"type":"mousemove","time":28258,"x":244,"y":173},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"9","time":29309,"target":"select"},{"time":29310,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":29350,"x":245,"y":178},{"type":"mousedown","time":29456,"x":243,"y":184},{"type":"mousemove","time":29553,"x":243,"y":184},{"type":"mouseup","time":29564,"x":243,"y":184},{"time":29565,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":29692,"x":243,"y":183},{"type":"mousemove","time":29903,"x":262,"y":159},{"type":"mousemove","time":30153,"x":255,"y":176},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"8","time":31043,"target":"select"},{"time":31044,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":31097,"x":236,"y":171},{"type":"mousemove","time":31302,"x":236,"y":171},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"7","time":32151,"target":"select"},{"time":32152,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":32193,"x":245,"y":177},{"type":"mousemove","time":32402,"x":245,"y":178},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"6","time":33143,"target":"select"},{"time":33144,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":33172,"x":252,"y":180},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"5","time":34298,"target":"select"},{"time":34299,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":34359,"x":254,"y":167},{"type":"mousemove","time":34571,"x":249,"y":175},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"4","time":35962,"target":"select"},{"time":35963,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":35989,"x":258,"y":152},{"type":"mousemove","time":36189,"x":258,"y":168},{"type":"mousemove","time":36436,"x":258,"y":168},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"3","time":37227,"target":"select"},{"time":37228,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":37268,"x":264,"y":177},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"2","time":38359,"target":"select"},{"time":38360,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":38386,"x":269,"y":169},{"type":"mousemove","time":38587,"x":270,"y":169},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"1","time":39525,"target":"select"},{"time":39526,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":39550,"x":277,"y":147},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(2)>select.test-inputs-select-select","value":"0","time":40849,"target":"select"},{"time":40850,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":40865,"x":178,"y":131},{"type":"mousemove","time":41095,"x":126,"y":144},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select>select.test-inputs-select-select","value":"0","time":42156,"target":"select"},{"time":42157,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":42173,"x":254,"y":131},{"type":"mousemove","time":42382,"x":507,"y":146},{"type":"mousemove","time":42608,"x":514,"y":149},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(3)>select.test-inputs-select-select","value":"2","time":44627,"target":"select"},{"time":44628,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":44655,"x":352,"y":199},{"type":"mousemove","time":44855,"x":280,"y":180},{"type":"mousemove","time":45069,"x":280,"y":179},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"2","time":45862,"target":"select"},{"time":45863,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":45906,"x":311,"y":173},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"3","time":46911,"target":"select"},{"time":46912,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":46952,"x":310,"y":172},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"4","time":47926,"target":"select"},{"time":47927,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":47967,"x":311,"y":171},{"type":"mousemove","time":48171,"x":312,"y":167},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"5","time":48995,"target":"select"},{"time":48996,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":49054,"x":312,"y":169},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"6","time":49997,"target":"select"},{"time":49998,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":50042,"x":321,"y":178},{"type":"mousemove","time":50254,"x":322,"y":175},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"7","time":51043,"target":"select"},{"time":51044,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":51083,"x":318,"y":167},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"8","time":52628,"target":"select"},{"time":52629,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":52655,"x":301,"y":198},{"type":"mousemove","time":52856,"x":304,"y":178},{"type":"mousemove","time":53071,"x":304,"y":174},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"9","time":53828,"target":"select"},{"time":53829,"delay":400,"type":"screenshot-auto"},{"type":"mousedown","time":53883,"x":318,"y":162},{"type":"mousemove","time":53897,"x":318,"y":162},{"type":"mouseup","time":53992,"x":318,"y":162},{"time":53993,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":54159,"x":318,"y":163},{"type":"mousemove","time":54369,"x":315,"y":168},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"10","time":55244,"target":"select"},{"time":55245,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":55283,"x":321,"y":169},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"11","time":56227,"target":"select"},{"time":56228,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":56257,"x":462,"y":168},{"type":"mousemove","time":56459,"x":510,"y":145},{"type":"mousemove","time":56672,"x":509,"y":145},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(3)>select.test-inputs-select-select","value":"3","time":57530,"target":"select"},{"time":57531,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":57546,"x":469,"y":172},{"type":"mousemove","time":57755,"x":311,"y":178},{"type":"mousemove","time":57972,"x":311,"y":178},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"10","time":58913,"target":"select"},{"time":58914,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":58942,"x":295,"y":172},{"type":"mousemove","time":59157,"x":283,"y":193},{"type":"mousemove","time":59371,"x":282,"y":177},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"9","time":60444,"target":"select"},{"time":60445,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":60487,"x":285,"y":174},{"type":"mousemove","time":60688,"x":296,"y":181},{"type":"mousemove","time":60905,"x":296,"y":178},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"8","time":61912,"target":"select"},{"time":61913,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":61955,"x":282,"y":168},{"type":"mousemove","time":62156,"x":278,"y":168},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"7","time":62994,"target":"select"},{"time":62995,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":63036,"x":276,"y":168},{"type":"mousemove","time":63238,"x":273,"y":175},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"6","time":64662,"target":"select"},{"time":64663,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":64690,"x":272,"y":157},{"type":"mousemove","time":64895,"x":266,"y":172},{"type":"mousemove","time":65106,"x":266,"y":174},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"5","time":65936,"target":"select"},{"time":65937,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":65973,"x":273,"y":164},{"type":"mousemove","time":66173,"x":267,"y":174},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"4","time":67061,"target":"select"},{"time":67062,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":67090,"x":272,"y":171},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"3","time":68112,"target":"select"},{"time":68113,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":68141,"x":280,"y":167},{"type":"mousemove","time":68355,"x":280,"y":167},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"2","time":69297,"target":"select"},{"time":69298,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":69360,"x":281,"y":159},{"type":"mousemove","time":69572,"x":274,"y":171},{"type":"mousemove","time":69791,"x":274,"y":171},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"1","time":70513,"target":"select"},{"time":70514,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":70543,"x":374,"y":156},{"type":"mousemove","time":70746,"x":491,"y":152},{"type":"mousemove","time":70956,"x":492,"y":152},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(3)>select.test-inputs-select-select","value":"4","time":71747,"target":"select"},{"time":71748,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":71763,"x":315,"y":179},{"type":"mousemove","time":71995,"x":277,"y":173},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"2","time":72922,"target":"select"},{"time":72923,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":72971,"x":311,"y":177},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"3","time":73948,"target":"select"},{"time":73949,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":73992,"x":314,"y":163},{"type":"mousemove","time":74193,"x":314,"y":163},{"type":"mousemove","time":74393,"x":306,"y":178},{"type":"mousemove","time":74607,"x":306,"y":178},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"4","time":75396,"target":"select"},{"time":75397,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":75427,"x":323,"y":172},{"type":"mousemove","time":75638,"x":323,"y":173},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"5","time":76379,"target":"select"},{"time":76380,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":76426,"x":323,"y":169},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"6","time":77447,"target":"select"},{"time":77448,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":77493,"x":322,"y":170},{"type":"mousemove","time":77693,"x":322,"y":179},{"type":"mousemove","time":77894,"x":321,"y":200},{"type":"mousemove","time":78093,"x":321,"y":200},{"type":"mousemove","time":78293,"x":322,"y":173},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"7","time":79231,"target":"select"},{"time":79232,"delay":400,"type":"screenshot-auto"},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"8","time":80379,"target":"select"},{"time":80380,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":80421,"x":316,"y":170},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"9","time":81447,"target":"select"},{"time":81448,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":81498,"x":319,"y":165},{"type":"mousemove","time":81709,"x":318,"y":165},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"10","time":82679,"target":"select"},{"time":82680,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":82727,"x":314,"y":178},{"type":"mousemove","time":82939,"x":314,"y":178},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"11","time":83781,"target":"select"},{"time":83782,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":83796,"x":382,"y":184},{"type":"mousemove","time":84009,"x":471,"y":181},{"type":"mousemove","time":84210,"x":480,"y":173},{"type":"mousemove","time":84410,"x":498,"y":158},{"type":"mousemove","time":84621,"x":511,"y":145},{"type":"mousemove","time":84859,"x":512,"y":145},{"type":"mousemove","time":85074,"x":514,"y":146},{"type":"mousemove","time":85194,"x":514,"y":146},{"type":"mousemove","time":85406,"x":514,"y":146},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(3)>select.test-inputs-select-select","value":"0","time":86595,"target":"select"},{"time":86596,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":86625,"x":726,"y":137},{"type":"mousemove","time":86826,"x":738,"y":143},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(4)>select.test-inputs-select-select","value":"2","time":88529,"target":"select"},{"time":88530,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":88548,"x":572,"y":198},{"type":"mousemove","time":88757,"x":241,"y":198},{"type":"mousemove","time":88859,"x":241,"y":197},{"type":"mousemove","time":89065,"x":267,"y":175},{"type":"mousemove","time":89274,"x":269,"y":174},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"10","time":90339,"target":"select"},{"time":90340,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":90374,"x":271,"y":169},{"type":"mousemove","time":90575,"x":271,"y":173},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"9","time":91447,"target":"select"},{"time":91448,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":91478,"x":264,"y":175},{"type":"mousemove","time":91690,"x":264,"y":175},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"8","time":92579,"target":"select"},{"time":92580,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":92611,"x":596,"y":154},{"type":"mousemove","time":92860,"x":797,"y":153},{"type":"mousemove","time":93074,"x":737,"y":143},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(4)>select.test-inputs-select-select","value":"1","time":94047,"target":"select"},{"time":94048,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":94078,"x":721,"y":145},{"type":"mousemove","time":94291,"x":720,"y":147},{"type":"mousemove","time":94506,"x":719,"y":150},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(4)>select.test-inputs-select-select","value":"2","time":95420,"target":"select"},{"time":95421,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":95440,"x":458,"y":160},{"type":"mousemove","time":95641,"x":309,"y":181},{"type":"mousemove","time":95860,"x":309,"y":172},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"7","time":96847,"target":"select"},{"time":96848,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":96893,"x":311,"y":166},{"type":"mousemove","time":97107,"x":309,"y":172},{"type":"mousemove","time":97325,"x":308,"y":173},{"type":"mousemove","time":98811,"x":622,"y":203},{"type":"mousemove","time":99011,"x":730,"y":141},{"type":"mousedown","time":99098,"x":731,"y":141},{"type":"mouseup","time":99198,"x":731,"y":141},{"time":99199,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":99257,"x":731,"y":141},{"type":"mousemove","time":99272,"x":731,"y":141},{"type":"mousemove","time":99476,"x":721,"y":154},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(4)>select.test-inputs-select-select","value":"1","time":100615,"target":"select"},{"time":100616,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":100677,"x":715,"y":151},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(4)>select.test-inputs-select-select","value":"2","time":101850,"target":"select"},{"time":101851,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":101883,"x":635,"y":167},{"type":"mousemove","time":102095,"x":312,"y":172},{"type":"mousemove","time":102307,"x":304,"y":172},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"6","time":103195,"target":"select"},{"time":103196,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":103242,"x":293,"y":169},{"type":"mousemove","time":103443,"x":292,"y":172},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"5","time":104589,"target":"select"},{"time":104590,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":104630,"x":294,"y":170},{"type":"mousemove","time":104839,"x":294,"y":173},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"4","time":105729,"target":"select"},{"time":105730,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":105771,"x":294,"y":176},{"type":"mousemove","time":105972,"x":293,"y":176},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"3","time":106817,"target":"select"},{"time":106818,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":106865,"x":296,"y":167},{"type":"mousemove","time":107074,"x":296,"y":168},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"2","time":107933,"target":"select"},{"time":107934,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":107967,"x":290,"y":162},{"type":"mousemove","time":108176,"x":284,"y":172},{"type":"mousemove","time":108391,"x":283,"y":174},{"type":"valuechange","selector":"#main_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"1","time":109214,"target":"select"},{"time":109215,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":109231,"x":293,"y":155},{"type":"mousemove","time":109440,"x":295,"y":156},{"type":"mousemove","time":110128,"x":295,"y":156},{"type":"mousemove","time":110342,"x":297,"y":156},{"type":"mousemove","time":110544,"x":351,"y":156},{"type":"mousemove","time":110754,"x":405,"y":166},{"type":"mousemove","time":110956,"x":408,"y":167},{"type":"mousemove","time":111144,"x":408,"y":167},{"type":"mousemove","time":111344,"x":408,"y":167},{"type":"mousemove","time":111557,"x":408,"y":166},{"type":"mousemove","time":111944,"x":408,"y":167},{"type":"mousedown","time":112066,"x":408,"y":167},{"type":"mouseup","time":112191,"x":408,"y":167},{"time":112192,"delay":400,"type":"screenshot-auto"}],"scrollY":671,"scrollX":0,"timestamp":1748968199670},{"name":"Action 5","ops":[{"type":"mousemove","time":704,"x":738,"y":219},{"type":"mousemove","time":917,"x":517,"y":196},{"type":"mousemove","time":1133,"x":407,"y":188},{"type":"mousemove","time":1236,"x":407,"y":188},{"type":"mousemove","time":1436,"x":263,"y":174},{"type":"mousemove","time":1647,"x":224,"y":162},{"type":"mousemove","time":1851,"x":216,"y":164},{"type":"mousemove","time":2320,"x":216,"y":164},{"type":"mousedown","time":2416,"x":216,"y":163},{"type":"mouseup","time":2535,"x":216,"y":163},{"time":2536,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":2575,"x":216,"y":163},{"type":"mousemove","time":2670,"x":217,"y":163},{"type":"mousemove","time":2870,"x":333,"y":162},{"type":"mousedown","time":2985,"x":343,"y":160},{"type":"mousemove","time":3082,"x":343,"y":160},{"type":"mouseup","time":3108,"x":343,"y":160},{"time":3109,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":3203,"x":346,"y":160},{"type":"mousemove","time":3404,"x":472,"y":167},{"type":"mousemove","time":3620,"x":518,"y":159},{"type":"mousedown","time":3792,"x":522,"y":157},{"type":"mousemove","time":3866,"x":522,"y":157},{"type":"mouseup","time":3896,"x":522,"y":157},{"time":3897,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":3986,"x":519,"y":157},{"type":"mousemove","time":4199,"x":306,"y":187},{"type":"mousemove","time":4402,"x":195,"y":193},{"type":"mousemove","time":4602,"x":189,"y":185},{"type":"mousedown","time":4694,"x":186,"y":182},{"type":"mouseup","time":4766,"x":186,"y":182},{"time":4767,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":4825,"x":186,"y":182},{"type":"mousemove","time":5069,"x":186,"y":182},{"type":"mousemove","time":5270,"x":136,"y":249},{"type":"mousedown","time":5404,"x":129,"y":260},{"type":"mousemove","time":5493,"x":129,"y":260},{"type":"mouseup","time":5536,"x":129,"y":260},{"time":5537,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":6004,"x":130,"y":260},{"type":"mousemove","time":6206,"x":209,"y":264},{"type":"mousedown","time":6286,"x":211,"y":264},{"type":"mouseup","time":6400,"x":211,"y":264},{"time":6401,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":6436,"x":211,"y":264},{"type":"mousemove","time":6652,"x":212,"y":264},{"type":"mousemove","time":6856,"x":360,"y":248},{"type":"mousedown","time":6919,"x":363,"y":248},{"type":"mouseup","time":7017,"x":363,"y":248},{"time":7018,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":7065,"x":363,"y":248},{"type":"mousemove","time":7203,"x":365,"y":248},{"type":"mousemove","time":7411,"x":579,"y":256},{"type":"mousedown","time":7478,"x":583,"y":257},{"type":"mouseup","time":7559,"x":583,"y":257},{"time":7560,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":7617,"x":608,"y":257},{"type":"mousemove","time":7820,"x":732,"y":250},{"type":"mousedown","time":7989,"x":755,"y":256},{"type":"mousemove","time":8036,"x":755,"y":256},{"type":"mouseup","time":8094,"x":755,"y":256},{"time":8095,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":8254,"x":755,"y":256},{"type":"mousedown","time":8459,"x":763,"y":319},{"type":"mousemove","time":8488,"x":763,"y":319},{"type":"mouseup","time":8566,"x":763,"y":319},{"time":8567,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":8637,"x":763,"y":319},{"type":"mousemove","time":8849,"x":754,"y":477},{"type":"mousemove","time":9051,"x":753,"y":489},{"type":"mousedown","time":9124,"x":753,"y":489},{"type":"mouseup","time":9235,"x":753,"y":489},{"time":9236,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":9387,"x":752,"y":494},{"type":"mousedown","time":9558,"x":744,"y":531},{"type":"mousemove","time":9604,"x":744,"y":531},{"type":"mouseup","time":9651,"x":744,"y":531},{"time":9652,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":9806,"x":704,"y":531},{"type":"mousedown","time":9907,"x":684,"y":532},{"type":"mouseup","time":10018,"x":683,"y":532},{"time":10019,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":10038,"x":683,"y":532},{"type":"mousemove","time":10245,"x":324,"y":531},{"type":"mousemove","time":10445,"x":251,"y":530},{"type":"mousedown","time":10588,"x":243,"y":529},{"type":"mouseup","time":10652,"x":243,"y":529},{"time":10653,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":10692,"x":243,"y":529},{"type":"mousemove","time":10869,"x":243,"y":529},{"type":"mousedown","time":11049,"x":184,"y":545},{"type":"mousemove","time":11099,"x":184,"y":545},{"type":"mouseup","time":11145,"x":184,"y":545},{"time":11146,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":11319,"x":177,"y":545},{"type":"mousedown","time":11534,"x":94,"y":543},{"type":"mousemove","time":11565,"x":94,"y":543},{"type":"mouseup","time":11677,"x":94,"y":543},{"time":11678,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":11745,"x":94,"y":536},{"type":"mousedown","time":11890,"x":94,"y":499},{"type":"mousemove","time":11954,"x":94,"y":499},{"type":"mouseup","time":12102,"x":94,"y":499},{"time":12103,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":12282,"x":94,"y":497},{"type":"mousedown","time":12432,"x":98,"y":429},{"type":"mousemove","time":12499,"x":98,"y":429},{"type":"mouseup","time":12547,"x":98,"y":429},{"time":12548,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":12836,"x":98,"y":428},{"type":"mousedown","time":13064,"x":119,"y":355},{"type":"mousemove","time":13078,"x":119,"y":355},{"type":"mouseup","time":13152,"x":119,"y":355},{"time":13153,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":13278,"x":228,"y":353},{"type":"mousedown","time":13345,"x":241,"y":353},{"type":"mouseup","time":13447,"x":241,"y":353},{"time":13448,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":13487,"x":244,"y":353},{"type":"mousemove","time":13717,"x":381,"y":326},{"type":"mousedown","time":13749,"x":381,"y":326},{"type":"mouseup","time":13862,"x":381,"y":326},{"time":13863,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":13937,"x":381,"y":326},{"type":"mousemove","time":14143,"x":251,"y":407},{"type":"mousedown","time":14354,"x":202,"y":439},{"type":"mousemove","time":14371,"x":201,"y":439},{"type":"mouseup","time":14484,"x":201,"y":439},{"time":14485,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":14580,"x":218,"y":438},{"type":"mousedown","time":14749,"x":294,"y":426},{"type":"mousemove","time":14803,"x":294,"y":426},{"type":"mouseup","time":14818,"x":294,"y":426},{"time":14819,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":14974,"x":294,"y":426},{"type":"mousedown","time":15136,"x":342,"y":425},{"type":"mousemove","time":15207,"x":343,"y":425},{"type":"mouseup","time":15253,"x":343,"y":425},{"time":15254,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":15424,"x":416,"y":425},{"type":"mousedown","time":15545,"x":428,"y":425},{"type":"mousemove","time":15644,"x":428,"y":425},{"type":"mouseup","time":15691,"x":428,"y":425},{"time":15692,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":15743,"x":428,"y":426},{"type":"mousemove","time":15949,"x":334,"y":447},{"type":"mousedown","time":16139,"x":327,"y":354},{"type":"mousemove","time":16185,"x":327,"y":354},{"type":"mouseup","time":16301,"x":327,"y":354},{"time":16302,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":16900,"x":326,"y":348},{"type":"mousemove","time":17102,"x":134,"y":169},{"type":"mousemove","time":17310,"x":135,"y":154},{"type":"mousemove","time":17524,"x":127,"y":130},{"type":"mousedown","time":17629,"x":124,"y":127},{"type":"mousemove","time":17741,"x":124,"y":127},{"type":"mouseup","time":17785,"x":124,"y":127},{"time":17786,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":17874,"x":124,"y":126},{"type":"mousemove","time":18079,"x":124,"y":120},{"type":"valuechange","selector":"#main_dim_data_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(3)>select.test-inputs-select-select","value":"1","time":18938,"target":"select"},{"time":18939,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":18956,"x":164,"y":135},{"type":"mousemove","time":19189,"x":294,"y":117},{"type":"mousemove","time":19418,"x":288,"y":113},{"type":"mousemove","time":19886,"x":288,"y":113},{"type":"mousemove","time":20101,"x":250,"y":221},{"type":"mousedown","time":20313,"x":246,"y":248},{"type":"mousemove","time":20363,"x":246,"y":248},{"type":"mouseup","time":20433,"x":246,"y":248},{"time":20434,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":20528,"x":245,"y":248},{"type":"mousemove","time":20736,"x":117,"y":242},{"type":"mousemove","time":20970,"x":116,"y":242},{"type":"mousedown","time":20986,"x":116,"y":242},{"type":"mouseup","time":21148,"x":116,"y":242},{"time":21149,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":21222,"x":117,"y":242},{"type":"mousemove","time":21422,"x":150,"y":237},{"type":"mousemove","time":21624,"x":80,"y":308},{"type":"mousedown","time":21677,"x":79,"y":308},{"type":"mouseup","time":21852,"x":79,"y":308},{"time":21853,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":21889,"x":79,"y":308},{"type":"mousemove","time":22291,"x":79,"y":307},{"type":"mousemove","time":22492,"x":75,"y":293},{"type":"mousemove","time":22704,"x":76,"y":456},{"type":"mousemove","time":22905,"x":89,"y":518},{"type":"mousedown","time":22936,"x":89,"y":518},{"type":"mouseup","time":23101,"x":89,"y":518},{"time":23102,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":23146,"x":89,"y":518},{"type":"mousemove","time":23304,"x":89,"y":522},{"type":"mousemove","time":23522,"x":90,"y":552},{"type":"mousedown","time":23554,"x":90,"y":552},{"type":"mouseup","time":23704,"x":90,"y":552},{"time":23705,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":23773,"x":92,"y":552},{"type":"mousedown","time":23958,"x":184,"y":554},{"type":"mousemove","time":23974,"x":184,"y":554},{"type":"mouseup","time":24087,"x":184,"y":554},{"time":24088,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":24176,"x":199,"y":554},{"type":"mousemove","time":24389,"x":292,"y":551},{"type":"mousemove","time":24553,"x":298,"y":550},{"type":"mousemove","time":24754,"x":548,"y":530},{"type":"mousemove","time":24954,"x":555,"y":540},{"type":"mousedown","time":25004,"x":560,"y":541},{"type":"mouseup","time":25089,"x":560,"y":542},{"time":25090,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":25158,"x":561,"y":539},{"type":"mousemove","time":25365,"x":685,"y":531},{"type":"mousemove","time":25566,"x":729,"y":522},{"type":"mousemove","time":25777,"x":730,"y":543},{"type":"mousedown","time":25828,"x":730,"y":543},{"type":"mouseup","time":25989,"x":730,"y":543},{"time":25990,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":26104,"x":730,"y":541},{"type":"mousemove","time":26312,"x":735,"y":512},{"type":"mousemove","time":26519,"x":752,"y":522},{"type":"mousemove","time":26724,"x":764,"y":518},{"type":"mousemove","time":26955,"x":761,"y":478},{"type":"mousemove","time":27155,"x":763,"y":504},{"type":"mousemove","time":27356,"x":765,"y":475},{"type":"mousedown","time":27471,"x":763,"y":439},{"type":"mousemove","time":27567,"x":763,"y":439},{"type":"mouseup","time":27622,"x":763,"y":439},{"time":27623,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":27770,"x":763,"y":438},{"type":"mousemove","time":27979,"x":753,"y":352},{"type":"mousedown","time":28013,"x":753,"y":352},{"type":"mouseup","time":28100,"x":753,"y":351},{"time":28101,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":28186,"x":753,"y":351},{"type":"mousemove","time":28288,"x":753,"y":351},{"type":"mousemove","time":28490,"x":759,"y":269},{"type":"mousedown","time":28532,"x":759,"y":269},{"type":"mouseup","time":28677,"x":759,"y":268},{"time":28678,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":28736,"x":759,"y":268},{"type":"mousemove","time":28770,"x":758,"y":268},{"type":"mousedown","time":28959,"x":702,"y":253},{"type":"mousemove","time":28977,"x":701,"y":253},{"type":"mouseup","time":29103,"x":701,"y":253},{"time":29104,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":29387,"x":698,"y":253},{"type":"mousedown","time":29596,"x":549,"y":245},{"type":"mousemove","time":29612,"x":549,"y":244},{"type":"mouseup","time":29718,"x":549,"y":244},{"time":29719,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":29872,"x":542,"y":249},{"type":"mousemove","time":30075,"x":358,"y":371},{"type":"mousedown","time":30110,"x":358,"y":371},{"type":"mouseup","time":30225,"x":358,"y":371},{"time":30226,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":30280,"x":361,"y":371},{"type":"mousedown","time":30401,"x":415,"y":371},{"type":"mouseup","time":30520,"x":415,"y":371},{"time":30521,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":30544,"x":383,"y":371},{"type":"mousedown","time":30746,"x":259,"y":371},{"type":"mousemove","time":30780,"x":259,"y":371},{"type":"mouseup","time":30871,"x":259,"y":371},{"time":30872,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":30911,"x":259,"y":372},{"type":"mousedown","time":31096,"x":259,"y":463},{"type":"mousemove","time":31114,"x":259,"y":464},{"type":"mouseup","time":31255,"x":259,"y":464},{"time":31256,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":31320,"x":214,"y":460},{"type":"mousedown","time":31462,"x":185,"y":460},{"type":"mousemove","time":31545,"x":185,"y":460},{"type":"mouseup","time":31575,"x":185,"y":460},{"time":31576,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":31755,"x":396,"y":438},{"type":"mousemove","time":31970,"x":739,"y":383},{"type":"mousemove","time":32180,"x":709,"y":367},{"type":"mousedown","time":32289,"x":689,"y":359},{"type":"mousemove","time":32398,"x":689,"y":359},{"type":"mouseup","time":32413,"x":689,"y":359},{"time":32414,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":32575,"x":687,"y":359},{"type":"mousemove","time":32790,"x":248,"y":110},{"type":"mousemove","time":32993,"x":249,"y":104},{"type":"mousemove","time":33205,"x":273,"y":114},{"type":"mousemove","time":33419,"x":274,"y":114},{"type":"valuechange","selector":"#main_dim_data_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(4)>select.test-inputs-select-select","value":"1","time":34730,"target":"select"},{"time":34731,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":34747,"x":277,"y":137},{"type":"mousemove","time":34959,"x":277,"y":138},{"type":"mousemove","time":35166,"x":247,"y":217},{"type":"mousemove","time":35375,"x":231,"y":236},{"type":"mousedown","time":35457,"x":229,"y":239},{"type":"mouseup","time":35556,"x":229,"y":239},{"time":35557,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":35611,"x":229,"y":239},{"type":"mousemove","time":35774,"x":233,"y":239},{"type":"mousemove","time":35989,"x":436,"y":257},{"type":"mousedown","time":36023,"x":436,"y":257},{"type":"mouseup","time":36140,"x":437,"y":257},{"time":36141,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":36190,"x":437,"y":257},{"type":"mousemove","time":36340,"x":437,"y":257},{"type":"mousemove","time":36555,"x":738,"y":250},{"type":"mousedown","time":36721,"x":747,"y":245},{"type":"mousemove","time":36771,"x":747,"y":245},{"type":"mouseup","time":36840,"x":747,"y":245},{"time":36841,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":36971,"x":747,"y":302},{"type":"mousemove","time":37183,"x":745,"y":362},{"type":"mousedown","time":37219,"x":745,"y":362},{"type":"mouseup","time":37326,"x":745,"y":362},{"time":37327,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":37408,"x":745,"y":362},{"type":"mousemove","time":37499,"x":742,"y":368},{"type":"mousemove","time":37725,"x":730,"y":463},{"type":"mousedown","time":37742,"x":730,"y":463},{"type":"mouseup","time":37854,"x":730,"y":463},{"time":37855,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":37916,"x":728,"y":472},{"type":"mousedown","time":38125,"x":653,"y":545},{"type":"mousemove","time":38141,"x":652,"y":545},{"type":"mouseup","time":38254,"x":652,"y":545},{"time":38255,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":38356,"x":698,"y":532},{"type":"mousedown","time":38566,"x":740,"y":528},{"type":"mousemove","time":38595,"x":740,"y":528},{"type":"mouseup","time":38727,"x":740,"y":528},{"time":38728,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":38753,"x":739,"y":528},{"type":"mousemove","time":38965,"x":135,"y":510},{"type":"mousemove","time":39166,"x":102,"y":530},{"type":"mousedown","time":39263,"x":96,"y":545},{"type":"mouseup","time":39358,"x":95,"y":545},{"time":39359,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":39410,"x":95,"y":545},{"type":"mousemove","time":39611,"x":175,"y":553},{"type":"mousedown","time":39646,"x":175,"y":553},{"type":"mouseup","time":39756,"x":175,"y":553},{"time":39757,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":39818,"x":135,"y":511},{"type":"mousedown","time":39977,"x":109,"y":447},{"type":"mousemove","time":40042,"x":109,"y":447},{"type":"mouseup","time":40095,"x":109,"y":447},{"time":40096,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":40248,"x":109,"y":447},{"type":"mousedown","time":40442,"x":141,"y":311},{"type":"mousemove","time":40499,"x":141,"y":311},{"type":"mouseup","time":40536,"x":141,"y":311},{"time":40537,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":40699,"x":213,"y":331},{"type":"mousedown","time":40744,"x":215,"y":333},{"type":"mouseup","time":40835,"x":215,"y":333},{"time":40836,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":40918,"x":246,"y":333},{"type":"mousedown","time":41045,"x":312,"y":337},{"type":"mouseup","time":41154,"x":312,"y":337},{"time":41155,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":41192,"x":312,"y":337},{"type":"mousemove","time":41318,"x":313,"y":338},{"type":"mousemove","time":41525,"x":370,"y":407},{"type":"mousedown","time":41747,"x":384,"y":441},{"type":"mousemove","time":41763,"x":384,"y":441},{"type":"mouseup","time":41818,"x":384,"y":441},{"time":41819,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":41981,"x":327,"y":351},{"type":"mousemove","time":42202,"x":289,"y":215},{"type":"mousemove","time":42313,"x":285,"y":213},{"type":"mousemove","time":42525,"x":145,"y":127},{"type":"mousemove","time":42736,"x":137,"y":115},{"type":"mousemove","time":42968,"x":132,"y":114},{"type":"valuechange","selector":"#main_dim_data_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(3)>select.test-inputs-select-select","value":"0","time":43852,"target":"select"},{"time":43853,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":43891,"x":210,"y":263},{"type":"mousemove","time":44115,"x":233,"y":283},{"type":"mousemove","time":44183,"x":236,"y":280},{"type":"mousemove","time":44386,"x":193,"y":347},{"type":"mousemove","time":44595,"x":174,"y":334},{"type":"mousedown","time":44817,"x":236,"y":315},{"type":"mousemove","time":44834,"x":236,"y":315},{"type":"mouseup","time":44867,"x":236,"y":315},{"time":44868,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":45036,"x":297,"y":266},{"type":"mousedown","time":45110,"x":302,"y":260},{"type":"mouseup","time":45222,"x":302,"y":260},{"time":45223,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":45245,"x":298,"y":260},{"type":"mousemove","time":45448,"x":129,"y":260},{"type":"mousedown","time":45545,"x":119,"y":260},{"type":"mouseup","time":45655,"x":119,"y":260},{"time":45656,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":45691,"x":119,"y":260},{"type":"mousemove","time":45751,"x":119,"y":260},{"type":"mousemove","time":45953,"x":107,"y":330},{"type":"mousedown","time":45990,"x":107,"y":330},{"type":"mouseup","time":46099,"x":107,"y":330},{"time":46100,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":46309,"x":107,"y":331},{"type":"mousemove","time":46510,"x":95,"y":483},{"type":"mousedown","time":46584,"x":95,"y":484},{"type":"mouseup","time":46693,"x":95,"y":484},{"time":46694,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":46749,"x":95,"y":484},{"type":"mousemove","time":46889,"x":95,"y":487},{"type":"mousemove","time":47090,"x":112,"y":569},{"type":"mousedown","time":47126,"x":112,"y":569},{"type":"mouseup","time":47310,"x":112,"y":569},{"time":47311,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":47480,"x":114,"y":569},{"type":"mousedown","time":47619,"x":190,"y":570},{"type":"mousemove","time":47755,"x":191,"y":570},{"type":"mouseup","time":47776,"x":191,"y":570},{"time":47777,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":48005,"x":486,"y":570},{"type":"mousemove","time":48213,"x":665,"y":551},{"type":"mousemove","time":48417,"x":729,"y":534},{"type":"mousemove","time":48621,"x":746,"y":545},{"type":"mousedown","time":48660,"x":747,"y":546},{"type":"mouseup","time":48754,"x":747,"y":546},{"time":48755,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":48835,"x":747,"y":525},{"type":"mousedown","time":48991,"x":755,"y":460},{"type":"mousemove","time":49042,"x":755,"y":460},{"type":"mouseup","time":49165,"x":755,"y":460},{"time":49166,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":49382,"x":755,"y":459},{"type":"mousedown","time":49600,"x":758,"y":395},{"type":"mousemove","time":49617,"x":758,"y":394},{"type":"mouseup","time":49721,"x":758,"y":394},{"time":49722,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":49824,"x":758,"y":384},{"type":"mousedown","time":50006,"x":758,"y":349},{"type":"mousemove","time":50024,"x":758,"y":349},{"type":"mouseup","time":50183,"x":758,"y":349},{"time":50184,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":50407,"x":758,"y":344},{"type":"mousedown","time":50530,"x":758,"y":303},{"type":"mousemove","time":50626,"x":758,"y":303},{"type":"mouseup","time":50659,"x":758,"y":303},{"time":50660,"delay":400,"type":"screenshot-auto"},{"type":"mousedown","time":50856,"x":758,"y":271},{"type":"mousemove","time":50927,"x":758,"y":271},{"type":"mouseup","time":51084,"x":758,"y":271},{"time":51085,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":51169,"x":751,"y":271},{"type":"mousemove","time":51372,"x":692,"y":246},{"type":"mousedown","time":51418,"x":690,"y":246},{"type":"mouseup","time":51525,"x":690,"y":246},{"time":51526,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":51588,"x":690,"y":246},{"type":"mousemove","time":51709,"x":689,"y":246},{"type":"mousedown","time":51914,"x":405,"y":247},{"type":"mousemove","time":51932,"x":405,"y":247},{"type":"mouseup","time":52025,"x":405,"y":247},{"time":52026,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":52139,"x":356,"y":331},{"type":"mousedown","time":52255,"x":348,"y":341},{"type":"mousemove","time":52377,"x":348,"y":341},{"type":"mouseup","time":52437,"x":348,"y":341},{"time":52438,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":52621,"x":348,"y":340},{"type":"mousemove","time":52822,"x":361,"y":304},{"type":"mousemove","time":53025,"x":461,"y":316},{"type":"mousedown","time":53211,"x":500,"y":322},{"type":"mousemove","time":53235,"x":500,"y":322},{"type":"mouseup","time":53339,"x":500,"y":322},{"time":53340,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":53448,"x":491,"y":355},{"type":"mousedown","time":53554,"x":481,"y":378},{"type":"mousemove","time":53667,"x":481,"y":378},{"type":"mouseup","time":53685,"x":481,"y":378},{"time":53686,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":53890,"x":329,"y":374},{"type":"mousedown","time":53972,"x":321,"y":374},{"type":"mouseup","time":54104,"x":321,"y":374},{"time":54105,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":54145,"x":321,"y":374},{"type":"mousedown","time":54293,"x":321,"y":441},{"type":"mousemove","time":54390,"x":321,"y":441},{"type":"mouseup","time":54408,"x":321,"y":441},{"time":54409,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":54607,"x":315,"y":445},{"type":"mousemove","time":54819,"x":233,"y":478},{"type":"mousedown","time":54897,"x":227,"y":479},{"type":"mouseup","time":55028,"x":227,"y":479},{"time":55029,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":55069,"x":227,"y":479},{"type":"mousemove","time":55279,"x":518,"y":472},{"type":"mousedown","time":55418,"x":567,"y":489},{"type":"mousemove","time":55492,"x":567,"y":489},{"type":"mouseup","time":55563,"x":567,"y":489},{"time":55564,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":56180,"x":569,"y":479},{"type":"mousemove","time":56391,"x":516,"y":131},{"type":"mousemove","time":56593,"x":511,"y":132},{"type":"mousemove","time":56838,"x":518,"y":111},{"type":"valuechange","selector":"#main_dim_data_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(5)>select.test-inputs-select-select","value":"1","time":59860,"target":"select"},{"time":59861,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":59878,"x":510,"y":137},{"type":"mousemove","time":60084,"x":435,"y":185},{"type":"mousemove","time":60286,"x":365,"y":239},{"type":"mousedown","time":62504,"x":365,"y":239},{"type":"mouseup","time":62605,"x":365,"y":239},{"time":62606,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":62772,"x":363,"y":239},{"type":"mousemove","time":62987,"x":176,"y":252},{"type":"mousemove","time":63197,"x":151,"y":249},{"type":"mousedown","time":63367,"x":135,"y":247},{"type":"mousemove","time":63418,"x":134,"y":247},{"type":"mouseup","time":63457,"x":134,"y":247},{"time":63458,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":63826,"x":132,"y":256},{"type":"mousedown","time":64042,"x":109,"y":307},{"type":"mousemove","time":64074,"x":109,"y":307},{"type":"mouseup","time":64141,"x":109,"y":307},{"time":64142,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":64349,"x":108,"y":315},{"type":"mousedown","time":64576,"x":104,"y":368},{"type":"mousemove","time":64609,"x":104,"y":368},{"type":"mouseup","time":64697,"x":104,"y":368},{"time":64698,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":64918,"x":103,"y":374},{"type":"mousemove","time":65118,"x":93,"y":497},{"type":"mousemove","time":65329,"x":87,"y":540},{"type":"mousedown","time":65435,"x":90,"y":543},{"type":"mouseup","time":65526,"x":90,"y":543},{"time":65527,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":65580,"x":90,"y":543},{"type":"mousemove","time":65680,"x":92,"y":543},{"type":"mousedown","time":65881,"x":233,"y":543},{"type":"mousemove","time":65897,"x":234,"y":543},{"type":"mouseup","time":65962,"x":234,"y":543},{"time":65963,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":66121,"x":238,"y":543},{"type":"mousemove","time":66330,"x":628,"y":527},{"type":"mousemove","time":66536,"x":722,"y":524},{"type":"mousemove","time":66742,"x":757,"y":523},{"type":"mousedown","time":66781,"x":757,"y":523},{"type":"mouseup","time":66894,"x":757,"y":523},{"time":66895,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":66949,"x":757,"y":523},{"type":"mousemove","time":67034,"x":757,"y":522},{"type":"mousedown","time":67234,"x":757,"y":478},{"type":"mousemove","time":67251,"x":757,"y":478},{"type":"mouseup","time":67417,"x":757,"y":478},{"time":67418,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":67652,"x":757,"y":477},{"type":"mousedown","time":67857,"x":755,"y":367},{"type":"mousemove","time":67876,"x":755,"y":366},{"type":"mouseup","time":68011,"x":755,"y":366},{"time":68012,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":68091,"x":755,"y":338},{"type":"mousemove","time":68305,"x":751,"y":253},{"type":"mousedown","time":68343,"x":751,"y":253},{"type":"mouseup","time":68501,"x":751,"y":253},{"time":68502,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":68526,"x":723,"y":253},{"type":"mousedown","time":68659,"x":661,"y":253},{"type":"mousemove","time":68750,"x":661,"y":253},{"type":"mouseup","time":68767,"x":661,"y":253},{"time":68768,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":69036,"x":661,"y":252},{"type":"mousemove","time":69264,"x":445,"y":152},{"type":"mousemove","time":69616,"x":450,"y":152},{"type":"mousemove","time":69822,"x":529,"y":117},{"type":"mousemove","time":70050,"x":529,"y":115},{"type":"mousemove","time":70271,"x":529,"y":114},{"type":"mousemove","time":70537,"x":528,"y":114},{"type":"mousemove","time":70759,"x":528,"y":114},{"type":"mousemove","time":70964,"x":440,"y":120},{"type":"mousemove","time":71173,"x":260,"y":138},{"type":"mousemove","time":71377,"x":218,"y":114},{"type":"mousemove","time":71579,"x":195,"y":113},{"type":"mousemove","time":71727,"x":192,"y":113},{"type":"mousemove","time":71947,"x":137,"y":127},{"type":"mousemove","time":72159,"x":132,"y":118},{"type":"mousemove","time":72394,"x":132,"y":117},{"type":"mousemove","time":74652,"x":286,"y":111},{"type":"valuechange","selector":"#main_dim_data_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(4)>select.test-inputs-select-select","value":"0","time":76074,"target":"select"},{"time":76075,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":76092,"x":287,"y":102},{"type":"mousemove","time":76771,"x":287,"y":102},{"type":"mousemove","time":76976,"x":77,"y":380},{"type":"mousedown","time":77160,"x":66,"y":389},{"type":"mousemove","time":77208,"x":66,"y":389},{"type":"mouseup","time":77226,"x":66,"y":389},{"time":77227,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":77424,"x":75,"y":380},{"type":"mousemove","time":77632,"x":244,"y":262},{"type":"mousedown","time":77673,"x":244,"y":261},{"type":"mouseup","time":77802,"x":244,"y":261},{"time":77803,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":77847,"x":218,"y":294},{"type":"mousemove","time":78066,"x":157,"y":433},{"type":"mousemove","time":78273,"x":217,"y":370},{"type":"mousedown","time":78396,"x":222,"y":356},{"type":"mouseup","time":78481,"x":222,"y":356},{"time":78482,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":78528,"x":222,"y":356},{"type":"mousemove","time":78682,"x":222,"y":356},{"type":"mousemove","time":78901,"x":326,"y":347},{"type":"mousemove","time":79105,"x":344,"y":336},{"type":"mousedown","time":79218,"x":332,"y":333},{"type":"mouseup","time":79297,"x":332,"y":333},{"time":79298,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":79324,"x":332,"y":334},{"type":"mousemove","time":79535,"x":332,"y":375},{"type":"mousedown","time":79597,"x":332,"y":376},{"type":"mouseup","time":79724,"x":332,"y":376},{"time":79725,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":79749,"x":328,"y":376},{"type":"mousedown","time":79935,"x":253,"y":428},{"type":"mousemove","time":79987,"x":253,"y":428},{"type":"mouseup","time":80020,"x":253,"y":428},{"time":80021,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":80201,"x":246,"y":439},{"type":"mousedown","time":80407,"x":169,"y":525},{"type":"mousemove","time":80427,"x":169,"y":525},{"type":"mouseup","time":80547,"x":169,"y":525},{"time":80548,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":80635,"x":246,"y":525},{"type":"mousedown","time":80789,"x":334,"y":526},{"type":"mouseup","time":80867,"x":335,"y":526},{"time":80868,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":80916,"x":335,"y":526},{"type":"mousemove","time":81031,"x":335,"y":526},{"type":"mousemove","time":81237,"x":594,"y":531},{"type":"mousedown","time":81291,"x":601,"y":532},{"type":"mouseup","time":81418,"x":601,"y":532},{"time":81419,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":81442,"x":603,"y":532},{"type":"mousemove","time":81658,"x":713,"y":527},{"type":"mousedown","time":81854,"x":733,"y":528},{"type":"mousemove","time":81872,"x":733,"y":529},{"type":"mouseup","time":82030,"x":733,"y":529},{"time":82031,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":82077,"x":734,"y":524},{"type":"mousemove","time":82278,"x":756,"y":451},{"type":"mousedown","time":82321,"x":756,"y":449},{"type":"mouseup","time":82487,"x":755,"y":448},{"time":82488,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":82512,"x":755,"y":412},{"type":"mousedown","time":82674,"x":752,"y":321},{"type":"mousemove","time":82732,"x":752,"y":321},{"type":"mouseup","time":82811,"x":752,"y":321},{"time":82812,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":83007,"x":752,"y":321},{"type":"mousedown","time":83193,"x":766,"y":252},{"type":"mousemove","time":83213,"x":766,"y":252},{"type":"mouseup","time":83312,"x":766,"y":252},{"time":83313,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":83429,"x":646,"y":252},{"type":"mousedown","time":83495,"x":638,"y":252},{"type":"mouseup","time":83612,"x":638,"y":252},{"time":83613,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":83673,"x":638,"y":252},{"type":"mousemove","time":83893,"x":626,"y":247},{"type":"mousemove","time":84130,"x":465,"y":138},{"type":"mousemove","time":84489,"x":463,"y":138},{"type":"mousemove","time":84719,"x":397,"y":134},{"type":"mousemove","time":84938,"x":506,"y":116},{"type":"mousemove","time":85143,"x":508,"y":115},{"type":"valuechange","selector":"#main_dim_data_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(5)>select.test-inputs-select-select","value":"2","time":86781,"target":"select"},{"time":86782,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":86802,"x":420,"y":355},{"type":"mousemove","time":87005,"x":244,"y":340},{"type":"mousedown","time":87073,"x":243,"y":340},{"type":"mouseup","time":87168,"x":243,"y":340},{"time":87169,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":87210,"x":244,"y":340},{"type":"mousedown","time":87393,"x":353,"y":321},{"type":"mousemove","time":87413,"x":354,"y":321},{"type":"mouseup","time":87491,"x":354,"y":321},{"time":87492,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":87627,"x":354,"y":356},{"type":"mousedown","time":87774,"x":353,"y":374},{"type":"mousemove","time":87852,"x":353,"y":374},{"type":"mouseup","time":87878,"x":353,"y":374},{"time":87879,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":88037,"x":353,"y":375},{"type":"mousemove","time":88238,"x":342,"y":419},{"type":"mousedown","time":88279,"x":341,"y":423},{"type":"mouseup","time":88363,"x":341,"y":423},{"time":88364,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":88455,"x":341,"y":466},{"type":"mousedown","time":88587,"x":341,"y":488},{"type":"mousemove","time":88662,"x":341,"y":488},{"type":"mouseup","time":88741,"x":341,"y":488},{"time":88742,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":88765,"x":341,"y":488},{"type":"mousedown","time":88948,"x":242,"y":488},{"type":"mousemove","time":89006,"x":242,"y":488},{"type":"mouseup","time":89062,"x":242,"y":488},{"time":89063,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":89214,"x":415,"y":541},{"type":"mousedown","time":89358,"x":461,"y":544},{"type":"mousemove","time":89440,"x":461,"y":544},{"type":"mouseup","time":89519,"x":461,"y":544},{"time":89520,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":89699,"x":462,"y":544},{"type":"mousemove","time":89924,"x":702,"y":536},{"type":"mousedown","time":90126,"x":760,"y":537},{"type":"mousemove","time":90144,"x":760,"y":537},{"type":"mouseup","time":90267,"x":760,"y":537},{"time":90268,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":90352,"x":760,"y":430},{"type":"mousedown","time":90436,"x":760,"y":416},{"type":"mouseup","time":90524,"x":760,"y":416},{"time":90525,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":90585,"x":760,"y":416},{"type":"mousemove","time":90667,"x":760,"y":407},{"type":"mousemove","time":90873,"x":756,"y":228},{"type":"mousedown","time":90913,"x":756,"y":227},{"type":"mouseup","time":91062,"x":756,"y":227},{"time":91063,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":91121,"x":756,"y":227},{"type":"mousemove","time":91283,"x":734,"y":227},{"type":"mousemove","time":91501,"x":168,"y":223},{"type":"mousemove","time":91704,"x":135,"y":228},{"type":"mousedown","time":91743,"x":135,"y":228},{"type":"mouseup","time":91865,"x":135,"y":228},{"time":91866,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":91929,"x":135,"y":228},{"type":"mousemove","time":92070,"x":118,"y":270},{"type":"mousedown","time":92231,"x":96,"y":333},{"type":"mousemove","time":92295,"x":96,"y":334},{"type":"mouseup","time":92314,"x":96,"y":334},{"time":92315,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":92484,"x":96,"y":341},{"type":"mousemove","time":92690,"x":75,"y":524},{"type":"mousedown","time":92828,"x":72,"y":554},{"type":"mousemove","time":92910,"x":72,"y":554},{"type":"mouseup","time":92949,"x":72,"y":554},{"time":92950,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":93158,"x":75,"y":554},{"type":"mousedown","time":93363,"x":205,"y":558},{"type":"mousemove","time":93383,"x":206,"y":558},{"type":"mouseup","time":93470,"x":206,"y":558},{"time":93471,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":93602,"x":206,"y":551},{"type":"mousedown","time":93790,"x":193,"y":442},{"type":"mousemove","time":93850,"x":193,"y":442},{"type":"mouseup","time":93903,"x":193,"y":442},{"time":93904,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":94076,"x":238,"y":431},{"type":"mousedown","time":94240,"x":251,"y":424},{"type":"mousemove","time":94301,"x":251,"y":424},{"type":"mouseup","time":94367,"x":251,"y":424},{"time":94368,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":94514,"x":251,"y":470},{"type":"mousedown","time":94609,"x":251,"y":476},{"type":"mouseup","time":94730,"x":251,"y":476},{"time":94731,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":94760,"x":251,"y":476},{"type":"mousemove","time":94987,"x":252,"y":471},{"type":"mousemove","time":95195,"x":250,"y":217},{"type":"mousemove","time":95403,"x":324,"y":182},{"type":"mousemove","time":95609,"x":315,"y":121},{"type":"mousemove","time":95819,"x":561,"y":163},{"type":"mousemove","time":96020,"x":564,"y":143},{"type":"mousemove","time":96227,"x":537,"y":119},{"type":"mousemove","time":99312,"x":305,"y":142},{"type":"mousemove","time":99521,"x":286,"y":128},{"type":"mousemove","time":99784,"x":275,"y":116},{"type":"valuechange","selector":"#main_dim_data_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(4)>select.test-inputs-select-select","value":"1","time":100729,"target":"select"},{"time":100730,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":100761,"x":221,"y":133},{"type":"mousemove","time":100979,"x":137,"y":113},{"type":"mousemove","time":101213,"x":136,"y":112},{"type":"mousemove","time":101414,"x":140,"y":165},{"type":"mousemove","time":101643,"x":131,"y":250},{"type":"mousemove","time":101844,"x":127,"y":131},{"type":"mousemove","time":102055,"x":130,"y":119},{"type":"mousemove","time":102272,"x":129,"y":128},{"type":"mousemove","time":102492,"x":123,"y":383},{"type":"mousemove","time":102705,"x":123,"y":383},{"type":"mousemove","time":102758,"x":123,"y":383},{"type":"mousedown","time":102822,"x":123,"y":382},{"type":"mouseup","time":102964,"x":123,"y":381},{"time":102965,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":103011,"x":123,"y":381},{"type":"mousemove","time":103219,"x":127,"y":277},{"type":"mousedown","time":103330,"x":117,"y":274},{"type":"mousemove","time":103422,"x":117,"y":274},{"type":"mouseup","time":103493,"x":117,"y":274},{"time":103494,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":103633,"x":203,"y":251},{"type":"mousedown","time":103719,"x":216,"y":251},{"type":"mouseup","time":103834,"x":216,"y":251},{"time":103835,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":103873,"x":216,"y":251},{"type":"mousemove","time":104121,"x":216,"y":262},{"type":"mousemove","time":104327,"x":214,"y":390},{"type":"mousedown","time":104375,"x":214,"y":389},{"type":"mouseup","time":104473,"x":214,"y":389},{"time":104474,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":104562,"x":214,"y":389},{"type":"mousemove","time":104711,"x":213,"y":391},{"type":"mousedown","time":104866,"x":175,"y":440},{"type":"mousemove","time":104941,"x":175,"y":441},{"type":"mouseup","time":104982,"x":175,"y":441},{"time":104983,"delay":400,"type":"screenshot-auto"},{"type":"mousedown","time":105175,"x":161,"y":561},{"type":"mousemove","time":105229,"x":161,"y":561},{"type":"mouseup","time":105319,"x":161,"y":561},{"time":105320,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":105397,"x":152,"y":557},{"type":"mousedown","time":105535,"x":83,"y":540},{"type":"mousemove","time":105619,"x":82,"y":540},{"type":"mouseup","time":105656,"x":82,"y":540},{"time":105657,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":105831,"x":720,"y":540},{"type":"mousemove","time":106062,"x":796,"y":540},{"type":"mousemove","time":106247,"x":774,"y":552},{"type":"mousedown","time":106380,"x":762,"y":542},{"type":"mousemove","time":106466,"x":762,"y":542},{"type":"mouseup","time":106522,"x":762,"y":542},{"time":106523,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":106670,"x":762,"y":445},{"type":"mousedown","time":106713,"x":762,"y":445},{"type":"mousemove","time":106883,"x":762,"y":429},{"type":"mouseup","time":106906,"x":762,"y":429},{"time":106907,"delay":400,"type":"screenshot-auto"},{"type":"mousedown","time":106991,"x":762,"y":384},{"type":"mousemove","time":107100,"x":762,"y":382},{"type":"mouseup","time":107178,"x":762,"y":367},{"time":107179,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":107303,"x":762,"y":367},{"type":"mousemove","time":107379,"x":762,"y":367},{"type":"mousedown","time":107591,"x":753,"y":281},{"type":"mousemove","time":107630,"x":753,"y":281},{"type":"mouseup","time":107737,"x":753,"y":281},{"time":107738,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":107890,"x":747,"y":279},{"type":"mousemove","time":108108,"x":149,"y":70},{"type":"mousemove","time":108317,"x":117,"y":91},{"type":"valuechange","selector":"#main_dim_data_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(3)>select.test-inputs-select-select","value":"1","time":109827,"target":"select"},{"time":109828,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":109850,"x":129,"y":185},{"type":"mousemove","time":110073,"x":189,"y":488},{"type":"mousemove","time":110312,"x":169,"y":518},{"type":"mousemove","time":110458,"x":168,"y":518},{"type":"mousedown","time":110503,"x":167,"y":518},{"type":"mouseup","time":110698,"x":167,"y":518},{"time":110699,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":110742,"x":167,"y":518},{"type":"mousemove","time":110817,"x":166,"y":518},{"type":"mousedown","time":110860,"x":166,"y":518},{"type":"mousemove","time":111054,"x":165,"y":518},{"type":"mouseup","time":111152,"x":165,"y":518},{"time":111153,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":111347,"x":160,"y":518},{"type":"mousedown","time":111472,"x":126,"y":523},{"type":"mousemove","time":111572,"x":126,"y":523},{"type":"mouseup","time":111661,"x":125,"y":519},{"time":111662,"delay":400,"type":"screenshot-auto"},{"type":"mousedown","time":111779,"x":125,"y":445},{"type":"mousemove","time":111849,"x":125,"y":445},{"type":"mouseup","time":111871,"x":125,"y":445},{"time":111872,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":112013,"x":125,"y":445},{"type":"mousemove","time":112220,"x":166,"y":254},{"type":"mousemove","time":112424,"x":136,"y":244},{"type":"mousedown","time":112468,"x":136,"y":244},{"type":"mouseup","time":112581,"x":136,"y":244},{"time":112582,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":112628,"x":171,"y":244},{"type":"mousedown","time":112753,"x":246,"y":244},{"type":"mousemove","time":112836,"x":246,"y":244},{"type":"mouseup","time":112856,"x":246,"y":244},{"time":112857,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":113010,"x":250,"y":244},{"type":"mousemove","time":113224,"x":748,"y":280},{"type":"mousemove","time":113441,"x":747,"y":243},{"type":"mousedown","time":113483,"x":747,"y":242},{"type":"mouseup","time":113586,"x":747,"y":241},{"time":113587,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":113645,"x":747,"y":241},{"type":"mousemove","time":113762,"x":747,"y":243},{"type":"mousemove","time":113966,"x":763,"y":302},{"type":"mousedown","time":114166,"x":738,"y":428},{"type":"mousemove","time":114186,"x":738,"y":429},{"type":"mouseup","time":114276,"x":738,"y":429},{"time":114277,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":114481,"x":737,"y":450},{"type":"mousedown","time":114621,"x":739,"y":490},{"type":"mousemove","time":114704,"x":739,"y":490},{"type":"mouseup","time":114724,"x":739,"y":490},{"time":114725,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":114916,"x":686,"y":540},{"type":"mousedown","time":114966,"x":685,"y":540},{"type":"mouseup","time":115072,"x":685,"y":540},{"time":115073,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":115121,"x":651,"y":532},{"type":"mousedown","time":115274,"x":550,"y":439},{"type":"mousemove","time":115339,"x":550,"y":439},{"type":"mouseup","time":115411,"x":550,"y":439},{"time":115412,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":115562,"x":542,"y":439},{"type":"mousedown","time":115710,"x":337,"y":434},{"type":"mousemove","time":115791,"x":337,"y":434},{"type":"mouseup","time":115817,"x":337,"y":434},{"time":115818,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":115965,"x":336,"y":426},{"type":"mousemove","time":116183,"x":150,"y":92},{"type":"valuechange","selector":"#main_dim_data_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(3)>select.test-inputs-select-select","value":"0","time":117451,"target":"select"},{"time":117452,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":117480,"x":277,"y":137},{"type":"mousemove","time":117689,"x":297,"y":121},{"type":"mousemove","time":117943,"x":298,"y":119},{"type":"mousemove","time":117964,"x":298,"y":119},{"type":"mousemove","time":118180,"x":283,"y":118},{"type":"mousemove","time":118407,"x":282,"y":118},{"type":"valuechange","selector":"#main_dim_data_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(4)>select.test-inputs-select-select","value":"0","time":119396,"target":"select"},{"time":119397,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":119420,"x":143,"y":102},{"type":"mousemove","time":119625,"x":108,"y":108},{"type":"valuechange","selector":"#main_dim_data_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(3)>select.test-inputs-select-select","value":"1","time":120576,"target":"select"},{"time":120577,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":120601,"x":99,"y":234},{"type":"mousedown","time":120777,"x":66,"y":414},{"type":"mousemove","time":120821,"x":66,"y":414},{"type":"mouseup","time":120906,"x":66,"y":414},{"time":120907,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":121090,"x":66,"y":422},{"type":"mousemove","time":121299,"x":70,"y":517},{"type":"mousedown","time":121457,"x":75,"y":541},{"type":"mouseup","time":121536,"x":75,"y":542},{"time":121537,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":121586,"x":75,"y":542},{"type":"mousemove","time":121701,"x":83,"y":542},{"type":"mousedown","time":121858,"x":199,"y":549},{"type":"mouseup","time":121937,"x":200,"y":549},{"time":121938,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":121984,"x":200,"y":549},{"type":"mousemove","time":122197,"x":432,"y":549},{"type":"mousedown","time":122241,"x":432,"y":549},{"type":"mouseup","time":122345,"x":432,"y":549},{"time":122346,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":122400,"x":432,"y":530},{"type":"mousedown","time":122589,"x":414,"y":419},{"type":"mousemove","time":122617,"x":413,"y":419},{"type":"mouseup","time":122702,"x":413,"y":419},{"time":122703,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":122858,"x":406,"y":420},{"type":"mousedown","time":123102,"x":316,"y":345},{"type":"mousemove","time":123123,"x":316,"y":345},{"type":"mouseup","time":123184,"x":316,"y":345},{"time":123185,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":123360,"x":315,"y":345},{"type":"mousemove","time":123569,"x":110,"y":234},{"type":"mousedown","time":123634,"x":108,"y":234},{"type":"mouseup","time":123751,"x":108,"y":234},{"time":123752,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":123779,"x":132,"y":234},{"type":"mousedown","time":123936,"x":375,"y":234},{"type":"mousemove","time":124021,"x":376,"y":234},{"type":"mouseup","time":124042,"x":376,"y":234},{"time":124043,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":124176,"x":377,"y":234},{"type":"mousemove","time":124388,"x":732,"y":266},{"type":"mousemove","time":124600,"x":764,"y":258},{"type":"mousedown","time":124642,"x":764,"y":258},{"type":"mouseup","time":124718,"x":764,"y":257},{"time":124719,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":124801,"x":764,"y":257},{"type":"mousemove","time":124896,"x":763,"y":267},{"type":"mousedown","time":125053,"x":754,"y":375},{"type":"mousemove","time":125133,"x":754,"y":375},{"type":"mouseup","time":125158,"x":754,"y":375},{"time":125159,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":125303,"x":754,"y":376},{"type":"mousemove","time":125521,"x":751,"y":461},{"type":"mousedown","time":125570,"x":751,"y":461},{"type":"mouseup","time":125614,"x":751,"y":461},{"time":125615,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":125764,"x":751,"y":461},{"type":"mousemove","time":125981,"x":737,"y":527},{"type":"mousedown","time":126025,"x":737,"y":527},{"type":"mouseup","time":126132,"x":737,"y":527},{"time":126133,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":126186,"x":725,"y":527},{"type":"mousedown","time":126326,"x":626,"y":528},{"type":"mousemove","time":126421,"x":625,"y":528},{"type":"mouseup","time":126443,"x":625,"y":528},{"time":126444,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":126622,"x":556,"y":447},{"type":"mousedown","time":126668,"x":556,"y":446},{"type":"mouseup","time":126775,"x":555,"y":446},{"time":126776,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":126851,"x":555,"y":446},{"type":"mousemove","time":127001,"x":555,"y":446},{"type":"mousemove","time":127211,"x":442,"y":184},{"type":"mousemove","time":127412,"x":511,"y":132},{"type":"mousemove","time":127640,"x":535,"y":123},{"type":"mousemove","time":127846,"x":549,"y":115},{"type":"mousemove","time":128076,"x":503,"y":103},{"type":"mousemove","time":128246,"x":503,"y":103},{"type":"mousemove","time":128461,"x":495,"y":93},{"type":"valuechange","selector":"#main_dim_data_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select>select.test-inputs-select-select","value":"1","time":129600,"target":"select"},{"time":129601,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":129622,"x":476,"y":125},{"type":"mousemove","time":130211,"x":472,"y":126},{"type":"mousemove","time":130411,"x":249,"y":253},{"type":"mousemove","time":130634,"x":239,"y":317},{"type":"mousemove","time":130845,"x":235,"y":324},{"type":"mousedown","time":131005,"x":235,"y":324},{"type":"mouseup","time":131092,"x":235,"y":324},{"time":131093,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":131121,"x":234,"y":324},{"type":"mousedown","time":131347,"x":93,"y":344},{"type":"mousemove","time":131388,"x":93,"y":344},{"type":"mouseup","time":131436,"x":93,"y":344},{"time":131437,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":131547,"x":100,"y":338},{"type":"mousemove","time":131768,"x":246,"y":247},{"type":"mousedown","time":131868,"x":256,"y":239},{"type":"mouseup","time":131955,"x":256,"y":239},{"time":131956,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":132020,"x":256,"y":239},{"type":"mousemove","time":132113,"x":255,"y":239},{"type":"mousemove","time":132334,"x":118,"y":230},{"type":"mousedown","time":132381,"x":117,"y":230},{"type":"mouseup","time":132493,"x":117,"y":230},{"time":132494,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":132544,"x":150,"y":230},{"type":"mousemove","time":132766,"x":728,"y":284},{"type":"mousemove","time":132966,"x":733,"y":241},{"type":"mousedown","time":133030,"x":733,"y":241},{"type":"mouseup","time":133139,"x":733,"y":241},{"time":133140,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":133188,"x":733,"y":246},{"type":"mousedown","time":133370,"x":737,"y":359},{"type":"mousemove","time":133393,"x":737,"y":359},{"type":"mouseup","time":133454,"x":737,"y":359},{"time":133455,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":133607,"x":737,"y":360},{"type":"mousemove","time":133818,"x":717,"y":494},{"type":"mousedown","time":134057,"x":732,"y":527},{"type":"mousemove","time":134077,"x":732,"y":527},{"type":"mouseup","time":134161,"x":732,"y":527},{"time":134162,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":134279,"x":661,"y":534},{"type":"mousedown","time":134353,"x":639,"y":544},{"type":"mouseup","time":134439,"x":639,"y":544},{"time":134440,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":134505,"x":639,"y":544},{"type":"mousemove","time":134620,"x":633,"y":544},{"type":"mousemove","time":134822,"x":141,"y":516},{"type":"mousedown","time":135042,"x":111,"y":529},{"type":"mousemove","time":135061,"x":111,"y":529},{"type":"mouseup","time":135102,"x":111,"y":529},{"time":135103,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":135324,"x":184,"y":453},{"type":"mousemove","time":135531,"x":567,"y":111},{"type":"mousemove","time":135744,"x":572,"y":101},{"type":"mousemove","time":135948,"x":527,"y":115},{"type":"mousemove","time":136153,"x":527,"y":115},{"type":"valuechange","selector":"#main_dim_data_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(5)>select.test-inputs-select-select","value":"0","time":137351,"target":"select"},{"time":137352,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":137395,"x":288,"y":128},{"type":"mousemove","time":137603,"x":288,"y":128},{"type":"mousemove","time":137794,"x":289,"y":127},{"type":"mousemove","time":138012,"x":271,"y":116},{"type":"mousemove","time":138243,"x":271,"y":115},{"type":"mousemove","time":138419,"x":271,"y":115},{"type":"mousemove","time":138622,"x":181,"y":113},{"type":"mousemove","time":138822,"x":158,"y":117},{"type":"mousemove","time":139065,"x":142,"y":121},{"type":"valuechange","selector":"#main_dim_data_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(3)>select.test-inputs-select-select","value":"0","time":139963,"target":"select"},{"time":139964,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":139986,"x":239,"y":101},{"type":"mousedown","time":140124,"x":284,"y":104},{"type":"mousemove","time":140192,"x":284,"y":104},{"type":"mouseup","time":140256,"x":284,"y":104},{"time":140257,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":140303,"x":284,"y":104},{"type":"mousedown","time":140487,"x":282,"y":125},{"type":"mousemove","time":140556,"x":282,"y":125},{"type":"mouseup","time":140672,"x":282,"y":125},{"time":140673,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":140963,"x":282,"y":123},{"type":"mousemove","time":141170,"x":283,"y":120},{"type":"mousemove","time":142062,"x":281,"y":155},{"type":"mousemove","time":142278,"x":94,"y":330},{"type":"mousedown","time":142381,"x":80,"y":340},{"type":"mouseup","time":142487,"x":80,"y":340},{"time":142488,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":142533,"x":80,"y":340},{"type":"mousemove","time":142627,"x":81,"y":340},{"type":"mousedown","time":142800,"x":234,"y":361},{"type":"mousemove","time":142868,"x":235,"y":362},{"type":"mouseup","time":142891,"x":235,"y":362},{"time":142892,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":143029,"x":241,"y":362},{"type":"mousedown","time":143208,"x":357,"y":362},{"type":"mousemove","time":143230,"x":357,"y":362},{"type":"mouseup","time":143338,"x":357,"y":362},{"time":143339,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":143430,"x":260,"y":460},{"type":"mousedown","time":143590,"x":198,"y":535},{"type":"mousemove","time":143663,"x":198,"y":535},{"type":"mouseup","time":143704,"x":198,"y":535},{"time":143705,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":143868,"x":202,"y":535},{"type":"mousemove","time":144089,"x":539,"y":519},{"type":"mousedown","time":144132,"x":539,"y":519},{"type":"mouseup","time":144237,"x":539,"y":520},{"time":144238,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":144309,"x":636,"y":499},{"type":"mousemove","time":144511,"x":752,"y":434},{"type":"mousedown","time":144597,"x":752,"y":434},{"type":"mouseup","time":144734,"x":752,"y":434},{"time":144735,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":144761,"x":752,"y":444},{"type":"mousemove","time":144985,"x":752,"y":519},{"type":"mousedown","time":145007,"x":752,"y":519},{"type":"mouseup","time":145162,"x":752,"y":519},{"time":145163,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":145190,"x":752,"y":509},{"type":"mousemove","time":145413,"x":741,"y":262},{"type":"mousedown","time":145489,"x":740,"y":257},{"type":"mousemove","time":145627,"x":740,"y":257},{"type":"mouseup","time":145672,"x":739,"y":257},{"time":145673,"delay":400,"type":"screenshot-auto"},{"type":"mousedown","time":145808,"x":644,"y":257},{"type":"mousemove","time":145830,"x":644,"y":257},{"type":"mouseup","time":145901,"x":644,"y":257},{"time":145902,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":146131,"x":640,"y":257},{"type":"mousemove","time":146360,"x":241,"y":228},{"type":"mousedown","time":146454,"x":230,"y":227},{"type":"mouseup","time":146573,"x":230,"y":227},{"time":146574,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":146624,"x":230,"y":227},{"type":"mousemove","time":146819,"x":226,"y":227},{"type":"mousemove","time":147023,"x":109,"y":245},{"type":"mousedown","time":147069,"x":109,"y":245},{"type":"mouseup","time":147155,"x":109,"y":245},{"time":147156,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":147257,"x":109,"y":245},{"type":"mousemove","time":147473,"x":242,"y":148},{"type":"mousemove","time":147689,"x":252,"y":119},{"type":"mousemove","time":147898,"x":279,"y":108},{"type":"mousemove","time":148130,"x":279,"y":108},{"type":"valuechange","selector":"#main_dim_data_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(4)>select.test-inputs-select-select","value":"1","time":148953,"target":"select"},{"time":148954,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":149004,"x":276,"y":213},{"type":"mousemove","time":149223,"x":243,"y":356},{"type":"mousedown","time":149379,"x":234,"y":375},{"type":"mousemove","time":149479,"x":234,"y":375},{"type":"mouseup","time":149502,"x":234,"y":375},{"time":149503,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":149693,"x":364,"y":378},{"type":"mousedown","time":149767,"x":373,"y":378},{"type":"mouseup","time":149842,"x":373,"y":378},{"time":149843,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":149938,"x":373,"y":378},{"type":"mousemove","time":150035,"x":382,"y":377},{"type":"mousedown","time":150203,"x":513,"y":359},{"type":"mousemove","time":150277,"x":513,"y":359},{"type":"mouseup","time":150321,"x":513,"y":359},{"time":150322,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":150495,"x":628,"y":359},{"type":"mousedown","time":150543,"x":628,"y":359},{"type":"mouseup","time":150638,"x":628,"y":359},{"time":150639,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":150914,"x":614,"y":346},{"type":"mousemove","time":151117,"x":80,"y":82},{"type":"mousemove","time":151349,"x":80,"y":83},{"type":"mousemove","time":151509,"x":80,"y":86},{"type":"mousemove","time":151712,"x":116,"y":111},{"type":"mousemove","time":151918,"x":119,"y":133},{"type":"mousemove","time":152150,"x":111,"y":147},{"type":"mousedown","time":152296,"x":108,"y":158},{"type":"mouseup","time":152389,"x":108,"y":158},{"time":152390,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":152439,"x":108,"y":158},{"type":"mousemove","time":152579,"x":117,"y":158},{"type":"mousemove","time":152785,"x":358,"y":144},{"type":"mousemove","time":152997,"x":369,"y":156},{"type":"mousedown","time":153091,"x":371,"y":159},{"type":"mouseup","time":153205,"x":371,"y":159},{"time":153206,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":153232,"x":378,"y":159},{"type":"mousedown","time":153472,"x":569,"y":159},{"type":"mousemove","time":153513,"x":569,"y":159},{"type":"mouseup","time":153608,"x":569,"y":159},{"time":153609,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":153661,"x":563,"y":159},{"type":"mousemove","time":153868,"x":438,"y":135},{"type":"mousemove","time":154087,"x":248,"y":99},{"type":"mousemove","time":154330,"x":247,"y":95},{"type":"valuechange","selector":"#main_dim_data_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select>select.test-inputs-select-select","value":"2","time":155393,"target":"select"},{"time":155394,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":155417,"x":204,"y":204},{"type":"mousemove","time":155821,"x":208,"y":198},{"type":"mousemove","time":156029,"x":172,"y":115},{"type":"mousemove","time":156232,"x":124,"y":106},{"type":"mousemove","time":156447,"x":119,"y":108},{"type":"mousemove","time":156680,"x":120,"y":113},{"type":"mousemove","time":156902,"x":113,"y":311},{"type":"mousedown","time":157006,"x":112,"y":330},{"type":"mouseup","time":157094,"x":112,"y":330},{"time":157095,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":157170,"x":112,"y":330},{"type":"mousemove","time":157283,"x":120,"y":330},{"type":"mousedown","time":157436,"x":250,"y":340},{"type":"mouseup","time":157528,"x":250,"y":341},{"time":157529,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":157583,"x":250,"y":341},{"type":"mousedown","time":157758,"x":370,"y":230},{"type":"mouseup","time":157831,"x":370,"y":230},{"time":157832,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":157877,"x":370,"y":230},{"type":"mousemove","time":157999,"x":380,"y":230},{"type":"mousemove","time":158208,"x":727,"y":255},{"type":"mousedown","time":158353,"x":739,"y":255},{"type":"mousemove","time":158449,"x":739,"y":255},{"type":"mouseup","time":158475,"x":739,"y":255},{"time":158476,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":158654,"x":739,"y":385},{"type":"mousedown","time":158701,"x":739,"y":385},{"type":"mouseup","time":158767,"x":739,"y":385},{"time":158768,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":158917,"x":739,"y":386},{"type":"mousemove","time":159126,"x":700,"y":523},{"type":"mousemove","time":159329,"x":742,"y":529},{"type":"mousedown","time":159400,"x":745,"y":528},{"type":"mouseup","time":159513,"x":745,"y":528},{"time":159514,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":159542,"x":725,"y":528},{"type":"mousedown","time":159684,"x":629,"y":547},{"type":"mousemove","time":159771,"x":629,"y":547},{"type":"mouseup","time":159813,"x":629,"y":547},{"time":159814,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":159976,"x":181,"y":387},{"type":"mousemove","time":160222,"x":174,"y":387},{"type":"mousedown","time":160428,"x":53,"y":386},{"type":"mousemove","time":160451,"x":53,"y":386},{"type":"mouseup","time":160493,"x":53,"y":386},{"time":160494,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":160667,"x":87,"y":324},{"type":"mousemove","time":160896,"x":258,"y":143},{"type":"mousemove","time":161129,"x":294,"y":119},{"type":"mousemove","time":161354,"x":289,"y":110},{"type":"valuechange","selector":"#main_dim_data_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(4)>select.test-inputs-select-select","value":"0","time":162311,"target":"select"},{"time":162312,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":162335,"x":289,"y":99},{"type":"mousemove","time":162557,"x":222,"y":267},{"type":"mousemove","time":162770,"x":219,"y":286},{"type":"mousedown","time":162906,"x":231,"y":309},{"type":"mouseup","time":162979,"x":231,"y":309},{"time":162980,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":163030,"x":231,"y":309},{"type":"mousemove","time":163153,"x":231,"y":312},{"type":"mousedown","time":163321,"x":245,"y":371},{"type":"mousemove","time":163395,"x":245,"y":371},{"type":"mouseup","time":163443,"x":245,"y":371},{"time":163444,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":163597,"x":240,"y":489},{"type":"mousedown","time":163700,"x":240,"y":491},{"type":"mousemove","time":163798,"x":240,"y":491},{"type":"mouseup","time":163821,"x":240,"y":491},{"time":163822,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":164018,"x":235,"y":505},{"type":"mousedown","time":164180,"x":222,"y":536},{"type":"mousemove","time":164248,"x":222,"y":536},{"type":"mouseup","time":164271,"x":222,"y":536},{"time":164272,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":164469,"x":392,"y":274},{"type":"mousedown","time":164649,"x":416,"y":242},{"type":"mousemove","time":164672,"x":416,"y":241},{"type":"mouseup","time":164741,"x":416,"y":241},{"time":164742,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":164884,"x":700,"y":296},{"type":"mousemove","time":165095,"x":730,"y":303},{"type":"mousedown","time":165224,"x":764,"y":318},{"type":"mouseup","time":165323,"x":765,"y":318},{"time":165324,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":165383,"x":765,"y":318},{"type":"mousemove","time":165631,"x":691,"y":557},{"type":"mousedown","time":165682,"x":691,"y":557},{"type":"mouseup","time":165871,"x":691,"y":557},{"time":165872,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":165907,"x":643,"y":507},{"type":"mousemove","time":166140,"x":154,"y":94},{"type":"mousemove","time":166373,"x":155,"y":94},{"type":"mousemove","time":166592,"x":167,"y":90},{"type":"mousemove","time":167035,"x":167,"y":90},{"type":"mousemove","time":167245,"x":134,"y":113},{"type":"valuechange","selector":"#main_dim_data_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(3)>select.test-inputs-select-select","value":"1","time":168280,"target":"select"},{"time":168281,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":168337,"x":142,"y":125},{"type":"mousemove","time":168547,"x":140,"y":221},{"type":"mousedown","time":168741,"x":112,"y":264},{"type":"mousemove","time":168765,"x":112,"y":264},{"type":"mouseup","time":168841,"x":112,"y":264},{"time":168842,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":168978,"x":103,"y":288},{"type":"mousemove","time":169181,"x":155,"y":280},{"type":"mousemove","time":169382,"x":113,"y":345},{"type":"mousedown","time":169431,"x":113,"y":345},{"type":"mouseup","time":169485,"x":113,"y":345},{"time":169486,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":169583,"x":113,"y":345},{"type":"mousemove","time":169697,"x":113,"y":345},{"type":"mousemove","time":169911,"x":162,"y":90},{"type":"mousemove","time":170136,"x":163,"y":88},{"type":"valuechange","selector":"#main_dim_data_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select>select.test-inputs-select-select","value":"3","time":170998,"target":"select"},{"time":170999,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":171024,"x":131,"y":127},{"type":"mousemove","time":171228,"x":125,"y":118},{"type":"mousemove","time":171461,"x":123,"y":111},{"type":"valuechange","selector":"#main_dim_data_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(3)>select.test-inputs-select-select","value":"0","time":172809,"target":"select"},{"time":172810,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":172835,"x":217,"y":92},{"type":"mousemove","time":173058,"x":264,"y":107},{"type":"mousemove","time":173264,"x":336,"y":123},{"type":"mousemove","time":173476,"x":176,"y":152},{"type":"mousemove","time":173684,"x":157,"y":152},{"type":"mousedown","time":173754,"x":157,"y":152},{"type":"mouseup","time":173874,"x":157,"y":152},{"time":173875,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":173926,"x":165,"y":152},{"type":"mousedown","time":174145,"x":331,"y":161},{"type":"mousemove","time":174168,"x":331,"y":161},{"type":"mouseup","time":174270,"x":331,"y":161},{"time":174271,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":174443,"x":334,"y":161},{"type":"mousemove","time":174671,"x":530,"y":139},{"type":"mousedown","time":174722,"x":532,"y":139},{"type":"mouseup","time":174856,"x":533,"y":140},{"time":174857,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":174929,"x":533,"y":140},{"type":"mousemove","time":174954,"x":533,"y":141},{"type":"mousedown","time":175099,"x":533,"y":160},{"type":"mouseup","time":175192,"x":533,"y":160},{"time":175193,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":175223,"x":529,"y":160},{"type":"mousedown","time":175474,"x":409,"y":172},{"type":"mousemove","time":175517,"x":409,"y":172},{"type":"mouseup","time":175561,"x":409,"y":172},{"time":175562,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":175890,"x":406,"y":172},{"type":"mousemove","time":176105,"x":195,"y":197},{"type":"mousedown","time":176154,"x":194,"y":197},{"type":"mousemove","time":176325,"x":194,"y":197},{"type":"mouseup","time":176389,"x":194,"y":197},{"time":176390,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":176419,"x":195,"y":196},{"type":"mousedown","time":176591,"x":215,"y":181},{"type":"mousemove","time":176663,"x":215,"y":181},{"type":"mouseup","time":176747,"x":215,"y":181},{"time":176748,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":177041,"x":215,"y":181},{"type":"mousemove","time":177253,"x":511,"y":148},{"type":"mousemove","time":177476,"x":208,"y":251},{"type":"mousemove","time":177678,"x":168,"y":271},{"type":"mousemove","time":177882,"x":160,"y":269},{"type":"mousedown","time":178099,"x":111,"y":265},{"type":"mousemove","time":178122,"x":111,"y":265},{"type":"mouseup","time":178241,"x":111,"y":265},{"time":178242,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":178327,"x":116,"y":265},{"type":"mousemove","time":178545,"x":221,"y":264},{"type":"mousedown","time":178718,"x":256,"y":267},{"type":"mousemove","time":178795,"x":256,"y":267},{"type":"mouseup","time":178820,"x":256,"y":267},{"time":178821,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":178973,"x":256,"y":270},{"type":"mousedown","time":179137,"x":250,"y":306},{"type":"mousemove","time":179210,"x":250,"y":306},{"type":"mouseup","time":179235,"x":250,"y":306},{"time":179236,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":179410,"x":437,"y":306},{"type":"mousedown","time":179463,"x":437,"y":306},{"type":"mouseup","time":179510,"x":437,"y":306},{"time":179511,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":179646,"x":437,"y":306},{"type":"mousemove","time":179697,"x":431,"y":317},{"type":"mousemove","time":179934,"x":273,"y":385},{"type":"mousedown","time":179979,"x":273,"y":385},{"type":"mouseup","time":180030,"x":272,"y":385},{"time":180031,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":180139,"x":369,"y":385},{"type":"mousedown","time":180274,"x":432,"y":385},{"type":"mousemove","time":180375,"x":432,"y":385},{"type":"mouseup","time":180399,"x":432,"y":385},{"time":180400,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":180601,"x":400,"y":451},{"type":"mousedown","time":180652,"x":400,"y":451},{"type":"mouseup","time":180784,"x":400,"y":451},{"time":180785,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":180814,"x":398,"y":451},{"type":"mousedown","time":181022,"x":259,"y":457},{"type":"mousemove","time":181071,"x":259,"y":457},{"type":"mouseup","time":181151,"x":259,"y":457},{"time":181152,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":181220,"x":268,"y":462},{"type":"mousedown","time":181471,"x":298,"y":506},{"type":"mousemove","time":181520,"x":298,"y":506},{"type":"mouseup","time":181545,"x":298,"y":506},{"time":181546,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":181846,"x":291,"y":506},{"type":"mousedown","time":182068,"x":82,"y":506},{"type":"mousemove","time":182091,"x":82,"y":506},{"type":"mouseup","time":182137,"x":82,"y":506},{"time":182138,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":182431,"x":91,"y":497},{"type":"mousemove","time":182648,"x":755,"y":509},{"type":"mousemove","time":182861,"x":772,"y":489},{"type":"mousemove","time":183010,"x":772,"y":492},{"type":"mousemove","time":183210,"x":761,"y":521},{"type":"mousedown","time":183262,"x":760,"y":523},{"type":"mouseup","time":183396,"x":760,"y":523},{"time":183397,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":183449,"x":760,"y":522},{"type":"mousedown","time":183639,"x":753,"y":421},{"type":"mousemove","time":183663,"x":753,"y":421},{"type":"mouseup","time":183712,"x":753,"y":421},{"time":183713,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":183925,"x":748,"y":410},{"type":"mousemove","time":184136,"x":279,"y":94},{"type":"mousemove","time":184359,"x":289,"y":91},{"type":"mousemove","time":184604,"x":295,"y":91},{"type":"mousemove","time":184880,"x":295,"y":91},{"type":"mousemove","time":185455,"x":295,"y":91},{"type":"valuechange","selector":"#main_dim_data_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select>select.test-inputs-select-select","value":"4","time":186757,"target":"select"},{"time":186758,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":186782,"x":277,"y":121},{"type":"mousemove","time":187675,"x":276,"y":122},{"type":"mousemove","time":187896,"x":222,"y":234},{"type":"mousedown","time":188046,"x":221,"y":234},{"type":"mousemove","time":188114,"x":221,"y":234},{"type":"mouseup","time":188138,"x":221,"y":234},{"time":188139,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":188323,"x":221,"y":241},{"type":"mousedown","time":188513,"x":210,"y":354},{"type":"mousemove","time":188539,"x":210,"y":354},{"type":"mouseup","time":188641,"x":210,"y":354},{"time":188642,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":188744,"x":199,"y":354},{"type":"mousedown","time":188922,"x":110,"y":382},{"type":"mousemove","time":188996,"x":110,"y":382},{"type":"mouseup","time":189076,"x":110,"y":382},{"time":189077,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":189303,"x":107,"y":396},{"type":"mousemove","time":189511,"x":104,"y":510},{"type":"mousemove","time":189724,"x":100,"y":535},{"type":"mousedown","time":189832,"x":96,"y":561},{"type":"mouseup","time":189925,"x":96,"y":561},{"time":189926,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":189951,"x":96,"y":561},{"type":"mousemove","time":190171,"x":330,"y":526},{"type":"mousedown","time":190364,"x":613,"y":537},{"type":"mousemove","time":190396,"x":614,"y":537},{"type":"mouseup","time":190472,"x":614,"y":537},{"time":190473,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":190620,"x":747,"y":370},{"type":"mousedown","time":190724,"x":750,"y":368},{"type":"mouseup","time":190840,"x":750,"y":368},{"time":190841,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":190886,"x":750,"y":368},{"type":"mousemove","time":190910,"x":750,"y":367},{"type":"mousemove","time":191132,"x":627,"y":230},{"type":"mousemove","time":191228,"x":618,"y":219},{"type":"mousemove","time":191442,"x":498,"y":103},{"type":"mousedown","time":191643,"x":498,"y":103},{"type":"mouseup","time":191714,"x":498,"y":103},{"time":191715,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":191816,"x":498,"y":104},{"type":"mousemove","time":192045,"x":498,"y":112},{"type":"valuechange","selector":"#main_dim_data_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(5)>select.test-inputs-select-select","value":"1","time":192767,"target":"select"},{"time":192768,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":192824,"x":516,"y":115},{"type":"mousemove","time":193025,"x":385,"y":367},{"type":"mousedown","time":193074,"x":385,"y":367},{"type":"mouseup","time":193175,"x":385,"y":367},{"time":193176,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":193233,"x":346,"y":389},{"type":"mousemove","time":193434,"x":62,"y":434},{"type":"mousedown","time":193487,"x":60,"y":434},{"type":"mouseup","time":193563,"x":60,"y":434},{"time":193564,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":193657,"x":199,"y":340},{"type":"mousedown","time":193826,"x":351,"y":278},{"type":"mousemove","time":193905,"x":351,"y":278},{"type":"mouseup","time":193955,"x":351,"y":278},{"time":193956,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":194111,"x":421,"y":450},{"type":"mousemove","time":194318,"x":471,"y":505},{"type":"mousedown","time":194557,"x":514,"y":512},{"type":"mousemove","time":194605,"x":514,"y":512},{"type":"mouseup","time":194675,"x":514,"y":512},{"time":194676,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":194811,"x":515,"y":512},{"type":"mousemove","time":195015,"x":786,"y":545},{"type":"mousedown","time":195066,"x":786,"y":545},{"type":"mouseup","time":195147,"x":786,"y":545},{"time":195148,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":195237,"x":784,"y":454},{"type":"mousedown","time":195351,"x":776,"y":406},{"type":"mouseup","time":195423,"x":776,"y":406},{"time":195424,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":195505,"x":776,"y":406},{"type":"mousemove","time":195590,"x":759,"y":393},{"type":"mousemove","time":195835,"x":494,"y":126},{"type":"mousedown","time":195990,"x":494,"y":126},{"type":"mousemove","time":196162,"x":494,"y":125},{"type":"mouseup","time":196235,"x":494,"y":125},{"time":196236,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":196342,"x":493,"y":125},{"type":"mousemove","time":196595,"x":502,"y":122},{"type":"valuechange","selector":"#main_dim_data_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(5)>select.test-inputs-select-select","value":"3","time":197810,"target":"select"},{"time":197811,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":197840,"x":477,"y":220},{"type":"mousedown","time":197893,"x":466,"y":238},{"type":"mouseup","time":198006,"x":466,"y":238},{"time":198007,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":198065,"x":510,"y":260},{"type":"mousedown","time":198239,"x":579,"y":363},{"type":"mousemove","time":198312,"x":579,"y":364},{"type":"mouseup","time":198343,"x":579,"y":364},{"time":198344,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":198477,"x":586,"y":365},{"type":"mousedown","time":198656,"x":773,"y":395},{"type":"mousemove","time":198708,"x":773,"y":395},{"type":"mouseup","time":198757,"x":773,"y":395},{"time":198758,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":198938,"x":735,"y":557},{"type":"mousemove","time":199310,"x":730,"y":553},{"type":"mousedown","time":199427,"x":739,"y":493},{"type":"mousemove","time":199562,"x":739,"y":493},{"type":"mouseup","time":199588,"x":739,"y":493},{"time":199589,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":199839,"x":675,"y":519},{"type":"mousedown","time":200025,"x":735,"y":533},{"type":"mousemove","time":200049,"x":736,"y":533},{"type":"mouseup","time":200204,"x":736,"y":533},{"time":200205,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":200264,"x":223,"y":515},{"type":"mousedown","time":200475,"x":83,"y":515},{"type":"mousemove","time":200527,"x":83,"y":515},{"type":"mouseup","time":200605,"x":83,"y":515},{"time":200606,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":200771,"x":85,"y":507},{"type":"mousedown","time":200988,"x":114,"y":394},{"type":"mousemove","time":201014,"x":114,"y":393},{"type":"mouseup","time":201064,"x":114,"y":393},{"time":201065,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":201226,"x":122,"y":366},{"type":"mousemove","time":201429,"x":137,"y":106},{"type":"mousemove","time":201667,"x":131,"y":104},{"type":"mousemove","time":201924,"x":129,"y":110},{"type":"valuechange","selector":"#main_dim_data_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(3)>select.test-inputs-select-select","value":"1","time":202822,"target":"select"},{"time":202823,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":202850,"x":280,"y":124},{"type":"mousemove","time":203091,"x":306,"y":114},{"type":"valuechange","selector":"#main_dim_data_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(4)>select.test-inputs-select-select","value":"1","time":203779,"target":"select"},{"time":203780,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":203806,"x":247,"y":223},{"type":"mousedown","time":203855,"x":238,"y":234},{"type":"mouseup","time":203909,"x":238,"y":234},{"time":203910,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":204009,"x":114,"y":241},{"type":"mousedown","time":204121,"x":73,"y":242},{"type":"mouseup","time":204224,"x":73,"y":242},{"time":204225,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":204279,"x":73,"y":294},{"type":"mousemove","time":204480,"x":73,"y":380},{"type":"mousedown","time":204631,"x":73,"y":380},{"type":"mouseup","time":204762,"x":73,"y":380},{"time":204763,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":204791,"x":79,"y":380},{"type":"mousedown","time":204965,"x":278,"y":396},{"type":"mousemove","time":204991,"x":279,"y":396},{"type":"mouseup","time":205066,"x":279,"y":396},{"time":205067,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":205192,"x":277,"y":523},{"type":"mousedown","time":205272,"x":277,"y":524},{"type":"mouseup","time":205426,"x":277,"y":524},{"time":205427,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":205478,"x":277,"y":524},{"type":"mousemove","time":205563,"x":278,"y":524},{"type":"mousemove","time":205767,"x":691,"y":469},{"type":"mousemove","time":205994,"x":747,"y":454},{"type":"mousedown","time":206046,"x":749,"y":453},{"type":"mouseup","time":206164,"x":749,"y":453},{"time":206165,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":206247,"x":749,"y":453},{"type":"mousemove","time":206274,"x":748,"y":453},{"type":"mousemove","time":206475,"x":307,"y":7},{"type":"mousemove","time":206701,"x":287,"y":80},{"type":"valuechange","selector":"#main_dim_data_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select>select.test-inputs-select-select","value":"5","time":207908,"target":"select"},{"time":207909,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":207938,"x":162,"y":140},{"type":"mousemove","time":208157,"x":138,"y":146},{"type":"mousemove","time":208357,"x":137,"y":145},{"type":"mousemove","time":208589,"x":125,"y":112},{"type":"valuechange","selector":"#main_dim_data_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(3)>select.test-inputs-select-select","value":"0","time":209671,"target":"select"},{"time":209672,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":209698,"x":196,"y":99},{"type":"mousemove","time":209931,"x":277,"y":112},{"type":"valuechange","selector":"#main_dim_data_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(4)>select.test-inputs-select-select","value":"0","time":210814,"target":"select"},{"time":210815,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":210854,"x":463,"y":129},{"type":"mousemove","time":211083,"x":503,"y":122},{"type":"valuechange","selector":"#main_dim_data_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(5)>select.test-inputs-select-select","value":"0","time":213364,"target":"select"},{"time":213365,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":213400,"x":340,"y":256},{"type":"mousedown","time":213600,"x":188,"y":350},{"type":"mousemove","time":213626,"x":188,"y":350},{"type":"mouseup","time":213748,"x":188,"y":350},{"time":213749,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":213854,"x":259,"y":354},{"type":"mousedown","time":213934,"x":269,"y":354},{"type":"mouseup","time":214069,"x":269,"y":354},{"time":214070,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":214102,"x":279,"y":354},{"type":"mousemove","time":214320,"x":236,"y":350},{"type":"mousedown","time":214442,"x":210,"y":349},{"type":"mousemove","time":214569,"x":210,"y":349},{"type":"mouseup","time":214630,"x":210,"y":349},{"time":214631,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":214670,"x":232,"y":286},{"type":"mousedown","time":214853,"x":246,"y":240},{"type":"mousemove","time":214931,"x":246,"y":240},{"type":"mouseup","time":215039,"x":246,"y":240},{"time":215040,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":215070,"x":239,"y":240},{"type":"mousedown","time":215266,"x":124,"y":240},{"type":"mousemove","time":215290,"x":124,"y":240},{"type":"mouseup","time":215415,"x":124,"y":240},{"time":215416,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":215503,"x":139,"y":324},{"type":"mousemove","time":215711,"x":121,"y":391},{"type":"mousemove","time":215931,"x":103,"y":444},{"type":"mousemove","time":216146,"x":71,"y":533},{"type":"mousedown","time":216195,"x":71,"y":533},{"type":"mouseup","time":216318,"x":71,"y":533},{"time":216319,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":216349,"x":111,"y":534},{"type":"mousedown","time":216488,"x":266,"y":551},{"type":"mouseup","time":216582,"x":267,"y":551},{"time":216583,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":216639,"x":267,"y":551},{"type":"mousemove","time":216730,"x":267,"y":551},{"type":"mousedown","time":216874,"x":377,"y":406},{"type":"mousemove","time":216971,"x":377,"y":405},{"type":"mouseup","time":217001,"x":377,"y":405},{"time":217002,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":217182,"x":424,"y":421},{"type":"mousemove","time":217404,"x":677,"y":529},{"type":"mousemove","time":217608,"x":728,"y":524},{"type":"mousedown","time":217633,"x":728,"y":524},{"type":"mouseup","time":217769,"x":728,"y":524},{"time":217770,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":217835,"x":740,"y":477},{"type":"mousedown","time":217943,"x":742,"y":437},{"type":"mouseup","time":218076,"x":742,"y":437},{"time":218077,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":218128,"x":742,"y":437},{"type":"mousemove","time":218243,"x":739,"y":437},{"type":"mousedown","time":218422,"x":512,"y":392},{"type":"mousemove","time":218449,"x":510,"y":392},{"type":"mouseup","time":218528,"x":510,"y":392},{"time":218529,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":218653,"x":499,"y":395},{"type":"mousemove","time":218870,"x":270,"y":415},{"type":"mousemove","time":219082,"x":223,"y":425},{"type":"mousedown","time":219135,"x":222,"y":425},{"type":"mouseup","time":219244,"x":222,"y":425},{"time":219245,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":219336,"x":222,"y":425},{"type":"mousemove","time":219414,"x":205,"y":426},{"type":"mousedown","time":219540,"x":155,"y":441},{"type":"mousemove","time":219647,"x":155,"y":441},{"type":"mouseup","time":219704,"x":155,"y":441},{"time":219705,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":219922,"x":168,"y":441},{"type":"mousedown","time":219977,"x":170,"y":441},{"type":"mouseup","time":220112,"x":170,"y":441},{"time":220113,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":220152,"x":200,"y":441},{"type":"mousemove","time":220361,"x":258,"y":440},{"type":"mousedown","time":220416,"x":258,"y":440},{"type":"mouseup","time":220524,"x":258,"y":440},{"time":220525,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":220566,"x":293,"y":433},{"type":"mousemove","time":220817,"x":725,"y":329},{"type":"mousemove","time":220878,"x":782,"y":277},{"type":"mousedown","time":220998,"x":750,"y":267},{"type":"mouseup","time":221111,"x":750,"y":267},{"time":221112,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":221165,"x":750,"y":267},{"type":"mousemove","time":221259,"x":749,"y":266},{"type":"mousemove","time":221493,"x":405,"y":94},{"type":"mousemove","time":221739,"x":399,"y":93},{"type":"valuechange","selector":"#main_dim_data_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select>select.test-inputs-select-select","value":"6","time":222649,"target":"select"},{"time":222650,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":222682,"x":266,"y":237},{"type":"mousemove","time":222917,"x":185,"y":318},{"type":"mousemove","time":223211,"x":186,"y":318},{"type":"mousemove","time":223413,"x":256,"y":343},{"type":"mousedown","time":223612,"x":285,"y":337},{"type":"mousemove","time":223647,"x":285,"y":337},{"type":"mouseup","time":223728,"x":285,"y":337},{"time":223729,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":223902,"x":285,"y":340},{"type":"mousedown","time":224060,"x":260,"y":386},{"type":"mousemove","time":224139,"x":260,"y":386},{"type":"mouseup","time":224164,"x":260,"y":386},{"time":224165,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":224229,"x":249,"y":386},{"type":"mousedown","time":224381,"x":204,"y":386},{"type":"mousemove","time":224469,"x":204,"y":386},{"type":"mouseup","time":224496,"x":204,"y":386},{"time":224497,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":224681,"x":210,"y":312},{"type":"mousedown","time":224845,"x":210,"y":312},{"type":"mouseup","time":224961,"x":210,"y":312},{"time":224962,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":225005,"x":250,"y":312},{"type":"mousemove","time":225238,"x":412,"y":306},{"type":"mousedown","time":225370,"x":469,"y":306},{"type":"mousemove","time":225478,"x":469,"y":306},{"type":"mouseup","time":225529,"x":469,"y":306},{"time":225530,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":225696,"x":466,"y":251},{"type":"mousedown","time":225750,"x":466,"y":251},{"type":"mouseup","time":225870,"x":444,"y":250},{"time":225871,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":225900,"x":281,"y":254},{"type":"mousemove","time":226128,"x":175,"y":251},{"type":"mousemove","time":226340,"x":135,"y":261},{"type":"mousedown","time":226423,"x":134,"y":262},{"type":"mouseup","time":226509,"x":134,"y":262},{"time":226510,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":226546,"x":134,"y":308},{"type":"mousemove","time":226813,"x":122,"y":553},{"type":"mousemove","time":227006,"x":105,"y":594},{"type":"mousemove","time":227227,"x":112,"y":546},{"type":"mousedown","time":227293,"x":118,"y":537},{"type":"mouseup","time":227447,"x":118,"y":536},{"time":227448,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":227493,"x":221,"y":536},{"type":"mousedown","time":227655,"x":430,"y":536},{"type":"mousemove","time":227742,"x":431,"y":536},{"type":"mouseup","time":227796,"x":431,"y":536},{"time":227797,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":227965,"x":710,"y":532},{"type":"mousemove","time":228181,"x":761,"y":533},{"type":"mousedown","time":228294,"x":758,"y":536},{"type":"mousemove","time":228403,"x":758,"y":536},{"type":"mouseup","time":228480,"x":758,"y":536},{"time":228481,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":228630,"x":758,"y":421},{"type":"mousedown","time":228684,"x":758,"y":421},{"type":"mouseup","time":228740,"x":758,"y":421},{"time":228741,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":228832,"x":758,"y":421},{"type":"mousemove","time":228937,"x":758,"y":418},{"type":"mousedown","time":229186,"x":765,"y":277},{"type":"mousemove","time":229248,"x":765,"y":277},{"type":"mouseup","time":229327,"x":765,"y":277},{"time":229328,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":229394,"x":751,"y":281},{"type":"mousedown","time":229587,"x":668,"y":292},{"type":"mousemove","time":229672,"x":668,"y":292},{"type":"mouseup","time":229700,"x":668,"y":292},{"time":229701,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":229880,"x":567,"y":362},{"type":"mousedown","time":229978,"x":551,"y":374},{"type":"mousemove","time":230098,"x":550,"y":374},{"type":"mouseup","time":230159,"x":550,"y":374},{"time":230160,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":230264,"x":548,"y":358},{"type":"mousemove","time":230475,"x":540,"y":327},{"type":"mousedown","time":230686,"x":537,"y":303},{"type":"mousemove","time":230714,"x":537,"y":303},{"type":"mouseup","time":230773,"x":537,"y":303},{"time":230774,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":230988,"x":522,"y":291},{"type":"mousemove","time":231199,"x":293,"y":136},{"type":"mousemove","time":231431,"x":365,"y":85},{"type":"mousemove","time":231654,"x":324,"y":95},{"type":"mousemove","time":231892,"x":292,"y":108},{"type":"mousemove","time":232083,"x":292,"y":108},{"type":"mousemove","time":232339,"x":292,"y":108},{"type":"valuechange","selector":"#main_dim_data_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(4)>select.test-inputs-select-select","value":"1","time":233095,"target":"select"},{"time":233096,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":233135,"x":165,"y":130},{"type":"mousemove","time":233340,"x":136,"y":118},{"type":"mousemove","time":233594,"x":128,"y":112},{"type":"valuechange","selector":"#main_dim_data_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(3)>select.test-inputs-select-select","value":"1","time":234342,"target":"select"},{"time":234343,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":234372,"x":265,"y":131},{"type":"valuechange","selector":"#main_dim_data_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(4)>select.test-inputs-select-select","value":"0","time":235742,"target":"select"},{"time":235743,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":235776,"x":397,"y":128},{"type":"mousemove","time":235977,"x":515,"y":137},{"type":"mousemove","time":236229,"x":513,"y":120},{"type":"valuechange","selector":"#main_dim_data_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(5)>select.test-inputs-select-select","value":"2","time":236969,"target":"select"},{"time":236970,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":237009,"x":444,"y":278},{"type":"mousedown","time":237096,"x":359,"y":383},{"type":"mouseup","time":237197,"x":359,"y":383},{"time":237198,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":237288,"x":359,"y":383},{"type":"mousemove","time":237488,"x":354,"y":382},{"type":"mousemove","time":237694,"x":214,"y":300},{"type":"mousedown","time":237813,"x":214,"y":299},{"type":"mouseup","time":237923,"x":214,"y":299},{"time":237924,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":237981,"x":214,"y":299},{"type":"mousemove","time":238094,"x":214,"y":292},{"type":"mousemove","time":238305,"x":281,"y":139},{"type":"mousemove","time":238529,"x":284,"y":121},{"type":"mousemove","time":238754,"x":284,"y":118},{"type":"valuechange","selector":"#main_dim_data_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(4)>select.test-inputs-select-select","value":"1","time":239757,"target":"select"},{"time":239758,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":239788,"x":272,"y":336},{"type":"mousemove","time":240005,"x":208,"y":301},{"type":"mousedown","time":240149,"x":207,"y":299},{"type":"mousemove","time":240259,"x":207,"y":299},{"type":"mouseup","time":240288,"x":207,"y":299},{"time":240289,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":240479,"x":269,"y":350},{"type":"mousedown","time":240571,"x":280,"y":356},{"type":"mouseup","time":240690,"x":280,"y":356},{"time":240691,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":240728,"x":248,"y":273},{"type":"mousemove","time":240949,"x":249,"y":143},{"type":"mousemove","time":241164,"x":273,"y":119},{"type":"mousemove","time":241367,"x":275,"y":105},{"type":"valuechange","selector":"#main_dim_data_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(4)>select.test-inputs-select-select","value":"0","time":242274,"target":"select"},{"time":242275,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":242302,"x":137,"y":107},{"type":"mousemove","time":242518,"x":114,"y":119},{"type":"valuechange","selector":"#main_dim_data_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(3)>select.test-inputs-select-select","value":"0","time":243748,"target":"select"},{"time":243749,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":243780,"x":198,"y":326},{"type":"mousemove","time":243995,"x":211,"y":309},{"type":"mousemove","time":244220,"x":179,"y":319},{"type":"mousedown","time":244401,"x":182,"y":323},{"type":"mousemove","time":244435,"x":183,"y":322},{"type":"mouseup","time":244512,"x":183,"y":322},{"time":244513,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":244651,"x":211,"y":312},{"type":"mousedown","time":244765,"x":213,"y":312},{"type":"mouseup","time":244851,"x":213,"y":312},{"time":244852,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":244888,"x":213,"y":300},{"type":"mousemove","time":245127,"x":274,"y":167},{"type":"mousemove","time":245344,"x":261,"y":97},{"type":"mousemove","time":245583,"x":261,"y":95},{"type":"valuechange","selector":"#main_dim_data_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select>select.test-inputs-select-select","value":"7","time":246497,"target":"select"},{"time":246498,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":246526,"x":232,"y":355},{"type":"mousemove","time":246760,"x":230,"y":369},{"type":"mousemove","time":247102,"x":227,"y":371},{"type":"mousedown","time":247254,"x":215,"y":371},{"type":"mouseup","time":247333,"x":215,"y":371},{"time":247334,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":247389,"x":215,"y":371},{"type":"mousemove","time":247599,"x":113,"y":339},{"type":"mousedown","time":247654,"x":113,"y":339},{"type":"mouseup","time":247706,"x":113,"y":339},{"time":247707,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":247815,"x":145,"y":300},{"type":"mousedown","time":247992,"x":168,"y":269},{"type":"mousemove","time":248017,"x":168,"y":269},{"type":"mouseup","time":248145,"x":168,"y":269},{"time":248146,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":248228,"x":171,"y":273},{"type":"mousemove","time":248472,"x":245,"y":598},{"type":"mousemove","time":248576,"x":287,"y":584},{"type":"mousedown","time":248706,"x":297,"y":543},{"type":"mousemove","time":248816,"x":297,"y":543},{"type":"mouseup","time":248849,"x":297,"y":543},{"time":248850,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":249036,"x":490,"y":488},{"type":"mousemove","time":249260,"x":544,"y":488},{"type":"mousemove","time":249463,"x":739,"y":505},{"type":"mousemove","time":249679,"x":746,"y":494},{"type":"mousedown","time":249860,"x":758,"y":475},{"type":"mousemove","time":249949,"x":758,"y":475},{"type":"mouseup","time":250000,"x":758,"y":475},{"time":250001,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":250163,"x":742,"y":530},{"type":"mousemove","time":250389,"x":742,"y":530},{"type":"mousedown","time":250417,"x":742,"y":530},{"type":"mouseup","time":250524,"x":742,"y":530},{"time":250525,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":250568,"x":742,"y":458},{"type":"mousedown","time":250805,"x":742,"y":232},{"type":"mousemove","time":250859,"x":742,"y":232},{"type":"mouseup","time":250972,"x":742,"y":232},{"time":250973,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":251005,"x":705,"y":237},{"type":"mousedown","time":251184,"x":566,"y":260},{"type":"mousemove","time":251212,"x":566,"y":260},{"type":"mouseup","time":251269,"x":566,"y":260},{"time":251270,"delay":400,"type":"screenshot-auto"},{"type":"mousedown","time":251406,"x":517,"y":384},{"type":"mousemove","time":251433,"x":517,"y":385},{"type":"mouseup","time":251517,"x":517,"y":385},{"time":251518,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":251649,"x":581,"y":79},{"type":"mousemove","time":251866,"x":544,"y":87},{"type":"mousemove","time":252078,"x":509,"y":110},{"type":"mousemove","time":252308,"x":509,"y":111},{"type":"valuechange","selector":"#main_dim_data_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(5)>select.test-inputs-select-select","value":"0","time":253236,"target":"select"},{"time":253237,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":253266,"x":415,"y":327},{"type":"mousedown","time":253320,"x":409,"y":349},{"type":"mouseup","time":253440,"x":409,"y":349},{"time":253441,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":253474,"x":411,"y":344},{"type":"mousedown","time":253674,"x":510,"y":251},{"type":"mousemove","time":253701,"x":510,"y":251},{"type":"mouseup","time":253809,"x":510,"y":251},{"time":253810,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":253916,"x":162,"y":270},{"type":"mousedown","time":254133,"x":97,"y":253},{"type":"mousemove","time":254160,"x":97,"y":253},{"type":"mouseup","time":254241,"x":97,"y":253},{"time":254242,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":254373,"x":113,"y":253},{"type":"mousedown","time":254497,"x":120,"y":253},{"type":"mouseup","time":254615,"x":120,"y":253},{"time":254616,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":254676,"x":120,"y":253},{"type":"mousemove","time":254706,"x":120,"y":291},{"type":"mousedown","time":254925,"x":116,"y":402},{"type":"mousemove","time":254959,"x":116,"y":403},{"type":"mouseup","time":255012,"x":116,"y":403},{"time":255013,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":255162,"x":116,"y":407},{"type":"mousemove","time":255380,"x":115,"y":532},{"type":"mousedown","time":255472,"x":115,"y":545},{"type":"mouseup","time":255556,"x":115,"y":545},{"time":255557,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":255596,"x":125,"y":545},{"type":"mousedown","time":255781,"x":285,"y":552},{"type":"mousemove","time":255815,"x":286,"y":552},{"type":"mouseup","time":255871,"x":286,"y":552},{"time":255872,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":256029,"x":324,"y":549},{"type":"mousemove","time":256256,"x":717,"y":548},{"type":"mousemove","time":256480,"x":738,"y":555},{"type":"mousedown","time":256537,"x":738,"y":555},{"type":"mouseup","time":256646,"x":738,"y":555},{"time":256647,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":256688,"x":740,"y":540},{"type":"mousedown","time":256874,"x":740,"y":405},{"type":"mousemove","time":256923,"x":740,"y":404},{"type":"mouseup","time":257001,"x":740,"y":404},{"time":257002,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":257150,"x":735,"y":404},{"type":"mousedown","time":257318,"x":634,"y":402},{"type":"mouseup","time":257396,"x":633,"y":402},{"time":257397,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":257457,"x":633,"y":402},{"type":"mousemove","time":257660,"x":549,"y":111},{"type":"mousemove","time":257875,"x":512,"y":85},{"type":"mousemove","time":258157,"x":497,"y":117},{"type":"valuechange","selector":"#main_dim_data_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(5)>select.test-inputs-select-select","value":"1","time":260161,"target":"select"},{"time":260162,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":260190,"x":362,"y":322},{"type":"mousedown","time":260216,"x":362,"y":322},{"type":"mouseup","time":260250,"x":362,"y":322},{"time":260251,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":260406,"x":362,"y":322},{"type":"mousemove","time":260505,"x":357,"y":309},{"type":"mousedown","time":260648,"x":306,"y":229},{"type":"mouseup","time":260741,"x":306,"y":229},{"time":260742,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":260776,"x":246,"y":229},{"type":"mousedown","time":261006,"x":127,"y":229},{"type":"mousemove","time":261037,"x":126,"y":229},{"type":"mouseup","time":261093,"x":126,"y":229},{"time":261094,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":261263,"x":107,"y":441},{"type":"mousedown","time":261369,"x":107,"y":441},{"type":"mouseup","time":261427,"x":107,"y":441},{"time":261428,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":261517,"x":107,"y":441},{"type":"mousemove","time":261636,"x":119,"y":448},{"type":"mousemove","time":261874,"x":286,"y":515},{"type":"mousedown","time":261935,"x":286,"y":516},{"type":"mouseup","time":262044,"x":286,"y":516},{"time":262045,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":262086,"x":338,"y":516},{"type":"mousemove","time":262309,"x":692,"y":516},{"type":"mousemove","time":262523,"x":741,"y":517},{"type":"mousedown","time":262614,"x":745,"y":518},{"type":"mouseup","time":262730,"x":745,"y":518},{"time":262731,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":262765,"x":683,"y":289},{"type":"mousemove","time":263093,"x":103,"y":10},{"type":"mousemove","time":263309,"x":131,"y":98},{"type":"mousemove","time":263527,"x":131,"y":109},{"type":"mousemove","time":263747,"x":131,"y":109},{"type":"valuechange","selector":"#main_dim_data_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(3)>select.test-inputs-select-select","value":"1","time":264507,"target":"select"},{"time":264508,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":264540,"x":294,"y":119},{"type":"mousemove","time":264756,"x":281,"y":110},{"type":"valuechange","selector":"#main_dim_data_edge_cases>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(4)>select.test-inputs-select-select","value":"1","time":265697,"target":"select"},{"time":265698,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":265730,"x":249,"y":211},{"type":"mousemove","time":265942,"x":163,"y":341},{"type":"mousedown","time":266006,"x":163,"y":341},{"type":"mouseup","time":266147,"x":163,"y":341},{"time":266148,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":266218,"x":163,"y":341},{"type":"mousemove","time":266252,"x":134,"y":341},{"type":"mousedown","time":266312,"x":123,"y":341},{"type":"mouseup","time":266461,"x":123,"y":341},{"time":266462,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":266495,"x":194,"y":294},{"type":"mousedown","time":266620,"x":261,"y":250},{"type":"mousemove","time":266740,"x":261,"y":250},{"type":"mousemove","time":266947,"x":74,"y":245},{"type":"mousemove","time":267152,"x":118,"y":310},{"type":"mouseup","time":267269,"x":124,"y":316},{"time":267270,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":267357,"x":124,"y":306},{"type":"mousedown","time":267564,"x":115,"y":270},{"type":"mousemove","time":267593,"x":115,"y":269},{"type":"mouseup","time":267682,"x":115,"y":269},{"time":267683,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":267806,"x":415,"y":375},{"type":"mousemove","time":268029,"x":558,"y":524},{"type":"mousedown","time":268127,"x":544,"y":524},{"type":"mousemove","time":268236,"x":544,"y":524},{"type":"mouseup","time":268264,"x":544,"y":524},{"time":268265,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":268466,"x":701,"y":502},{"type":"mousedown","time":268686,"x":712,"y":515},{"type":"mousemove","time":268713,"x":712,"y":515},{"type":"mouseup","time":268827,"x":712,"y":515},{"time":268828,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":268932,"x":733,"y":493},{"type":"mousedown","time":269063,"x":737,"y":487},{"type":"mousemove","time":269174,"x":737,"y":487},{"type":"mouseup","time":269224,"x":737,"y":487},{"time":269225,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":269441,"x":737,"y":485},{"type":"mousemove","time":269696,"x":762,"y":238},{"type":"mousedown","time":269895,"x":754,"y":257},{"type":"mousemove","time":269925,"x":754,"y":257},{"type":"mouseup","time":270135,"x":754,"y":257},{"time":270136,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":270335,"x":750,"y":257},{"type":"mousemove","time":270572,"x":587,"y":238},{"type":"mousedown","time":270631,"x":587,"y":238},{"type":"mouseup","time":270717,"x":587,"y":238},{"time":270718,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":270818,"x":587,"y":238}],"scrollY":1480,"scrollX":0,"timestamp":1748968335095}] \ No newline at end of file diff --git a/test/runTest/actions/matrix3.json b/test/runTest/actions/matrix3.json new file mode 100644 index 0000000000..ba6fe8c4bf --- /dev/null +++ b/test/runTest/actions/matrix3.json @@ -0,0 +1 @@ +[{"name":"Action 1","ops":[{"type":"mousemove","time":257,"x":280,"y":290},{"type":"mousemove","time":465,"x":155,"y":58},{"type":"mousemove","time":673,"x":135,"y":39},{"type":"mousemove","time":880,"x":127,"y":28},{"type":"valuechange","selector":"#main_layout_components_series>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select>select.test-inputs-select-select","value":"1","time":1719,"target":"select"},{"time":1720,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":1732,"x":270,"y":36},{"type":"mousemove","time":1952,"x":274,"y":32},{"type":"valuechange","selector":"#main_layout_components_series>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(2)>select.test-inputs-select-select","value":"1","time":2952,"target":"select"},{"time":2953,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":2962,"x":141,"y":37},{"type":"valuechange","selector":"#main_layout_components_series>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select>select.test-inputs-select-select","value":"0","time":4230,"target":"select"},{"time":4231,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":4242,"x":319,"y":29},{"type":"mousemove","time":4478,"x":296,"y":26},{"type":"valuechange","selector":"#main_layout_components_series>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(2)>select.test-inputs-select-select","value":"0","time":5563,"target":"select"},{"time":5564,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":5573,"x":199,"y":205},{"type":"mousemove","time":5783,"x":145,"y":327},{"type":"mousemove","time":5985,"x":488,"y":395},{"type":"mousemove","time":6210,"x":540,"y":418},{"type":"mousemove","time":6436,"x":659,"y":506},{"type":"mousemove","time":6676,"x":714,"y":523},{"type":"mousemove","time":6887,"x":747,"y":560},{"type":"mousemove","time":7096,"x":774,"y":578},{"type":"mousemove","time":7302,"x":783,"y":578},{"type":"mousedown","time":7569,"x":783,"y":578},{"type":"mousemove","time":7579,"x":782,"y":578},{"type":"mousemove","time":7862,"x":569,"y":499},{"type":"mousemove","time":8140,"x":568,"y":499},{"type":"mousemove","time":8410,"x":335,"y":377},{"type":"mousemove","time":8619,"x":510,"y":358},{"type":"mousemove","time":8885,"x":633,"y":439},{"type":"mousemove","time":9149,"x":691,"y":461},{"type":"mousemove","time":9436,"x":723,"y":512},{"type":"mousemove","time":9704,"x":760,"y":528},{"type":"mouseup","time":9898,"x":760,"y":528},{"time":9899,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":9907,"x":735,"y":512},{"type":"mousemove","time":10114,"x":130,"y":210},{"type":"mousemove","time":10344,"x":114,"y":153},{"type":"mousemove","time":10586,"x":109,"y":154},{"type":"mousewheel","time":10638,"x":109,"y":154,"deltaY":1},{"type":"mousewheel","time":10700,"x":109,"y":154,"deltaY":12},{"type":"mousewheel","time":10763,"x":109,"y":154,"deltaY":122},{"type":"mousewheel","time":10822,"x":109,"y":154,"deltaY":20},{"type":"mousewheel","time":10878,"x":109,"y":154,"deltaY":36},{"type":"mousewheel","time":10932,"x":109,"y":154,"deltaY":351},{"type":"mousewheel","time":10986,"x":109,"y":154,"deltaY":107},{"type":"mousewheel","time":11043,"x":109,"y":154,"deltaY":106},{"type":"mousewheel","time":11095,"x":109,"y":154,"deltaY":57},{"type":"mousewheel","time":11151,"x":109,"y":154,"deltaY":55},{"type":"mousewheel","time":11224,"x":109,"y":154,"deltaY":-1},{"type":"mousewheel","time":11281,"x":109,"y":154,"deltaY":-19},{"type":"mousewheel","time":11337,"x":109,"y":154,"deltaY":-16},{"type":"mousewheel","time":11395,"x":109,"y":154,"deltaY":-3},{"type":"mousewheel","time":11454,"x":109,"y":154,"deltaY":0},{"type":"mousemove","time":11589,"x":109,"y":154},{"type":"mousemove","time":11801,"x":105,"y":253},{"type":"mousewheel","time":12015,"x":105,"y":253,"deltaY":1},{"type":"mousewheel","time":12074,"x":105,"y":253,"deltaY":25},{"type":"mousewheel","time":12130,"x":105,"y":253,"deltaY":51},{"type":"mousewheel","time":12185,"x":105,"y":253,"deltaY":15},{"type":"mousewheel","time":12241,"x":105,"y":253,"deltaY":14},{"type":"mousewheel","time":12298,"x":105,"y":253,"deltaY":133},{"type":"mousewheel","time":12371,"x":105,"y":253,"deltaY":39},{"type":"mousewheel","time":12433,"x":105,"y":253,"deltaY":47},{"type":"mousewheel","time":12493,"x":105,"y":253,"deltaY":20},{"type":"mousewheel","time":12547,"x":105,"y":253,"deltaY":6},{"type":"mousewheel","time":12614,"x":105,"y":253,"deltaY":-1},{"type":"mousewheel","time":12670,"x":105,"y":253,"deltaY":-75},{"type":"mousewheel","time":12728,"x":105,"y":253,"deltaY":-22},{"type":"mousewheel","time":12783,"x":105,"y":253,"deltaY":-2},{"type":"mousewheel","time":12836,"x":105,"y":253,"deltaY":-7},{"type":"mousewheel","time":12895,"x":105,"y":253,"deltaY":-65},{"type":"mousewheel","time":12952,"x":105,"y":253,"deltaY":-7},{"type":"mousemove","time":12984,"x":123,"y":252},{"type":"mousemove","time":13214,"x":207,"y":240},{"type":"mousemove","time":13520,"x":216,"y":242},{"type":"mousemove","time":13727,"x":169,"y":373},{"type":"mousemove","time":13937,"x":140,"y":401},{"type":"mousemove","time":14144,"x":150,"y":398},{"type":"mousemove","time":14351,"x":160,"y":395},{"type":"mousemove","time":14577,"x":160,"y":400},{"type":"mousemove","time":14773,"x":157,"y":400},{"type":"mousemove","time":14980,"x":144,"y":411},{"type":"mousedown","time":15199,"x":144,"y":411},{"type":"mousemove","time":15210,"x":139,"y":411},{"type":"mousemove","time":15473,"x":64,"y":405},{"type":"mouseup","time":15630,"x":64,"y":405},{"time":15631,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":15732,"x":92,"y":401},{"type":"mousemove","time":15938,"x":71,"y":408},{"type":"mousemove","time":16176,"x":60,"y":411},{"type":"mousemove","time":16399,"x":58,"y":410},{"type":"mousemove","time":16634,"x":65,"y":412},{"type":"mousedown","time":17021,"x":65,"y":412},{"type":"mousemove","time":17031,"x":70,"y":410},{"type":"mousemove","time":17286,"x":136,"y":411},{"type":"mouseup","time":17439,"x":136,"y":411},{"time":17440,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":17505,"x":105,"y":462},{"type":"mousemove","time":17723,"x":102,"y":469},{"type":"mousedown","time":17948,"x":102,"y":469},{"type":"mousemove","time":17959,"x":102,"y":471},{"type":"mousemove","time":18172,"x":99,"y":511},{"type":"mouseup","time":18254,"x":99,"y":511},{"time":18255,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":18392,"x":482,"y":268},{"type":"mousemove","time":18598,"x":513,"y":231},{"type":"mousemove","time":18820,"x":429,"y":269},{"type":"mousewheel","time":19025,"x":429,"y":269,"deltaY":1},{"type":"mousewheel","time":19062,"x":429,"y":269,"deltaY":4},{"type":"mousewheel","time":19099,"x":429,"y":269,"deltaY":11},{"type":"mousewheel","time":19140,"x":429,"y":269,"deltaY":12},{"type":"mousewheel","time":19181,"x":429,"y":269,"deltaY":4},{"type":"mousewheel","time":19217,"x":429,"y":269,"deltaY":3},{"type":"mousewheel","time":19257,"x":429,"y":269,"deltaY":2},{"type":"mousemove","time":19511,"x":431,"y":256},{"type":"mousemove","time":19734,"x":489,"y":158},{"type":"mousemove","time":19934,"x":492,"y":177},{"type":"mousemove","time":20151,"x":492,"y":179},{"type":"mousewheel","time":20206,"x":492,"y":179,"deltaY":1},{"type":"mousewheel","time":20241,"x":492,"y":179,"deltaY":4},{"type":"mousewheel","time":20281,"x":492,"y":179,"deltaY":12},{"type":"mousewheel","time":20320,"x":492,"y":179,"deltaY":16},{"type":"mousewheel","time":20366,"x":492,"y":179,"deltaY":13},{"type":"mousewheel","time":20419,"x":492,"y":179,"deltaY":4},{"type":"mousewheel","time":20643,"x":492,"y":179,"deltaY":-1},{"type":"mousewheel","time":20683,"x":492,"y":179,"deltaY":-1},{"type":"mousewheel","time":20727,"x":492,"y":179,"deltaY":-9},{"type":"mousewheel","time":20769,"x":492,"y":179,"deltaY":-11},{"type":"mousewheel","time":20808,"x":492,"y":179,"deltaY":-20},{"type":"mousewheel","time":20847,"x":492,"y":179,"deltaY":-12},{"type":"mousewheel","time":20885,"x":492,"y":179,"deltaY":-8},{"type":"mousewheel","time":20924,"x":492,"y":179,"deltaY":-9},{"type":"mousemove","time":21118,"x":496,"y":179},{"type":"mousemove","time":21328,"x":608,"y":195},{"type":"mousemove","time":21529,"x":624,"y":199},{"type":"mousemove","time":21743,"x":652,"y":219},{"type":"mousemove","time":21946,"x":677,"y":231},{"type":"mousemove","time":22152,"x":675,"y":274},{"type":"mousemove","time":22405,"x":668,"y":268},{"type":"mousewheel","time":22822,"x":668,"y":268,"deltaY":-1},{"type":"mousewheel","time":22908,"x":668,"y":268,"deltaY":-4},{"type":"mousewheel","time":22988,"x":668,"y":268,"deltaY":-4},{"type":"mousewheel","time":23069,"x":668,"y":268,"deltaY":-2},{"type":"mousewheel","time":23281,"x":668,"y":268,"deltaY":-1},{"type":"mousewheel","time":23366,"x":668,"y":268,"deltaY":-4},{"type":"mousewheel","time":23454,"x":668,"y":268,"deltaY":-1},{"type":"mousemove","time":23883,"x":665,"y":268},{"type":"mousemove","time":24087,"x":608,"y":370},{"type":"mousemove","time":24313,"x":606,"y":378},{"type":"mousemove","time":24363,"x":607,"y":379},{"type":"mousemove","time":24579,"x":622,"y":397},{"type":"mousemove","time":24797,"x":632,"y":397},{"type":"mousedown","time":24956,"x":638,"y":399},{"type":"mousemove","time":25041,"x":638,"y":399},{"type":"mouseup","time":25076,"x":638,"y":399},{"time":25077,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":25324,"x":638,"y":396},{"type":"mousemove","time":25560,"x":612,"y":345},{"type":"mousemove","time":25818,"x":605,"y":340},{"type":"mousemove","time":26028,"x":437,"y":378},{"type":"mousemove","time":26262,"x":424,"y":383},{"type":"mousemove","time":26462,"x":486,"y":477},{"type":"mousemove","time":26684,"x":666,"y":480},{"type":"mousemove","time":26897,"x":787,"y":511},{"type":"mousemove","time":27100,"x":764,"y":527},{"type":"mousemove","time":27317,"x":761,"y":528},{"type":"mousemove","time":27874,"x":761,"y":528},{"type":"mousemove","time":28083,"x":760,"y":507}],"scrollY":60,"scrollX":0,"timestamp":1750344079733},{"name":"Action 2","ops":[{"type":"mousedown","time":495,"x":474,"y":428},{"type":"mouseup","time":612,"x":474,"y":428},{"time":613,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":753,"x":475,"y":428},{"type":"mousemove","time":956,"x":507,"y":427},{"type":"mousemove","time":1165,"x":515,"y":426},{"type":"mousedown","time":1183,"x":515,"y":426},{"type":"mouseup","time":1313,"x":515,"y":426},{"time":1314,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":1553,"x":517,"y":426},{"type":"mousedown","time":1681,"x":523,"y":426},{"type":"mousemove","time":1764,"x":523,"y":426},{"type":"mouseup","time":1814,"x":523,"y":426},{"time":1815,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":2069,"x":522,"y":426},{"type":"mousemove","time":2269,"x":454,"y":434},{"type":"mousemove","time":2470,"x":365,"y":444},{"type":"mousedown","time":2664,"x":366,"y":427},{"type":"mousemove","time":2698,"x":366,"y":427},{"type":"mouseup","time":2782,"x":366,"y":427},{"time":2783,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":2903,"x":366,"y":427},{"type":"mousemove","time":3113,"x":392,"y":427},{"type":"mousedown","time":3202,"x":392,"y":427},{"type":"mouseup","time":3330,"x":392,"y":427},{"time":3331,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":3736,"x":392,"y":427},{"type":"mousedown","time":3791,"x":392,"y":427},{"type":"mouseup","time":3913,"x":392,"y":427},{"time":3914,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":3955,"x":392,"y":427},{"type":"mousedown","time":4281,"x":392,"y":427},{"type":"mouseup","time":4414,"x":392,"y":427},{"time":4415,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":4986,"x":392,"y":427},{"type":"mousemove","time":5186,"x":594,"y":401},{"type":"mousemove","time":5419,"x":714,"y":401},{"type":"mousedown","time":5464,"x":714,"y":401},{"type":"mouseup","time":5583,"x":714,"y":401},{"time":5584,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":5651,"x":714,"y":401},{"type":"mousemove","time":7469,"x":713,"y":401},{"type":"mousemove","time":7670,"x":379,"y":377},{"type":"mousemove","time":7890,"x":351,"y":406},{"type":"mousemove","time":8112,"x":362,"y":425},{"type":"mousedown","time":8183,"x":363,"y":426},{"type":"mousemove","time":8316,"x":363,"y":426},{"type":"mouseup","time":8481,"x":363,"y":426},{"time":8482,"delay":400,"type":"screenshot-auto"}],"scrollY":454,"scrollX":0,"timestamp":1750344113153},{"name":"Action 3","ops":[{"type":"mousemove","time":134,"x":761,"y":465},{"type":"mousemove","time":334,"x":774,"y":530},{"type":"mousemove","time":542,"x":785,"y":579},{"type":"mousemove","time":759,"x":787,"y":583},{"type":"mousedown","time":1059,"x":787,"y":583},{"type":"mousemove","time":1068,"x":787,"y":583},{"type":"mousemove","time":1278,"x":726,"y":570},{"type":"mousemove","time":1483,"x":693,"y":571},{"type":"mousemove","time":1691,"x":671,"y":572},{"type":"mousemove","time":1901,"x":669,"y":572},{"type":"mousemove","time":2101,"x":687,"y":496},{"type":"mousemove","time":2309,"x":707,"y":446},{"type":"mousemove","time":2500,"x":709,"y":444},{"type":"mousemove","time":2701,"x":727,"y":400},{"type":"mousemove","time":2910,"x":731,"y":381},{"type":"mousemove","time":3117,"x":732,"y":359},{"type":"mousemove","time":3317,"x":723,"y":329},{"type":"mousemove","time":3527,"x":723,"y":329},{"type":"mousemove","time":3584,"x":723,"y":329},{"type":"mousemove","time":3783,"x":721,"y":397},{"type":"mousemove","time":3990,"x":699,"y":483},{"type":"mousemove","time":4192,"x":697,"y":490},{"type":"mousemove","time":4300,"x":697,"y":490},{"type":"mousemove","time":4501,"x":513,"y":522},{"type":"mousemove","time":4711,"x":458,"y":546},{"type":"mousemove","time":4983,"x":454,"y":548},{"type":"mousemove","time":5183,"x":334,"y":531},{"type":"mousemove","time":5395,"x":305,"y":532},{"type":"mousemove","time":5534,"x":305,"y":532},{"type":"mousemove","time":5744,"x":204,"y":527},{"type":"mousemove","time":5951,"x":156,"y":525},{"type":"mousemove","time":6152,"x":116,"y":527},{"type":"mousemove","time":6364,"x":79,"y":530},{"type":"mousemove","time":6567,"x":57,"y":536},{"type":"mousemove","time":6777,"x":44,"y":539},{"type":"mousemove","time":7000,"x":42,"y":540},{"type":"mousemove","time":7214,"x":34,"y":541},{"type":"mousemove","time":7433,"x":25,"y":542},{"type":"mousemove","time":7635,"x":30,"y":541},{"type":"mousemove","time":7835,"x":47,"y":541},{"type":"mousemove","time":8035,"x":63,"y":539},{"type":"mousemove","time":8235,"x":83,"y":535},{"type":"mousemove","time":8444,"x":137,"y":533},{"type":"mousemove","time":8651,"x":474,"y":510},{"type":"mousemove","time":8851,"x":490,"y":506},{"type":"mousemove","time":9051,"x":690,"y":501},{"type":"mousemove","time":9262,"x":744,"y":521},{"type":"mousemove","time":9467,"x":740,"y":539},{"type":"mousemove","time":9667,"x":756,"y":552},{"type":"mousemove","time":9869,"x":760,"y":559},{"type":"mousemove","time":10079,"x":761,"y":561},{"type":"mousemove","time":10377,"x":761,"y":562},{"type":"mouseup","time":10544,"x":761,"y":562},{"time":10545,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":10554,"x":725,"y":520},{"type":"mousemove","time":10761,"x":267,"y":323},{"type":"mousemove","time":11034,"x":265,"y":320},{"type":"mousemove","time":11234,"x":240,"y":296},{"type":"mousemove","time":11444,"x":176,"y":214},{"type":"mousemove","time":11652,"x":152,"y":175},{"type":"mousemove","time":11861,"x":141,"y":155},{"type":"mousemove","time":12077,"x":141,"y":155},{"type":"valuechange","selector":"#main_text_overflow_and_tooltip>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select>select.test-inputs-select-select","value":"1","time":12784,"target":"select"},{"time":12785,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":12834,"x":139,"y":157},{"type":"valuechange","selector":"#main_text_overflow_and_tooltip>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select>select.test-inputs-select-select","value":"2","time":13866,"target":"select"},{"time":13867,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":13917,"x":142,"y":155},{"type":"valuechange","selector":"#main_text_overflow_and_tooltip>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select>select.test-inputs-select-select","value":"3","time":14885,"target":"select"},{"time":14886,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":14950,"x":144,"y":151},{"type":"valuechange","selector":"#main_text_overflow_and_tooltip>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select>select.test-inputs-select-select","value":"4","time":15934,"target":"select"},{"time":15935,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":16017,"x":142,"y":159},{"type":"mousemove","time":16227,"x":142,"y":158},{"type":"valuechange","selector":"#main_text_overflow_and_tooltip>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select>select.test-inputs-select-select","value":"0","time":17483,"target":"select"},{"time":17484,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":17494,"x":240,"y":94},{"type":"valuechange","selector":"#main_text_overflow_and_tooltip>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(2)>select.test-inputs-select-select","value":"1","time":18863,"target":"select"},{"time":18864,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":18901,"x":337,"y":163},{"type":"mousemove","time":19109,"x":338,"y":153},{"type":"valuechange","selector":"#main_text_overflow_and_tooltip>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(2)>select.test-inputs-select-select","value":"0","time":20223,"target":"select"},{"time":20224,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":20267,"x":482,"y":166},{"type":"mousemove","time":20467,"x":506,"y":163},{"type":"mousemove","time":20678,"x":509,"y":147},{"type":"valuechange","selector":"#main_text_overflow_and_tooltip>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(3)>select.test-inputs-select-select","value":"1","time":22423,"target":"select"},{"time":22424,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":22467,"x":489,"y":179},{"type":"mousemove","time":22668,"x":365,"y":296},{"type":"mousemove","time":22876,"x":312,"y":317},{"type":"mousemove","time":23093,"x":277,"y":337},{"type":"mousemove","time":23334,"x":278,"y":337},{"type":"mousemove","time":23535,"x":279,"y":332},{"type":"mousemove","time":23744,"x":103,"y":408},{"type":"mousemove","time":23951,"x":124,"y":437},{"type":"mousemove","time":24160,"x":122,"y":389},{"type":"mousemove","time":24376,"x":103,"y":380},{"type":"mousemove","time":24584,"x":107,"y":390},{"type":"mousemove","time":24784,"x":114,"y":443},{"type":"mousemove","time":24984,"x":114,"y":449},{"type":"mousemove","time":25185,"x":200,"y":380},{"type":"mousemove","time":25395,"x":204,"y":326},{"type":"mousemove","time":25600,"x":462,"y":195},{"type":"mousemove","time":25801,"x":495,"y":167},{"type":"mousemove","time":26011,"x":524,"y":154},{"type":"mousemove","time":26227,"x":515,"y":146},{"type":"valuechange","selector":"#main_text_overflow_and_tooltip>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(3)>select.test-inputs-select-select","value":"0","time":27968,"target":"select"},{"time":27969,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":27991,"x":579,"y":137},{"type":"mousedown","time":28095,"x":648,"y":139},{"type":"mouseup","time":28117,"x":648,"y":139},{"time":28118,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":28194,"x":648,"y":138},{"type":"mousemove","time":28401,"x":648,"y":149},{"type":"mousemove","time":28611,"x":648,"y":149},{"type":"valuechange","selector":"#main_text_overflow_and_tooltip>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(4)>select.test-inputs-select-select","value":"1","time":29318,"target":"select"},{"time":29319,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":29351,"x":585,"y":218},{"type":"mousemove","time":29561,"x":474,"y":314},{"type":"mousemove","time":29767,"x":417,"y":340},{"type":"mousemove","time":29988,"x":319,"y":414},{"type":"mousemove","time":30195,"x":273,"y":371},{"type":"mousemove","time":30396,"x":282,"y":310},{"type":"mousemove","time":30601,"x":321,"y":316},{"type":"mousemove","time":30811,"x":349,"y":316},{"type":"mousemove","time":31017,"x":271,"y":299},{"type":"mousemove","time":31218,"x":705,"y":187},{"type":"mousemove","time":31427,"x":709,"y":141},{"type":"mousemove","time":31643,"x":684,"y":157},{"type":"valuechange","selector":"#main_text_overflow_and_tooltip>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(4)>select.test-inputs-select-select","value":"0","time":32651,"target":"select"},{"time":32652,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":32686,"x":431,"y":185},{"type":"mousemove","time":32895,"x":181,"y":189},{"type":"mousemove","time":33101,"x":147,"y":183},{"type":"mousemove","time":33311,"x":118,"y":172},{"type":"valuechange","selector":"#main_text_overflow_and_tooltip>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(5)>select.test-inputs-select-select","value":"1","time":34202,"target":"select"},{"time":34203,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":34251,"x":239,"y":272},{"type":"mousemove","time":34460,"x":207,"y":256},{"type":"mousemove","time":34684,"x":239,"y":272},{"type":"mousemove","time":34894,"x":220,"y":186},{"type":"mousemove","time":35095,"x":220,"y":181},{"type":"mousemove","time":35311,"x":221,"y":176},{"type":"mousedown","time":35760,"x":221,"y":176},{"type":"mousemove","time":35770,"x":221,"y":176},{"type":"mousemove","time":35978,"x":223,"y":176},{"type":"mousemove","time":36193,"x":223,"y":176},{"type":"mousemove","time":36234,"x":224,"y":176},{"type":"mousemove","time":36446,"x":226,"y":177},{"type":"mousemove","time":36695,"x":228,"y":177},{"type":"mousemove","time":36917,"x":229,"y":177},{"type":"mousemove","time":37184,"x":229,"y":177},{"type":"mousemove","time":37384,"x":231,"y":177},{"type":"mousemove","time":37593,"x":234,"y":179},{"type":"mousemove","time":37828,"x":238,"y":180},{"type":"mousemove","time":38111,"x":239,"y":180},{"type":"mousemove","time":38317,"x":229,"y":181},{"type":"mousemove","time":38526,"x":229,"y":181},{"type":"mousemove","time":38761,"x":221,"y":181},{"type":"mousemove","time":38968,"x":221,"y":181},{"type":"mousemove","time":39168,"x":227,"y":181},{"type":"mousemove","time":39377,"x":228,"y":182},{"type":"mouseup","time":40163,"x":228,"y":182},{"time":40164,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":40175,"x":259,"y":181},{"type":"mousemove","time":40383,"x":405,"y":172},{"type":"mousemove","time":40599,"x":413,"y":171},{"type":"valuechange","selector":"#main_text_overflow_and_tooltip>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(6)>select.test-inputs-select-select","value":"1","time":41384,"target":"select"},{"time":41385,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":41435,"x":449,"y":183},{"type":"mousemove","time":41644,"x":501,"y":176},{"type":"mousemove","time":41862,"x":529,"y":171},{"type":"mousemove","time":42067,"x":521,"y":175},{"type":"mousedown","time":42478,"x":521,"y":175},{"type":"mousemove","time":42489,"x":521,"y":175},{"type":"mousemove","time":42694,"x":531,"y":177},{"type":"mousemove","time":42962,"x":533,"y":177},{"type":"mousemove","time":43165,"x":554,"y":178},{"type":"mousemove","time":43365,"x":571,"y":179},{"type":"mousemove","time":43568,"x":560,"y":181},{"type":"mousemove","time":43777,"x":547,"y":182},{"type":"mousemove","time":44061,"x":536,"y":182},{"type":"mousemove","time":44333,"x":535,"y":182},{"type":"mousemove","time":44544,"x":531,"y":182},{"type":"mousemove","time":44650,"x":531,"y":182},{"type":"mousemove","time":44861,"x":527,"y":182},{"type":"mousemove","time":45084,"x":526,"y":182},{"type":"mousemove","time":45284,"x":518,"y":182},{"type":"mousemove","time":45495,"x":518,"y":182},{"type":"mousemove","time":45710,"x":521,"y":183},{"type":"mousemove","time":46618,"x":520,"y":183},{"type":"mousemove","time":46828,"x":501,"y":182},{"type":"mousemove","time":47034,"x":501,"y":182},{"type":"mousemove","time":47236,"x":520,"y":182},{"type":"mousemove","time":47501,"x":520,"y":182},{"type":"mousemove","time":47711,"x":523,"y":183},{"type":"mousemove","time":47934,"x":526,"y":183},{"type":"mouseup","time":48310,"x":526,"y":183},{"time":48311,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":48322,"x":509,"y":185},{"type":"mousemove","time":48528,"x":347,"y":201},{"type":"mousemove","time":48735,"x":265,"y":183},{"type":"mousemove","time":48945,"x":214,"y":175},{"type":"mousemove","time":49151,"x":218,"y":175},{"type":"mousemove","time":49361,"x":230,"y":177},{"type":"mousedown","time":49594,"x":230,"y":177},{"type":"mousemove","time":49605,"x":230,"y":177},{"type":"mousemove","time":49811,"x":212,"y":178},{"type":"mousemove","time":50011,"x":202,"y":177},{"type":"mousemove","time":50212,"x":195,"y":177},{"type":"mousemove","time":50468,"x":195,"y":177},{"type":"mousemove","time":50678,"x":190,"y":177},{"type":"mousemove","time":50894,"x":187,"y":178},{"type":"mousemove","time":51135,"x":187,"y":178},{"type":"mousemove","time":51345,"x":207,"y":179},{"type":"mousemove","time":51561,"x":216,"y":179},{"type":"mousemove","time":51762,"x":218,"y":180},{"type":"mousemove","time":51884,"x":219,"y":180},{"type":"mousemove","time":52095,"x":220,"y":180},{"type":"mousemove","time":52300,"x":221,"y":181},{"type":"mousemove","time":52501,"x":224,"y":182},{"type":"mousemove","time":52835,"x":224,"y":182},{"type":"mousemove","time":53046,"x":226,"y":182},{"type":"mouseup","time":53628,"x":226,"y":182},{"time":53629,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":53640,"x":213,"y":187},{"type":"mousemove","time":53847,"x":176,"y":201},{"type":"mousemove","time":54062,"x":174,"y":201},{"type":"mousemove","time":54279,"x":174,"y":201},{"type":"mousedown","time":54411,"x":174,"y":201},{"type":"mousemove","time":54423,"x":174,"y":201},{"type":"mousemove","time":54629,"x":181,"y":201},{"type":"mousemove","time":54884,"x":186,"y":201},{"type":"mousemove","time":55095,"x":192,"y":202},{"type":"mousemove","time":55301,"x":183,"y":202},{"type":"mousemove","time":55505,"x":172,"y":203},{"type":"mousemove","time":55712,"x":163,"y":204},{"type":"mousemove","time":55942,"x":146,"y":204},{"type":"mousemove","time":56161,"x":129,"y":204},{"type":"mousemove","time":56361,"x":122,"y":204},{"type":"mousemove","time":56578,"x":122,"y":204},{"type":"mousemove","time":56617,"x":122,"y":204},{"type":"mousemove","time":56817,"x":126,"y":206},{"type":"mousemove","time":57034,"x":128,"y":207},{"type":"mousemove","time":57244,"x":129,"y":207},{"type":"mousemove","time":57460,"x":129,"y":208},{"type":"mousemove","time":57716,"x":130,"y":208},{"type":"mousemove","time":57932,"x":134,"y":208},{"type":"mousemove","time":58214,"x":136,"y":208},{"type":"mouseup","time":58729,"x":136,"y":208},{"time":58730,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":58742,"x":174,"y":205},{"type":"mousemove","time":58946,"x":341,"y":201},{"type":"mousemove","time":59150,"x":398,"y":194},{"type":"mousemove","time":59361,"x":399,"y":194},{"type":"mousedown","time":59628,"x":399,"y":194},{"type":"mousemove","time":59639,"x":399,"y":194},{"type":"mousemove","time":59845,"x":375,"y":195},{"type":"mousemove","time":60051,"x":370,"y":196},{"type":"mousemove","time":60268,"x":352,"y":198},{"type":"mousemove","time":60480,"x":339,"y":199},{"type":"mousemove","time":60683,"x":325,"y":199},{"type":"mousemove","time":60891,"x":378,"y":205},{"type":"mousemove","time":61116,"x":409,"y":205},{"type":"mousemove","time":61317,"x":423,"y":205},{"type":"mousemove","time":61529,"x":421,"y":203},{"type":"mousemove","time":61735,"x":393,"y":203},{"type":"mousemove","time":61943,"x":380,"y":203},{"type":"mousemove","time":62144,"x":369,"y":203},{"type":"mousemove","time":62351,"x":365,"y":203},{"type":"mousemove","time":62566,"x":360,"y":202},{"type":"mouseup","time":62861,"x":360,"y":202},{"time":62862,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":62874,"x":340,"y":199},{"type":"mousemove","time":63079,"x":208,"y":179},{"type":"mousemove","time":63283,"x":167,"y":167},{"type":"mousemove","time":63483,"x":150,"y":156},{"type":"valuechange","selector":"#main_text_overflow_and_tooltip>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select>select.test-inputs-select-select","value":"1","time":64941,"target":"select"},{"time":64942,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":65015,"x":190,"y":214},{"type":"mousemove","time":65216,"x":256,"y":224},{"type":"mousemove","time":65416,"x":197,"y":203},{"type":"mousemove","time":65616,"x":159,"y":196},{"type":"mousemove","time":65816,"x":139,"y":194},{"type":"mousemove","time":66027,"x":138,"y":194},{"type":"mousedown","time":66228,"x":138,"y":194},{"type":"mousemove","time":66240,"x":139,"y":194},{"type":"mousemove","time":66455,"x":159,"y":195},{"type":"mousemove","time":66665,"x":164,"y":195},{"type":"mousemove","time":66867,"x":167,"y":196},{"type":"mousemove","time":67078,"x":135,"y":195},{"type":"mousemove","time":67283,"x":123,"y":196},{"type":"mousemove","time":67516,"x":123,"y":196},{"type":"mousemove","time":67716,"x":129,"y":197},{"type":"mousemove","time":67928,"x":134,"y":199},{"type":"mousemove","time":68133,"x":137,"y":199},{"type":"mousemove","time":68344,"x":140,"y":200},{"type":"mousemove","time":68559,"x":142,"y":200},{"type":"mouseup","time":68777,"x":142,"y":200},{"time":68778,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":68790,"x":163,"y":200},{"type":"mousemove","time":68993,"x":306,"y":208},{"type":"mousemove","time":69199,"x":323,"y":207},{"type":"mousemove","time":69399,"x":347,"y":199},{"type":"mousemove","time":69600,"x":351,"y":199},{"type":"mousemove","time":69811,"x":352,"y":199},{"type":"mousedown","time":70012,"x":352,"y":199},{"type":"mousemove","time":70025,"x":353,"y":199},{"type":"mousemove","time":70229,"x":367,"y":198},{"type":"mousemove","time":70441,"x":381,"y":197},{"type":"mousemove","time":70642,"x":405,"y":196},{"type":"mousemove","time":70845,"x":412,"y":196},{"type":"mousemove","time":71054,"x":401,"y":198},{"type":"mousemove","time":71265,"x":375,"y":199},{"type":"mousemove","time":71466,"x":344,"y":198},{"type":"mousemove","time":71677,"x":323,"y":197},{"type":"mousemove","time":71882,"x":316,"y":195},{"type":"mousemove","time":72083,"x":350,"y":202},{"type":"mousemove","time":72299,"x":359,"y":202},{"type":"mousemove","time":72499,"x":363,"y":202},{"type":"mousemove","time":72699,"x":370,"y":202},{"type":"mousemove","time":72911,"x":370,"y":203},{"type":"mouseup","time":73244,"x":370,"y":203},{"time":73245,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":73256,"x":360,"y":203},{"type":"mousemove","time":73462,"x":220,"y":167},{"type":"mousemove","time":73666,"x":163,"y":150},{"type":"valuechange","selector":"#main_text_overflow_and_tooltip>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select>select.test-inputs-select-select","value":"0","time":76316,"target":"select"},{"time":76317,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":76366,"x":145,"y":178},{"type":"mousemove","time":76566,"x":147,"y":192},{"type":"mousemove","time":76767,"x":164,"y":217},{"type":"mousemove","time":76979,"x":189,"y":220},{"type":"mousemove","time":77195,"x":190,"y":220},{"type":"valuechange","selector":"#main_text_overflow_and_tooltip>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(10)>select.test-inputs-select-select","value":"1","time":78160,"target":"select"},{"time":78161,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":78249,"x":226,"y":219},{"type":"mousemove","time":78449,"x":226,"y":219},{"type":"valuechange","selector":"#main_text_overflow_and_tooltip>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(10)>select.test-inputs-select-select","value":"2","time":79664,"target":"select"},{"time":79665,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":79700,"x":237,"y":226},{"type":"mousemove","time":79913,"x":239,"y":221},{"type":"mousemove","time":80128,"x":239,"y":221},{"type":"valuechange","selector":"#main_text_overflow_and_tooltip>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(10)>select.test-inputs-select-select","value":"3","time":80924,"target":"select"},{"time":80925,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":80958,"x":243,"y":220},{"type":"mousemove","time":81162,"x":242,"y":224},{"type":"valuechange","selector":"#main_text_overflow_and_tooltip>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(10)>select.test-inputs-select-select","value":"5","time":82050,"target":"select"},{"time":82051,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":82100,"x":252,"y":219},{"type":"mousemove","time":82310,"x":252,"y":218},{"type":"valuechange","selector":"#main_text_overflow_and_tooltip>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(10)>select.test-inputs-select-select","value":"7","time":83249,"target":"select"},{"time":83250,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":83316,"x":261,"y":225},{"type":"mousemove","time":83527,"x":261,"y":222},{"type":"valuechange","selector":"#main_text_overflow_and_tooltip>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(10)>select.test-inputs-select-select","value":"9","time":84467,"target":"select"},{"time":84468,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":84532,"x":266,"y":212},{"type":"mousemove","time":84732,"x":266,"y":215},{"type":"mousemove","time":84944,"x":266,"y":217},{"type":"valuechange","selector":"#main_text_overflow_and_tooltip>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(10)>select.test-inputs-select-select","value":"11","time":86017,"target":"select"},{"time":86018,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":86099,"x":261,"y":226},{"type":"mousemove","time":86299,"x":252,"y":227},{"type":"mousemove","time":86510,"x":252,"y":215},{"type":"valuechange","selector":"#main_text_overflow_and_tooltip>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(10)>select.test-inputs-select-select","value":"8","time":87416,"target":"select"},{"time":87417,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":87453,"x":345,"y":187},{"type":"mousemove","time":87660,"x":485,"y":219},{"type":"mousemove","time":87878,"x":483,"y":221},{"type":"mousemove","time":88165,"x":482,"y":220},{"type":"valuechange","selector":"#main_text_overflow_and_tooltip>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(11)>select.test-inputs-select-select","value":"1","time":90099,"target":"select"},{"time":90100,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":90111,"x":480,"y":246},{"type":"mousemove","time":90899,"x":481,"y":245},{"type":"mousemove","time":91099,"x":485,"y":236},{"type":"mousemove","time":91310,"x":492,"y":222},{"type":"mousemove","time":91515,"x":310,"y":214},{"type":"mousemove","time":91716,"x":244,"y":223},{"type":"mousemove","time":91927,"x":244,"y":223},{"type":"valuechange","selector":"#main_text_overflow_and_tooltip>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(10)>select.test-inputs-select-select","value":"0","time":93332,"target":"select"},{"time":93333,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":93345,"x":347,"y":139},{"type":"mousemove","time":93546,"x":470,"y":190},{"type":"valuechange","selector":"#main_text_overflow_and_tooltip>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(11)>select.test-inputs-select-select","value":"2","time":95033,"target":"select"},{"time":95034,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":95098,"x":500,"y":221},{"type":"mousemove","time":95299,"x":500,"y":221},{"type":"valuechange","selector":"#main_text_overflow_and_tooltip>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(11)>select.test-inputs-select-select","value":"3","time":96284,"target":"select"},{"time":96285,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":96323,"x":509,"y":224},{"type":"mousemove","time":96532,"x":513,"y":218},{"type":"mousemove","time":96747,"x":513,"y":217},{"type":"valuechange","selector":"#main_text_overflow_and_tooltip>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(11)>select.test-inputs-select-select","value":"4","time":97704,"target":"select"},{"time":97705,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":97741,"x":496,"y":250},{"type":"mousemove","time":97944,"x":505,"y":226},{"type":"mousemove","time":98145,"x":505,"y":219},{"type":"mousemove","time":98360,"x":505,"y":218},{"type":"mousemove","time":99316,"x":620,"y":218},{"type":"mousemove","time":99526,"x":620,"y":218},{"type":"mousedown","time":99761,"x":620,"y":218},{"type":"mousemove","time":99773,"x":621,"y":218},{"type":"mousemove","time":99978,"x":623,"y":218},{"type":"mousemove","time":100186,"x":625,"y":218},{"type":"mousemove","time":100402,"x":628,"y":218},{"type":"mousemove","time":100610,"x":630,"y":218},{"type":"mousemove","time":100998,"x":630,"y":218},{"type":"mousemove","time":101203,"x":622,"y":218},{"type":"mousemove","time":101415,"x":616,"y":219},{"type":"mousemove","time":101625,"x":612,"y":220},{"type":"mousemove","time":101827,"x":611,"y":220},{"type":"mousemove","time":101965,"x":611,"y":220},{"type":"mousemove","time":102177,"x":611,"y":220},{"type":"mouseup","time":102280,"x":611,"y":220},{"time":102281,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":102293,"x":514,"y":218},{"type":"mousemove","time":102498,"x":272,"y":236},{"type":"mousemove","time":102708,"x":208,"y":251},{"type":"mousedown","time":102852,"x":196,"y":253},{"type":"mousemove","time":102929,"x":196,"y":253},{"type":"mouseup","time":102994,"x":196,"y":253},{"time":102995,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":103199,"x":196,"y":253},{"type":"mousemove","time":103410,"x":198,"y":245},{"type":"valuechange","selector":"#main_text_overflow_and_tooltip>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(13)>select.test-inputs-select-select","value":"1","time":104406,"target":"select"},{"time":104407,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":104437,"x":198,"y":262},{"type":"mousemove","time":104646,"x":208,"y":251},{"type":"mousemove","time":104851,"x":205,"y":248},{"type":"mousemove","time":105062,"x":201,"y":245},{"type":"mousemove","time":105182,"x":201,"y":246},{"type":"mousemove","time":105383,"x":605,"y":237},{"type":"mousemove","time":105583,"x":629,"y":228},{"type":"mousemove","time":105794,"x":616,"y":213},{"type":"mousemove","time":105999,"x":568,"y":214},{"type":"valuechange","selector":"#main_text_overflow_and_tooltip>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(11)>select.test-inputs-select-select","value":"2","time":107903,"target":"select"},{"time":107904,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":107946,"x":450,"y":215},{"type":"mousemove","time":108147,"x":375,"y":259},{"type":"mousemove","time":108416,"x":372,"y":262},{"type":"mousemove","time":108616,"x":214,"y":254},{"type":"mousemove","time":108827,"x":194,"y":251},{"type":"valuechange","selector":"#main_text_overflow_and_tooltip>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(13)>select.test-inputs-select-select","value":"2","time":110085,"target":"select"},{"time":110086,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":110149,"x":188,"y":245},{"type":"mousemove","time":110361,"x":188,"y":245},{"type":"valuechange","selector":"#main_text_overflow_and_tooltip>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(13)>select.test-inputs-select-select","value":"3","time":111294,"target":"select"},{"time":111295,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":111332,"x":204,"y":243},{"type":"mousemove","time":111532,"x":295,"y":236},{"type":"mousemove","time":111732,"x":458,"y":246},{"type":"mousemove","time":111944,"x":473,"y":246},{"type":"valuechange","selector":"#main_text_overflow_and_tooltip>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(14)>select.test-inputs-select-select","value":"1","time":113021,"target":"select"},{"time":113022,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":113070,"x":459,"y":245},{"type":"mousemove","time":113281,"x":459,"y":246},{"type":"valuechange","selector":"#main_text_overflow_and_tooltip>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(14)>select.test-inputs-select-select","value":"2","time":114068,"target":"select"},{"time":114069,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":114121,"x":451,"y":238},{"type":"mousemove","time":114327,"x":449,"y":240},{"type":"valuechange","selector":"#main_text_overflow_and_tooltip>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(14)>select.test-inputs-select-select","value":"3","time":115284,"target":"select"},{"time":115285,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":115310,"x":450,"y":270},{"type":"mousemove","time":115511,"x":506,"y":270},{"type":"mousemove","time":115716,"x":675,"y":250},{"type":"mousemove","time":115916,"x":726,"y":229},{"type":"mousemove","time":116117,"x":730,"y":212},{"type":"mousemove","time":116327,"x":730,"y":212},{"type":"mousedown","time":116395,"x":730,"y":212},{"type":"mouseup","time":116528,"x":730,"y":212},{"time":116529,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":116565,"x":730,"y":212},{"type":"mousemove","time":117232,"x":730,"y":212},{"type":"mousemove","time":117433,"x":242,"y":191},{"type":"mousemove","time":117646,"x":294,"y":206},{"type":"mousemove","time":117848,"x":450,"y":225},{"type":"mousemove","time":118057,"x":493,"y":222},{"type":"mousemove","time":118261,"x":493,"y":222},{"type":"valuechange","selector":"#main_text_overflow_and_tooltip>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(11)>select.test-inputs-select-select","value":"0","time":119098,"target":"select"},{"time":119099,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":119113,"x":451,"y":210},{"type":"mousemove","time":119314,"x":236,"y":310},{"type":"mousemove","time":119632,"x":214,"y":311},{"type":"mousemove","time":119834,"x":214,"y":277},{"type":"mousemove","time":120047,"x":211,"y":255},{"type":"mousemove","time":120251,"x":201,"y":245},{"type":"mousemove","time":120460,"x":200,"y":244},{"type":"valuechange","selector":"#main_text_overflow_and_tooltip>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(13)>select.test-inputs-select-select","value":"2","time":121451,"target":"select"},{"time":121452,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":121480,"x":185,"y":241},{"type":"mousemove","time":121681,"x":182,"y":249},{"type":"mousemove","time":121896,"x":181,"y":249},{"type":"valuechange","selector":"#main_text_overflow_and_tooltip>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(13)>select.test-inputs-select-select","value":"1","time":122786,"target":"select"},{"time":122787,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":122800,"x":303,"y":221},{"type":"mousemove","time":123006,"x":447,"y":226},{"type":"mousemove","time":123211,"x":443,"y":239},{"type":"valuechange","selector":"#main_text_overflow_and_tooltip>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(14)>select.test-inputs-select-select","value":"2","time":124293,"target":"select"},{"time":124294,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":124335,"x":443,"y":234},{"type":"mousemove","time":124545,"x":443,"y":234},{"type":"valuechange","selector":"#main_text_overflow_and_tooltip>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select:nth-child(14)>select.test-inputs-select-select","value":"1","time":125401,"target":"select"},{"time":125402,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":125429,"x":617,"y":244},{"type":"mousemove","time":125629,"x":685,"y":237},{"type":"mousemove","time":125833,"x":705,"y":229},{"type":"mousemove","time":126033,"x":705,"y":224},{"type":"mousedown","time":126162,"x":711,"y":215},{"type":"mousemove","time":126245,"x":711,"y":215},{"type":"mouseup","time":126297,"x":711,"y":215},{"time":126298,"delay":400,"type":"screenshot-auto"}],"scrollY":925.5,"scrollX":0,"timestamp":1750344403074}] \ No newline at end of file diff --git a/test/runTest/actions/matrix_application.json b/test/runTest/actions/matrix_application.json new file mode 100644 index 0000000000..10618b611f --- /dev/null +++ b/test/runTest/actions/matrix_application.json @@ -0,0 +1 @@ +[{"name":"Action 1","ops":[{"type":"mousemove","time":837,"x":747,"y":202},{"type":"mousemove","time":1037,"x":557,"y":172},{"type":"mousemove","time":1237,"x":445,"y":144},{"type":"mousemove","time":1447,"x":440,"y":142},{"type":"mousemove","time":1654,"x":398,"y":143},{"type":"mousemove","time":1863,"x":368,"y":149},{"type":"mousemove","time":2069,"x":366,"y":149},{"type":"mousemove","time":2270,"x":423,"y":149},{"type":"mousemove","time":2470,"x":454,"y":145},{"type":"mousemove","time":2671,"x":464,"y":141},{"type":"mousemove","time":2878,"x":466,"y":141},{"type":"mousemove","time":3111,"x":472,"y":139},{"type":"mousedown","time":3471,"x":472,"y":139},{"type":"mousemove","time":3479,"x":472,"y":139},{"type":"mousemove","time":3691,"x":400,"y":143},{"type":"mousemove","time":3901,"x":369,"y":145},{"type":"mousemove","time":4111,"x":357,"y":145},{"type":"mousemove","time":4387,"x":357,"y":145},{"type":"mousemove","time":4598,"x":392,"y":147},{"type":"mousemove","time":4813,"x":449,"y":147},{"type":"mousemove","time":5013,"x":516,"y":140},{"type":"mouseup","time":5228,"x":517,"y":139},{"time":5229,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":5236,"x":491,"y":139},{"type":"mousemove","time":5437,"x":361,"y":144},{"type":"mousemove","time":5637,"x":342,"y":146},{"type":"mousemove","time":5846,"x":331,"y":144},{"type":"mousedown","time":6122,"x":331,"y":144},{"type":"mousemove","time":6129,"x":331,"y":144},{"type":"mousemove","time":6343,"x":405,"y":146},{"type":"mousemove","time":6546,"x":427,"y":143},{"type":"mousemove","time":6752,"x":427,"y":143},{"type":"mousemove","time":6959,"x":343,"y":149},{"type":"mousemove","time":7163,"x":333,"y":151},{"type":"mouseup","time":7319,"x":333,"y":151},{"time":7320,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":7327,"x":337,"y":154},{"type":"mousemove","time":7531,"x":431,"y":244},{"type":"mousemove","time":7736,"x":453,"y":257},{"type":"mousemove","time":7937,"x":453,"y":258},{"type":"mousemove","time":8053,"x":451,"y":260},{"type":"mousemove","time":8253,"x":407,"y":290},{"type":"mousemove","time":8462,"x":400,"y":295},{"type":"mousemove","time":8670,"x":469,"y":347},{"type":"mousemove","time":8879,"x":471,"y":348},{"type":"mousemove","time":8987,"x":470,"y":348},{"type":"mousemove","time":9196,"x":466,"y":342},{"type":"mousemove","time":9411,"x":466,"y":342}],"scrollY":403,"scrollX":0,"timestamp":1748966383025},{"name":"Action 2","ops":[{"type":"mousemove","time":564,"x":745,"y":364},{"type":"mousemove","time":776,"x":481,"y":430},{"type":"mousemove","time":979,"x":342,"y":470},{"type":"mousemove","time":1188,"x":332,"y":471},{"type":"mousemove","time":1397,"x":107,"y":536},{"type":"mousemove","time":1597,"x":60,"y":560},{"type":"mousemove","time":1797,"x":76,"y":563},{"type":"mousemove","time":2004,"x":77,"y":562},{"type":"mousemove","time":2229,"x":81,"y":560},{"type":"mousedown","time":2548,"x":81,"y":560},{"type":"mousemove","time":2556,"x":81,"y":560},{"type":"mousemove","time":2767,"x":205,"y":553},{"type":"mousemove","time":3006,"x":359,"y":547},{"type":"mousemove","time":3223,"x":360,"y":548},{"type":"mousemove","time":3747,"x":360,"y":548},{"type":"mousemove","time":3947,"x":390,"y":547},{"type":"mousemove","time":4174,"x":440,"y":549},{"type":"mousemove","time":4380,"x":489,"y":546},{"type":"mousemove","time":4590,"x":495,"y":546},{"type":"mouseup","time":4870,"x":495,"y":546},{"time":4871,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":4880,"x":505,"y":543},{"type":"mousemove","time":5081,"x":529,"y":537},{"type":"mousemove","time":5291,"x":534,"y":550},{"type":"mousemove","time":5498,"x":535,"y":547},{"type":"mousemove","time":5704,"x":535,"y":546},{"type":"mousedown","time":5948,"x":535,"y":546},{"type":"mousemove","time":5956,"x":535,"y":546},{"type":"mousemove","time":6163,"x":426,"y":549},{"type":"mousemove","time":6375,"x":393,"y":551},{"type":"mousemove","time":6575,"x":370,"y":552},{"type":"mousemove","time":6782,"x":316,"y":556},{"type":"mousemove","time":6991,"x":303,"y":557},{"type":"mousemove","time":7080,"x":302,"y":557},{"type":"mousemove","time":7280,"x":201,"y":569},{"type":"mousemove","time":7488,"x":146,"y":577},{"type":"mousemove","time":7690,"x":145,"y":578},{"type":"mouseup","time":7787,"x":145,"y":578},{"time":7788,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":7897,"x":393,"y":488},{"type":"mousemove","time":8103,"x":631,"y":418},{"type":"mousemove","time":8310,"x":709,"y":395},{"type":"mousemove","time":8513,"x":735,"y":383},{"type":"mousedown","time":8662,"x":743,"y":379},{"type":"mousemove","time":8724,"x":743,"y":379},{"type":"mousemove","time":9813,"x":735,"y":350},{"type":"mousemove","time":10021,"x":735,"y":350}],"scrollY":3109.5,"scrollX":0,"timestamp":1748966404699},{"name":"Action 3","ops":[{"type":"mousemove","time":421,"x":745,"y":207},{"type":"mousemove","time":633,"x":501,"y":58},{"type":"mousemove","time":837,"x":386,"y":151},{"type":"mousemove","time":1037,"x":437,"y":56},{"type":"mousemove","time":1248,"x":461,"y":36},{"type":"mousemove","time":1453,"x":463,"y":23},{"type":"mousemove","time":1653,"x":465,"y":18},{"type":"mousemove","time":1866,"x":466,"y":18},{"type":"mousedown","time":2013,"x":466,"y":18},{"type":"mousemove","time":2021,"x":465,"y":18},{"type":"mousemove","time":2234,"x":425,"y":21},{"type":"mousemove","time":2444,"x":406,"y":24},{"type":"mousemove","time":2664,"x":381,"y":25},{"type":"mousemove","time":2879,"x":367,"y":25},{"type":"mousemove","time":3097,"x":375,"y":23},{"type":"mousemove","time":3314,"x":404,"y":18},{"type":"mousemove","time":3514,"x":428,"y":12},{"type":"mouseup","time":3620,"x":428,"y":12},{"time":3621,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":3629,"x":413,"y":15},{"type":"mousemove","time":3849,"x":329,"y":19},{"type":"mousedown","time":4271,"x":329,"y":19},{"type":"mousemove","time":4279,"x":329,"y":19},{"type":"mousemove","time":4491,"x":404,"y":11},{"type":"mousemove","time":4704,"x":424,"y":11},{"type":"mousemove","time":4909,"x":437,"y":10},{"type":"mousemove","time":5127,"x":415,"y":13},{"type":"mousemove","time":5327,"x":304,"y":22},{"type":"mousemove","time":5559,"x":275,"y":26},{"type":"mouseup","time":5711,"x":275,"y":26},{"time":5712,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":5771,"x":376,"y":82},{"type":"mousemove","time":5992,"x":478,"y":141},{"type":"mousemove","time":6196,"x":393,"y":19},{"type":"mousemove","time":6401,"x":378,"y":17},{"type":"mousemove","time":6618,"x":522,"y":151},{"type":"mousemove","time":6835,"x":581,"y":141},{"type":"mousemove","time":7048,"x":582,"y":152},{"type":"mousemove","time":7254,"x":553,"y":155},{"type":"mousemove","time":7458,"x":535,"y":156},{"type":"mousemove","time":7667,"x":493,"y":171},{"type":"mousemove","time":7875,"x":466,"y":105},{"type":"mousemove","time":8090,"x":377,"y":22},{"type":"mousemove","time":8298,"x":392,"y":13},{"type":"mousemove","time":8528,"x":411,"y":13},{"type":"mousemove","time":8747,"x":403,"y":14},{"type":"mousemove","time":8960,"x":390,"y":14}],"scrollY":1897,"scrollX":0,"timestamp":1748966428492},{"name":"Action 4","ops":[{"type":"mousemove","time":89,"x":728,"y":495},{"type":"mousemove","time":289,"x":689,"y":564},{"type":"mousemove","time":489,"x":682,"y":577},{"type":"mousemove","time":689,"x":686,"y":580},{"type":"mousemove","time":897,"x":687,"y":581},{"type":"mousedown","time":1169,"x":687,"y":581},{"type":"mousemove","time":1178,"x":687,"y":581},{"type":"mousemove","time":1381,"x":620,"y":539},{"type":"mousemove","time":1597,"x":581,"y":525},{"type":"mousemove","time":1722,"x":580,"y":525},{"type":"mousemove","time":1922,"x":397,"y":495},{"type":"mousemove","time":2123,"x":320,"y":494},{"type":"mousemove","time":2326,"x":293,"y":491},{"type":"mousemove","time":2543,"x":474,"y":506},{"type":"mousemove","time":2749,"x":615,"y":536},{"type":"mousemove","time":2956,"x":653,"y":543},{"type":"mouseup","time":3187,"x":653,"y":543},{"time":3188,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":3197,"x":616,"y":508},{"type":"mousemove","time":3400,"x":96,"y":202},{"type":"mousemove","time":3606,"x":119,"y":189},{"type":"mousemove","time":3812,"x":136,"y":189},{"type":"valuechange","selector":"#main_grid_layout>div.test-chart-block-left>div.test-inputs.test-buttons.test-inputs-style-compact>span.test-inputs-select>select.test-inputs-select-select","value":"1","time":4665,"target":"select"},{"time":4666,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":4706,"x":204,"y":248},{"type":"mousemove","time":4911,"x":651,"y":535},{"type":"mousemove","time":5117,"x":680,"y":556},{"type":"mousemove","time":5331,"x":680,"y":557},{"type":"mousemove","time":5540,"x":680,"y":557},{"type":"mousemove","time":5740,"x":648,"y":556},{"type":"mousedown","time":5987,"x":648,"y":556},{"type":"mousemove","time":5996,"x":648,"y":556},{"type":"mousemove","time":6200,"x":536,"y":542},{"type":"mousemove","time":6406,"x":478,"y":539},{"type":"mousemove","time":6608,"x":427,"y":537},{"type":"mousemove","time":6814,"x":415,"y":536},{"type":"mousemove","time":7022,"x":484,"y":545},{"type":"mousemove","time":7225,"x":627,"y":547},{"type":"mousemove","time":7429,"x":676,"y":557},{"type":"mousemove","time":7630,"x":677,"y":557},{"type":"mouseup","time":7754,"x":677,"y":557},{"time":7755,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":7763,"x":682,"y":547},{"type":"mousemove","time":7964,"x":749,"y":438}],"scrollY":3606,"scrollX":0,"timestamp":1749038158325}] \ No newline at end of file diff --git a/test/runTest/actions/matrix_application2.json b/test/runTest/actions/matrix_application2.json new file mode 100644 index 0000000000..bfe797f896 --- /dev/null +++ b/test/runTest/actions/matrix_application2.json @@ -0,0 +1 @@ +[{"name":"Action 1","ops":[{"type":"mousemove","time":604,"x":740,"y":10},{"type":"mousemove","time":804,"x":504,"y":229},{"type":"mousemove","time":1010,"x":450,"y":267},{"type":"mousemove","time":1538,"x":450,"y":267},{"type":"mousemove","time":1738,"x":341,"y":286},{"type":"mousemove","time":1943,"x":337,"y":286},{"type":"mousemove","time":2187,"x":335,"y":286},{"type":"mousemove","time":2388,"x":139,"y":134},{"type":"mousemove","time":2588,"x":94,"y":96},{"type":"mousemove","time":2794,"x":89,"y":85},{"type":"mousemove","time":3004,"x":322,"y":227},{"type":"mousemove","time":3204,"x":309,"y":226},{"type":"mousemove","time":3404,"x":315,"y":238},{"type":"mousedown","time":3620,"x":315,"y":238},{"type":"mousemove","time":3635,"x":315,"y":238},{"type":"mousemove","time":3836,"x":405,"y":336},{"type":"mousemove","time":4038,"x":403,"y":335},{"type":"mousemove","time":4244,"x":401,"y":334},{"type":"mousemove","time":4454,"x":362,"y":386},{"type":"mousemove","time":4655,"x":417,"y":454},{"type":"mousemove","time":4860,"x":418,"y":456},{"type":"mousemove","time":4888,"x":417,"y":457},{"type":"mousemove","time":5088,"x":449,"y":440},{"type":"mouseup","time":5278,"x":449,"y":440},{"time":5279,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":5292,"x":484,"y":428},{"type":"mousedown","time":5471,"x":624,"y":356},{"type":"mousemove","time":5502,"x":624,"y":356},{"type":"mouseup","time":5570,"x":624,"y":356},{"time":5571,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":5837,"x":624,"y":355},{"type":"mousemove","time":6038,"x":557,"y":224},{"type":"mousemove","time":6238,"x":593,"y":224},{"type":"mousemove","time":6444,"x":593,"y":224},{"type":"mousedown","time":6594,"x":593,"y":224},{"type":"mousemove","time":6606,"x":593,"y":227},{"type":"mousemove","time":6818,"x":595,"y":280},{"type":"mousemove","time":7022,"x":594,"y":338},{"type":"mousemove","time":7229,"x":594,"y":377},{"type":"mousemove","time":7421,"x":594,"y":378},{"type":"mousemove","time":7621,"x":594,"y":455},{"type":"mousemove","time":7828,"x":593,"y":465},{"type":"mouseup","time":8061,"x":593,"y":465},{"time":8062,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":8074,"x":597,"y":455},{"type":"mousemove","time":8278,"x":612,"y":132},{"type":"mousemove","time":8487,"x":598,"y":149},{"type":"mousedown","time":8604,"x":588,"y":172},{"type":"mousemove","time":8695,"x":588,"y":172},{"type":"mouseup","time":8704,"x":588,"y":172},{"time":8705,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":8954,"x":585,"y":172},{"type":"mousemove","time":9154,"x":533,"y":172},{"type":"mousedown","time":9204,"x":533,"y":172},{"type":"mouseup","time":9354,"x":533,"y":172},{"time":9355,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":9471,"x":532,"y":172},{"type":"mousemove","time":9671,"x":200,"y":368},{"type":"mousemove","time":9871,"x":188,"y":330},{"type":"mousemove","time":10071,"x":236,"y":286},{"type":"mousemove","time":10271,"x":242,"y":283},{"type":"mousedown","time":10404,"x":253,"y":281},{"type":"mousemove","time":10478,"x":253,"y":281},{"type":"mouseup","time":10628,"x":253,"y":281},{"time":10629,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":10871,"x":253,"y":283},{"type":"mousemove","time":11072,"x":223,"y":314},{"type":"mousemove","time":11279,"x":231,"y":317},{"type":"mousedown","time":11322,"x":232,"y":317},{"type":"mouseup","time":11454,"x":232,"y":317},{"time":11455,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":11495,"x":232,"y":317},{"type":"mousemove","time":11554,"x":230,"y":317},{"type":"mousemove","time":11754,"x":202,"y":317},{"type":"mousemove","time":11954,"x":209,"y":264},{"type":"mousemove","time":12163,"x":209,"y":255},{"type":"mousedown","time":12171,"x":209,"y":255},{"type":"mouseup","time":12270,"x":209,"y":255},{"time":12271,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":12384,"x":209,"y":255},{"type":"mousemove","time":12521,"x":207,"y":255},{"type":"mousemove","time":12729,"x":170,"y":271},{"type":"mousemove","time":12937,"x":169,"y":362},{"type":"mousemove","time":13138,"x":169,"y":361},{"type":"mousedown","time":13262,"x":175,"y":358},{"type":"mousemove","time":13344,"x":175,"y":358},{"type":"mouseup","time":13387,"x":175,"y":358},{"time":13388,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":13537,"x":174,"y":358},{"type":"mousemove","time":13738,"x":149,"y":370},{"type":"mousedown","time":13755,"x":149,"y":370},{"type":"mouseup","time":13937,"x":149,"y":370},{"time":13938,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":13965,"x":149,"y":370},{"type":"mousemove","time":14037,"x":150,"y":370},{"type":"mousemove","time":14238,"x":419,"y":334},{"type":"mousemove","time":14446,"x":441,"y":326},{"type":"mousedown","time":14629,"x":441,"y":326},{"type":"mousemove","time":14640,"x":442,"y":326},{"type":"mousemove","time":14849,"x":464,"y":419},{"type":"mousemove","time":15062,"x":464,"y":437},{"type":"mouseup","time":15212,"x":464,"y":437},{"time":15213,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":15224,"x":406,"y":371},{"type":"mousemove","time":15433,"x":255,"y":226},{"type":"mousemove","time":15638,"x":216,"y":173},{"type":"mousemove","time":15845,"x":215,"y":172},{"type":"mousedown","time":15972,"x":215,"y":172},{"type":"mousemove","time":15984,"x":215,"y":172},{"type":"mousemove","time":16185,"x":248,"y":206},{"type":"mousemove","time":16388,"x":276,"y":219},{"type":"mousemove","time":16588,"x":254,"y":213},{"type":"mousemove","time":16789,"x":215,"y":208},{"type":"mousemove","time":16995,"x":211,"y":206},{"type":"mouseup","time":17113,"x":211,"y":206},{"time":17114,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":17126,"x":252,"y":241},{"type":"mousemove","time":17330,"x":317,"y":252},{"type":"mousemove","time":17537,"x":387,"y":258},{"type":"mousemove","time":17746,"x":387,"y":274},{"type":"mousedown","time":17905,"x":387,"y":274},{"type":"mousemove","time":17917,"x":387,"y":274},{"type":"mousemove","time":18117,"x":261,"y":243},{"type":"mousemove","time":18322,"x":209,"y":185},{"type":"mousemove","time":18530,"x":206,"y":157},{"type":"mousemove","time":18738,"x":236,"y":236},{"type":"mousemove","time":18938,"x":445,"y":310},{"type":"mousemove","time":19139,"x":481,"y":333},{"type":"mousemove","time":19351,"x":517,"y":410},{"type":"mousemove","time":19562,"x":523,"y":418},{"type":"mousemove","time":19588,"x":525,"y":419},{"type":"mousemove","time":19789,"x":603,"y":508},{"type":"mousemove","time":19999,"x":579,"y":474},{"type":"mousemove","time":20213,"x":576,"y":464},{"type":"mouseup","time":20262,"x":576,"y":464},{"time":20263,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":20275,"x":589,"y":455},{"type":"mousedown","time":20430,"x":654,"y":394},{"type":"mousemove","time":20480,"x":654,"y":394},{"type":"mouseup","time":20504,"x":654,"y":394},{"time":20505,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":20689,"x":651,"y":394},{"type":"mousemove","time":20896,"x":102,"y":84},{"type":"mousemove","time":21106,"x":308,"y":143},{"type":"mousemove","time":21314,"x":358,"y":271},{"type":"mousemove","time":21529,"x":348,"y":266},{"type":"mousedown","time":21596,"x":348,"y":266},{"type":"mousemove","time":21607,"x":351,"y":268},{"type":"mousemove","time":21814,"x":459,"y":337},{"type":"mouseup","time":22130,"x":460,"y":337},{"time":22131,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":22143,"x":408,"y":282},{"type":"mousemove","time":22349,"x":97,"y":52},{"type":"mousemove","time":22554,"x":77,"y":73},{"type":"mousedown","time":22600,"x":77,"y":74},{"type":"mouseup","time":22704,"x":77,"y":74},{"time":22705,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":22761,"x":77,"y":74},{"type":"mousedown","time":23362,"x":77,"y":74},{"type":"mouseup","time":23487,"x":77,"y":74},{"time":23488,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":23656,"x":78,"y":74},{"type":"mousemove","time":23856,"x":158,"y":73},{"type":"mousedown","time":23996,"x":175,"y":75},{"type":"mousemove","time":24062,"x":175,"y":75},{"type":"mouseup","time":24120,"x":175,"y":75},{"time":24121,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":24271,"x":113,"y":80},{"type":"mousedown","time":24413,"x":97,"y":80},{"type":"mousemove","time":24479,"x":97,"y":80},{"type":"mouseup","time":24537,"x":97,"y":80},{"time":24538,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":24888,"x":99,"y":80},{"type":"mousemove","time":25088,"x":173,"y":76},{"type":"mousedown","time":25171,"x":180,"y":75},{"type":"mouseup","time":25254,"x":180,"y":75},{"time":25255,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":25298,"x":180,"y":75},{"type":"mousedown","time":25480,"x":96,"y":77},{"type":"mousemove","time":25513,"x":96,"y":77},{"type":"mouseup","time":25587,"x":96,"y":77},{"time":25588,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":25655,"x":96,"y":77},{"type":"mousemove","time":25870,"x":402,"y":163},{"type":"mousemove","time":26072,"x":433,"y":174},{"type":"mousemove","time":26280,"x":359,"y":324},{"type":"mousemove","time":26405,"x":369,"y":322},{"type":"mousemove","time":26613,"x":636,"y":318},{"type":"mousemove","time":26829,"x":603,"y":346},{"type":"mousedown","time":26971,"x":603,"y":346},{"type":"mousemove","time":26984,"x":603,"y":346},{"type":"mousemove","time":27184,"x":666,"y":390},{"type":"mousemove","time":27388,"x":669,"y":406},{"type":"mousemove","time":27595,"x":669,"y":406},{"type":"mouseup","time":27630,"x":669,"y":406},{"time":27631,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":27643,"x":641,"y":376},{"type":"mousemove","time":27845,"x":331,"y":139},{"type":"mousemove","time":28055,"x":315,"y":101},{"type":"mousemove","time":28264,"x":322,"y":75},{"type":"mousedown","time":28321,"x":323,"y":73},{"type":"mouseup","time":28437,"x":323,"y":73},{"time":28438,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":28481,"x":323,"y":73},{"type":"mousemove","time":28490,"x":323,"y":73},{"type":"mousemove","time":28698,"x":385,"y":216},{"type":"mousemove","time":28904,"x":391,"y":294},{"type":"mousemove","time":29105,"x":371,"y":301},{"type":"mousemove","time":29313,"x":385,"y":325},{"type":"mousemove","time":29521,"x":377,"y":321},{"type":"mousemove","time":29722,"x":376,"y":361},{"type":"mousemove","time":29931,"x":381,"y":347},{"type":"mousemove","time":30146,"x":379,"y":346},{"type":"mousedown","time":30338,"x":379,"y":346},{"type":"mousemove","time":30350,"x":379,"y":346},{"type":"mousemove","time":30551,"x":447,"y":417},{"type":"mousemove","time":30755,"x":477,"y":432},{"type":"mousemove","time":30956,"x":492,"y":442},{"type":"mouseup","time":31280,"x":492,"y":442},{"time":31281,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":31295,"x":469,"y":387},{"type":"mousemove","time":31497,"x":225,"y":144},{"type":"mousemove","time":31705,"x":233,"y":97},{"type":"mousemove","time":31906,"x":334,"y":77},{"type":"mousedown","time":32021,"x":335,"y":73},{"type":"mousemove","time":32113,"x":335,"y":73},{"type":"mouseup","time":32164,"x":335,"y":73},{"time":32165,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":32221,"x":335,"y":75},{"type":"mousemove","time":32430,"x":499,"y":335},{"type":"mousemove","time":32638,"x":542,"y":364},{"type":"mousemove","time":32839,"x":603,"y":461},{"type":"mousemove","time":33046,"x":610,"y":462},{"type":"mousedown","time":33055,"x":610,"y":462},{"type":"mouseup","time":33154,"x":610,"y":462},{"time":33155,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":33271,"x":610,"y":460},{"type":"mousedown","time":33471,"x":610,"y":392},{"type":"mousemove","time":33483,"x":610,"y":392},{"type":"mouseup","time":33571,"x":610,"y":392},{"time":33572,"delay":400,"type":"screenshot-auto"},{"type":"mousedown","time":33638,"x":610,"y":392},{"type":"mouseup","time":33737,"x":610,"y":392},{"time":33738,"delay":400,"type":"screenshot-auto"},{"type":"mousedown","time":34730,"x":610,"y":392},{"type":"mouseup","time":34830,"x":610,"y":392},{"time":34831,"delay":400,"type":"screenshot-auto"},{"type":"mousedown","time":34904,"x":610,"y":392},{"type":"mouseup","time":35004,"x":610,"y":392},{"time":35005,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":35897,"x":607,"y":409},{"type":"mousedown","time":36054,"x":604,"y":501},{"type":"mousemove","time":36100,"x":604,"y":501},{"type":"mouseup","time":36163,"x":604,"y":501},{"time":36164,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":36356,"x":604,"y":501},{"type":"mousedown","time":36555,"x":598,"y":467},{"type":"mousemove","time":36568,"x":598,"y":467},{"type":"mouseup","time":36654,"x":598,"y":467},{"time":36655,"delay":400,"type":"screenshot-auto"},{"type":"mousedown","time":36721,"x":598,"y":467},{"type":"mouseup","time":36821,"x":598,"y":467},{"time":36822,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":36955,"x":598,"y":467},{"type":"mousemove","time":37157,"x":614,"y":465},{"type":"mousemove","time":37364,"x":612,"y":461},{"type":"mousemove","time":37571,"x":633,"y":453},{"type":"mousemove","time":37772,"x":653,"y":454},{"type":"mousedown","time":37790,"x":653,"y":454},{"type":"mouseup","time":37921,"x":653,"y":454},{"time":37922,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":38257,"x":653,"y":454},{"type":"mousemove","time":38464,"x":635,"y":518},{"type":"mousemove","time":38672,"x":654,"y":472},{"type":"mousemove","time":38872,"x":648,"y":477},{"type":"mousedown","time":38955,"x":654,"y":482},{"type":"mouseup","time":39037,"x":654,"y":482},{"time":39038,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":39080,"x":654,"y":482},{"type":"mousemove","time":39289,"x":387,"y":520},{"type":"mousedown","time":39405,"x":361,"y":520},{"type":"mousemove","time":39497,"x":361,"y":520},{"type":"mouseup","time":39522,"x":361,"y":520},{"time":39523,"delay":400,"type":"screenshot-auto"},{"type":"mousedown","time":39588,"x":361,"y":520},{"type":"mouseup","time":39704,"x":361,"y":520},{"time":39705,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":39855,"x":362,"y":520},{"type":"mousemove","time":40055,"x":367,"y":518},{"type":"mousemove","time":40261,"x":403,"y":449},{"type":"mousemove","time":40472,"x":425,"y":458},{"type":"mousedown","time":40654,"x":450,"y":483},{"type":"mousemove","time":40717,"x":450,"y":483},{"type":"mouseup","time":40780,"x":450,"y":483},{"time":40781,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":40888,"x":450,"y":483},{"type":"mousemove","time":41098,"x":510,"y":269},{"type":"mousemove","time":41272,"x":510,"y":265},{"type":"mousemove","time":41473,"x":452,"y":110},{"type":"mousemove","time":41673,"x":453,"y":95},{"type":"mousemove","time":41873,"x":628,"y":222},{"type":"mousemove","time":42073,"x":449,"y":298},{"type":"mousemove","time":42273,"x":482,"y":393},{"type":"mousedown","time":42332,"x":482,"y":396},{"type":"mouseup","time":42438,"x":482,"y":397},{"time":42439,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":42483,"x":482,"y":378},{"type":"mousemove","time":42688,"x":475,"y":141},{"type":"mousemove","time":42890,"x":456,"y":101},{"type":"mousemove","time":43101,"x":446,"y":77},{"type":"mousedown","time":43181,"x":444,"y":72},{"type":"mouseup","time":43254,"x":444,"y":72},{"time":43255,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":43316,"x":444,"y":72},{"type":"mousemove","time":43522,"x":424,"y":201},{"type":"mousemove","time":43722,"x":426,"y":410},{"type":"mousemove","time":43923,"x":586,"y":392},{"type":"mousemove","time":44136,"x":633,"y":346},{"type":"mousemove","time":44339,"x":664,"y":295},{"type":"mousedown","time":44362,"x":664,"y":295},{"type":"mouseup","time":44487,"x":664,"y":295},{"time":44488,"delay":400,"type":"screenshot-auto"}],"scrollY":111,"scrollX":0,"timestamp":1748966463125},{"name":"Action 2","ops":[{"type":"mousemove","time":425,"x":540,"y":112},{"type":"mousemove","time":633,"x":478,"y":146},{"type":"mousemove","time":841,"x":374,"y":201},{"type":"mousemove","time":1041,"x":372,"y":197},{"type":"mousedown","time":1307,"x":372,"y":197},{"type":"mousemove","time":1323,"x":373,"y":199},{"type":"mousemove","time":1524,"x":383,"y":224},{"type":"mousemove","time":1724,"x":390,"y":228},{"type":"mousemove","time":1924,"x":468,"y":229},{"type":"mousemove","time":2124,"x":498,"y":227},{"type":"mousemove","time":2324,"x":566,"y":230},{"type":"mousemove","time":2531,"x":561,"y":231},{"type":"mousemove","time":2557,"x":557,"y":232},{"type":"mousemove","time":2758,"x":468,"y":333},{"type":"mousemove","time":2970,"x":450,"y":344},{"type":"mousemove","time":3174,"x":438,"y":295},{"type":"mousemove","time":3374,"x":380,"y":197},{"type":"mousemove","time":3582,"x":380,"y":197},{"type":"mouseup","time":3799,"x":380,"y":197},{"time":3800,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":3812,"x":392,"y":197},{"type":"mousemove","time":4018,"x":446,"y":188},{"type":"mousedown","time":4332,"x":446,"y":188},{"type":"mousemove","time":4344,"x":446,"y":189},{"type":"mousemove","time":4555,"x":450,"y":206},{"type":"mousemove","time":4757,"x":459,"y":221},{"type":"mousemove","time":4958,"x":322,"y":372},{"type":"mousemove","time":5158,"x":358,"y":299},{"type":"mousemove","time":5366,"x":167,"y":373},{"type":"mousemove","time":5574,"x":396,"y":295},{"type":"mousemove","time":5774,"x":718,"y":250},{"type":"mousemove","time":5975,"x":799,"y":229},{"type":"mousemove","time":6225,"x":790,"y":226},{"type":"mousemove","time":6433,"x":748,"y":235},{"type":"mousemove","time":6649,"x":751,"y":235},{"type":"mouseup","time":6766,"x":751,"y":235},{"time":6767,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":6780,"x":598,"y":236},{"type":"mousemove","time":6984,"x":160,"y":324},{"type":"mousedown","time":7117,"x":139,"y":329},{"type":"mousemove","time":7200,"x":139,"y":329},{"type":"mouseup","time":7384,"x":115,"y":330},{"time":7385,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":7516,"x":112,"y":330},{"type":"mousedown","time":7667,"x":112,"y":330},{"type":"mousemove","time":7774,"x":112,"y":330},{"type":"mouseup","time":7850,"x":100,"y":330},{"time":7851,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":7983,"x":100,"y":329},{"type":"mousedown","time":8067,"x":100,"y":329},{"type":"mousemove","time":8076,"x":102,"y":329},{"type":"mousemove","time":8284,"x":177,"y":374},{"type":"mouseup","time":8417,"x":177,"y":374},{"time":8418,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":8427,"x":191,"y":374},{"type":"mousemove","time":8635,"x":347,"y":403},{"type":"mousemove","time":8843,"x":329,"y":271},{"type":"mousemove","time":9050,"x":339,"y":313},{"type":"mousedown","time":9267,"x":339,"y":313},{"type":"mousemove","time":9281,"x":341,"y":314},{"type":"mousemove","time":9489,"x":587,"y":500},{"type":"mousemove","time":9691,"x":639,"y":524},{"type":"mousemove","time":9905,"x":732,"y":584},{"type":"mousemove","time":10025,"x":672,"y":581},{"type":"mousemove","time":10226,"x":240,"y":496},{"type":"mousemove","time":10434,"x":260,"y":481},{"type":"mousemove","time":10492,"x":263,"y":477},{"type":"mousemove","time":10695,"x":181,"y":72},{"type":"mousemove","time":10923,"x":348,"y":-1},{"type":"mousemove","time":11018,"x":477,"y":6},{"type":"mousemove","time":11229,"x":610,"y":107},{"type":"mousemove","time":11439,"x":1,"y":347},{"type":"mousemove","time":11741,"x":20,"y":453},{"type":"mousemove","time":11942,"x":89,"y":416},{"type":"mousemove","time":12142,"x":106,"y":399},{"type":"mousemove","time":12349,"x":108,"y":397},{"type":"mouseup","time":12450,"x":108,"y":397},{"time":12451,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":12464,"x":129,"y":392},{"type":"mousedown","time":12602,"x":161,"y":389},{"type":"mousemove","time":12667,"x":161,"y":389},{"type":"mousemove","time":12875,"x":274,"y":389},{"type":"mousemove","time":13076,"x":346,"y":408},{"type":"mouseup","time":13194,"x":350,"y":408},{"time":13195,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":13284,"x":350,"y":408},{"type":"mousedown","time":13467,"x":367,"y":410},{"type":"mousemove","time":13582,"x":368,"y":410},{"type":"mouseup","time":13641,"x":368,"y":410},{"time":13642,"delay":400,"type":"screenshot-auto"},{"type":"mousedown","time":13950,"x":368,"y":410},{"type":"mousemove","time":13963,"x":374,"y":410},{"type":"mousemove","time":14171,"x":605,"y":438},{"type":"mousemove","time":14384,"x":604,"y":439},{"type":"mouseup","time":14591,"x":604,"y":439},{"time":14592,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":14606,"x":617,"y":419},{"type":"mousemove","time":14807,"x":516,"y":328},{"type":"mousemove","time":15009,"x":422,"y":235},{"type":"mousemove","time":15216,"x":417,"y":227},{"type":"mousedown","time":15301,"x":417,"y":227},{"type":"mousemove","time":15314,"x":418,"y":228},{"type":"mousemove","time":15522,"x":490,"y":310},{"type":"mousemove","time":15724,"x":506,"y":413},{"type":"mousemove","time":15924,"x":505,"y":421},{"type":"mousemove","time":16125,"x":503,"y":418},{"type":"mouseup","time":16201,"x":503,"y":418},{"time":16202,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":16213,"x":549,"y":386},{"type":"mousedown","time":16418,"x":666,"y":308},{"type":"mousemove","time":16431,"x":666,"y":308},{"type":"mouseup","time":16524,"x":666,"y":308},{"time":16525,"delay":400,"type":"screenshot-auto"}],"scrollY":111,"scrollX":0,"timestamp":1748966552406}] \ No newline at end of file diff --git a/test/sankey-roam.html b/test/sankey-roam.html index 259d7b04d0..11f7a71921 100644 --- a/test/sankey-roam.html +++ b/test/sankey-roam.html @@ -33,6 +33,11 @@ @@ -42,18 +47,20 @@ - - diff --git a/test/tmp-base.html b/test/tmp-base.html index d8883e3735..7c03de1db4 100644 --- a/test/tmp-base.html +++ b/test/tmp-base.html @@ -28,7 +28,8 @@ - + + @@ -41,26 +42,109 @@ -
+
+ + + \ No newline at end of file diff --git a/test/tree-roam.html b/test/tree-roam.html index d207a8e98a..831c742ccc 100644 --- a/test/tree-roam.html +++ b/test/tree-roam.html @@ -25,29 +25,37 @@ + + + + + -
+ + +
+ + + \ No newline at end of file diff --git a/test/treemap-simple2.html b/test/treemap-simple2.html index 305bffd226..c78b71b7cc 100644 --- a/test/treemap-simple2.html +++ b/test/treemap-simple2.html @@ -39,6 +39,8 @@
+
+ @@ -132,6 +134,107 @@ + + + + + + +