You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@echarts.apache.org by su...@apache.org on 2025/05/30 18:12:54 UTC
(echarts) branch feat-matrix-enhance created (now 6beeef2f0)
This is an automated email from the ASF dual-hosted git repository.
sushuang pushed a change to branch feat-matrix-enhance
in repository https://gitbox.apache.org/repos/asf/echarts.git
at 6beeef2f0 feat(matrix & calendar): (1). Support matrix col/row size option. (2). Support corner content options in matrix. (3) Support body content options. (4). Support matrix `mergeCells`. (5). Fix and refactor the matrix style options, conserning their inheritance and z-order and flexibility. (6). Support declaratively layout other components (including grid/polar/geo/graph coord sys, and dataZoom/visualMap/legend/... components) based on `matrix`/`calendar`. And and enhance t [...]
This branch includes the following new commits:
new 6bcfe1064 test: sync test helper.
new 139206be7 test: manually sync visual test updates.
new 8ddd15d32 feat(visualMap): support specify `seriesId` on visualMap option; previously only seriesIndex can be specified, not convenient and not consistent with other components.
new 6beeef2f0 feat(matrix & calendar): (1). Support matrix col/row size option. (2). Support corner content options in matrix. (3) Support body content options. (4). Support matrix `mergeCells`. (5). Fix and refactor the matrix style options, conserning their inheritance and z-order and flexibility. (6). Support declaratively layout other components (including grid/polar/geo/graph coord sys, and dataZoom/visualMap/legend/... components) based on `matrix`/`calendar`. And and enhance t [...]
The 4 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@echarts.apache.org
For additional commands, e-mail: commits-help@echarts.apache.org
(echarts) 04/04: feat(matrix & calendar): (1). Support matrix col/row size option. (2). Support corner content options in matrix. (3) Support body content options. (4). Support matrix `mergeCells`. (5). Fix and refactor the matrix style options, conserning their inheritance and z-order and flexibility. (6). Support declaratively layout other components (including grid/polar/geo/graph coord sys, and dataZoom/visualMap/legend/... components) based on `matrix`/`calendar`. And and enhance the echarts API `conv [...]
Posted by su...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
sushuang pushed a commit to branch feat-matrix-enhance
in repository https://gitbox.apache.org/repos/asf/echarts.git
commit 6beeef2f0ef91d6e51e0d8ab3248bd3f4b35f74a
Author: 100pah <su...@gmail.com>
AuthorDate: Sat May 31 02:12:15 2025 +0800
feat(matrix & calendar): (1). Support matrix col/row size option. (2). Support corner content options in matrix. (3) Support body content options. (4). Support matrix `mergeCells`. (5). Fix and refactor the matrix style options, conserning their inheritance and z-order and flexibility. (6). Support declaratively layout other components (including grid/polar/geo/graph coord sys, and dataZoom/visualMap/legend/... components) based on `matrix`/`calendar`. And and enhance the echarts API [...]
---
src/chart/bar/BarSeries.ts | 10 +-
src/chart/bar/PictorialBarSeries.ts | 5 +-
src/chart/custom/CustomSeries.ts | 25 +-
src/chart/custom/CustomView.ts | 2 +
src/chart/funnel/FunnelSeries.ts | 1 +
src/chart/funnel/funnelLayout.ts | 13 +-
src/chart/graph/createView.ts | 23 +-
src/chart/heatmap/HeatmapSeries.ts | 8 +-
src/chart/heatmap/HeatmapView.ts | 28 +-
src/chart/helper/createGraphFromNodeEdge.ts | 2 +-
src/chart/helper/createSeriesData.ts | 10 +
src/chart/map/MapSeries.ts | 14 +-
src/chart/pie/PieSeries.ts | 17 +-
src/chart/pie/PieView.ts | 6 +-
src/chart/pie/labelLayout.ts | 6 +-
src/chart/pie/pieLayout.ts | 84 +-
src/chart/scatter/ScatterSeries.ts | 2 +-
src/component/axisPointer/modelHelper.ts | 1 +
src/component/calendar/CalendarView.ts | 8 +-
src/component/dataZoom/SliderZoomView.ts | 9 +-
src/component/helper/listComponent.ts | 54 +-
src/component/legend/LegendView.ts | 15 +-
src/component/matrix/MatrixView.ts | 494 ++++--
src/component/timeline/SliderTimelineView.ts | 5 +-
src/component/title/install.ts | 10 +-
src/component/toolbox/ToolboxView.ts | 24 +-
src/component/visualMap/VisualMapView.ts | 3 +-
src/coord/CoordinateSystem.ts | 70 +-
src/coord/View.ts | 15 +-
src/coord/calendar/Calendar.ts | 107 +-
src/coord/calendar/CalendarModel.ts | 9 +-
src/coord/calendar/prepareCustom.ts | 13 +-
src/coord/cartesian/Cartesian2D.ts | 4 +-
src/coord/cartesian/Grid.ts | 76 +-
src/coord/cartesian/GridModel.ts | 8 +-
src/coord/cartesian/cartesianAxisHelper.ts | 13 +-
src/coord/cartesian/defaultAxisExtentFromData.ts | 13 +-
src/coord/geo/Geo.ts | 18 +-
src/coord/geo/geoCreator.ts | 40 +-
src/coord/matrix/Matrix.ts | 626 ++++++--
src/coord/matrix/MatrixBodyCorner.ts | 291 ++++
src/coord/matrix/MatrixDim.ts | 578 +++++--
src/coord/matrix/MatrixModel.ts | 320 +++-
src/coord/matrix/matrixCoordHelper.ts | 363 +++++
src/coord/matrix/prepareCustom.ts | 17 +-
src/coord/parallel/Parallel.ts | 9 +-
src/coord/polar/Polar.ts | 31 +-
src/coord/polar/polarCreator.ts | 11 +-
src/coord/radar/Radar.ts | 11 +-
src/coord/single/Single.ts | 44 +-
src/core/CoordinateSystem.ts | 313 +++-
src/core/echarts.ts | 115 +-
src/data/OrdinalMeta.ts | 35 +-
src/label/labelStyle.ts | 5 +-
src/model/Component.ts | 10 +-
src/model/Series.ts | 3 +-
src/model/referHelper.ts | 36 +-
src/util/graphic.ts | 102 +-
src/util/layout.ts | 115 +-
src/util/model.ts | 86 +-
src/util/number.ts | 46 +-
src/util/types.ts | 93 +-
test/calendar-other-coord-sys.html | 748 +++++++++
test/matrix.html | 240 ++-
test/matrix2.html | 1802 ++++++++++++++++++++++
test/matrix3.html | 858 ++++++++++
test/matrix_application.html | 384 ++++-
test/matrix_application2.html | 801 ++++++++++
test/pie-coordinate-system.html | 16 +-
69 files changed, 8405 insertions(+), 969 deletions(-)
diff --git a/src/chart/bar/BarSeries.ts b/src/chart/bar/BarSeries.ts
index de03a19ea..395c2f14a 100644
--- a/src/chart/bar/BarSeries.ts
+++ b/src/chart/bar/BarSeries.ts
@@ -37,11 +37,13 @@ import { inheritDefaultOption } from '../../util/component';
import SeriesData from '../../data/SeriesData';
import { BrushCommonSelectorsForSeries } from '../../component/brush/selector';
-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<SeriesLabelOption, 'position'>
- & {position?: PolarBarLabelPosition | 'outside'};
+export type BarSeriesLabelOption = SeriesLabelOption<
+ CallbackDataParams,
+ {positionExtra: PolarBarLabelPositionExtra | 'outside'}
+>;
export interface BarStateOption<TCbParams = never> {
itemStyle?: BarItemStyleOption<TCbParams>
diff --git a/src/chart/bar/PictorialBarSeries.ts b/src/chart/bar/PictorialBarSeries.ts
index 95fbdd230..5292a1a62 100644
--- a/src/chart/bar/PictorialBarSeries.ts
+++ b/src/chart/bar/PictorialBarSeries.ts
@@ -27,14 +27,15 @@ import {
StatesOptionMixin,
OptionDataItemObject,
DefaultEmphasisFocus,
- SeriesEncodeOptionMixin
+ SeriesEncodeOptionMixin,
+ CallbackDataParams
} from '../../util/types';
import type Cartesian2D from '../../coord/cartesian/Cartesian2D';
import { inheritDefaultOption } from '../../util/component';
export interface PictorialBarStateOption {
itemStyle?: ItemStyleOption
- label?: SeriesLabelOption
+ label?: SeriesLabelOption<CallbackDataParams, {positionExtra: 'outside'}>
}
interface PictorialBarSeriesSymbolOption {
diff --git a/src/chart/custom/CustomSeries.ts b/src/chart/custom/CustomSeries.ts
index 29014e1a6..606ccb3f1 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,
@@ -286,13 +288,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<number[]>;
@@ -377,7 +396,7 @@ export default class CustomSeriesModel extends SeriesModel<CustomSeriesOption> {
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 603b5ebb9..5ce6cceac 100644
--- a/src/chart/custom/CustomView.ts
+++ b/src/chart/custom/CustomView.ts
@@ -152,6 +152,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`.
diff --git a/src/chart/funnel/FunnelSeries.ts b/src/chart/funnel/FunnelSeries.ts
index 152cdb37d..307b15a2e 100644
--- a/src/chart/funnel/FunnelSeries.ts
+++ b/src/chart/funnel/FunnelSeries.ts
@@ -147,6 +147,7 @@ class FunnelSeriesModel extends SeriesModel<FunnelSeriesOption> {
}
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 4d433c1b1..952904117 100644
--- a/src/chart/funnel/funnelLayout.ts
+++ b/src/chart/funnel/funnelLayout.ts
@@ -25,14 +25,6 @@ import SeriesData from '../../data/SeriesData';
import GlobalModel from '../../model/Global';
import { isFunction } from 'zrender/src/core/util';
-function getViewRect(seriesModel: FunnelSeriesModel, api: ExtensionAPI) {
- return layout.getLayoutRect(
- seriesModel.getBoxLayoutParams(), {
- width: api.getWidth(),
- height: api.getHeight()
- }
- );
-}
function getSortedIndices(data: SeriesData, sort: FunnelSeriesOption['sort']) {
const valueDim = data.mapDimension('value');
@@ -251,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 = 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/createView.ts b/src/chart/graph/createView.ts
index 238c4b618..90b7b2b53 100644
--- a/src/chart/graph/createView.ts
+++ b/src/chart/graph/createView.ts
@@ -19,29 +19,34 @@
// FIXME Where to create the simple view coordinate system
import View from '../../coord/View';
-import {getLayoutRect} from '../../util/layout';
+import {createBoxLayoutReference, getLayoutRect} 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()
- });
+ return getLayoutRect(option, layoutRef.refContainer);
}
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<GraphNodeItemOption>(idx);
@@ -77,7 +82,7 @@ export default function createViewCoordSys(ecModel: GlobalModel, api: ExtensionA
const viewWidth = viewRect.width;
const viewHeight = viewRect.height;
- const viewCoordSys = seriesModel.coordinateSystem = new View();
+ const viewCoordSys = new View();
viewCoordSys.zoomLimit = seriesModel.get('scaleLimit');
viewCoordSys.setBoundingRect(
@@ -92,6 +97,8 @@ export default function createViewCoordSys(ecModel: GlobalModel, api: ExtensionA
viewCoordSys.setZoom(seriesModel.get('zoom'));
viewList.push(viewCoordSys);
+
+ return viewCoordSys;
}
});
diff --git a/src/chart/heatmap/HeatmapSeries.ts b/src/chart/heatmap/HeatmapSeries.ts
index df68efdeb..7647f1837 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';
type HeatmapDataValue = OptionDataValue[];
@@ -67,7 +68,7 @@ export interface HeatmapSeriesOption
type?: 'heatmap'
- coordinateSystem?: 'cartesian2d' | 'geo' | 'calendar'
+ coordinateSystem?: 'cartesian2d' | 'geo' | 'calendar' | 'matrix'
// Available on geo coordinate system
blurSize?: number
@@ -84,9 +85,8 @@ class HeatmapSeriesModel extends SeriesModel<HeatmapSeriesOption> {
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 a181685fa..cdab43f2e 100644
--- a/src/chart/heatmap/HeatmapView.ts
+++ b/src/chart/heatmap/HeatmapView.ts
@@ -265,27 +265,33 @@ class HeatmapView extends ChartView {
});
}
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: coordSys.dataToRect(
- [
- data.get(dataDims[0], idx) as string,
- data.get(dataDims[1], idx) as string
- ]
- ),
- style
+ shape,
+ style,
});
}
- else {
+ else { // Calendar
// Ignore empty data
if (isNaN(data.get(dataDims[1], idx) as number)) {
continue;
}
-
+ const layout = coordSys.dataToLayout([data.get(dataDims[0], idx)]);
+ const shape = layout.contentRect || layout.rect;
+ if (zrUtil.eqNaN(shape.x)) {
+ continue;
+ }
rect = new graphic.Rect({
z2: 1,
- shape: coordSys.dataToRect([data.get(dataDims[0], idx)]).contentShape,
- style
+ shape,
+ style,
});
}
diff --git a/src/chart/helper/createGraphFromNodeEdge.ts b/src/chart/helper/createGraphFromNodeEdge.ts
index 14eea23fd..a6fcaaed9 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 4e1bfd714..a15490643 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 157678f41..7e177e886 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';
export interface MapStateOption<TCbParams = never> {
itemStyle?: GeoItemStyleOption<TCbParams>
@@ -146,10 +147,15 @@ class MapSeries extends SeriesModel<MapSeriesOption> {
* 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 c50da0405..a57b3f4e1 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<TCbParams = never> extends ItemStyleOption<TCbParams> {
// can be 10
@@ -168,9 +169,8 @@ class PieSeriesModel extends SeriesModel<PieSeriesOption> {
* @overwrite
*/
getInitialData(this: PieSeriesModel): SeriesData {
- const isMatrix = this.option.coordinateSystem === 'matrix';
return createSeriesDataSimply(this, {
- coordDimensions: isMatrix ? ['x', 'y', 'value'] : ['value'],
+ coordDimensions: ['value'],
encodeDefaulter: zrUtil.curry(makeSeriesEncodeForNameBased, this)
});
}
@@ -244,6 +244,7 @@ class PieSeriesModel extends SeriesModel<PieSeriesOption> {
stillShowZeroSum: true,
// cursor: null,
+ coordinateSystemUsage: 'box',
left: 0,
top: 0,
@@ -266,7 +267,8 @@ class PieSeriesModel extends SeriesModel<PieSeriesOption> {
// 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,不支持异步回调
@@ -328,4 +330,13 @@ class PieSeriesModel extends SeriesModel<PieSeriesOption> {
}
+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 04a81f1e6..cd3621102 100644
--- a/src/chart/pie/PieView.ts
+++ b/src/chart/pie/PieView.ts
@@ -19,7 +19,7 @@
*/
-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';
@@ -33,7 +33,7 @@ import { setLabelLineStyle, getLabelLineStatesModels } from '../../label/labelGu
import { setLabelStyle, getLabelStatesModels } from '../../label/labelStyle';
import { getSectorCornerRadius } from '../helper/sectorHelper';
import { saveOldStyle } from '../../animation/basicTransition';
-import { getBasicPieLayout, getSeriesLayoutData } from './pieLayout';
+import { getSeriesLayoutData } from './pieLayout';
/**
* Piece of pie including Sector, Label, LabelLine
@@ -262,7 +262,7 @@ class PieView extends ChartView {
if (data.count() === 0 && seriesModel.get('showEmptyCircle')) {
const layoutData = getSeriesLayoutData(seriesModel);
const sector = new graphic.Sector({
- shape: extend(getBasicPieLayout(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 934b49cad..419079478 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 4ec992ec4..05915b96b 100644
--- a/src/chart/pie/pieLayout.ts
+++ b/src/chart/pie/pieLayout.ts
@@ -26,25 +26,51 @@ import PieSeriesModel from './PieSeries';
import { SectorShape } from 'zrender/src/graphic/shape/Sector';
import { normalizeArcAngles } from 'zrender/src/core/PathProxy';
import { makeInner } from '../../util/model';
+import { BoxCoordinateSystemCoordFrom } from '../../core/CoordinateSystem';
const PI2 = Math.PI * 2;
const RADIAN = Math.PI / 180;
-function getViewRect(seriesModel: PieSeriesModel, api: ExtensionAPI) {
- return layout.getLayoutRect(
- seriesModel.getBoxLayoutParams(), {
- width: api.getWidth(),
- height: api.getHeight()
- }
- );
+function getViewRectAndCenter(seriesModel: PieSeriesModel, api: ExtensionAPI) {
+
+ const layoutRef = layout.createBoxLayoutReference(seriesModel, api, {
+ enableLayoutOnlyByCenter: true,
+ });
+ const boxLayoutParams = seriesModel.getBoxLayoutParams();
+
+ let viewRect: layout.LayoutRect;
+ let center: number[];
+ if (layoutRef.type === layout.BoxLayoutReferenceType.point) {
+ center = layoutRef.refPoint;
+ // `viewRect` is required in `pie/labelLayout.ts`.
+ viewRect = layout.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 = layout.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 getBasicPieLayout(seriesModel: PieSeriesModel, api: ExtensionAPI):
- Pick<SectorShape, 'cx' | 'cy' | 'r' | 'r0'> {
- const viewRect = getViewRect(seriesModel, api);
+function getBasicPieLayout(seriesModel: PieSeriesModel, api: ExtensionAPI):
+ Pick<SectorShape, 'cx' | 'cy' | 'r' | 'r0'> & {viewRect: layout.LayoutRect} {
// center can be string or number when coordinateSystem is specified
- let center = seriesModel.get('center');
+ const {viewRect, center} = getViewRectAndCenter(seriesModel, api);
+
let radius = seriesModel.get('radius');
if (!zrUtil.isArray(radius)) {
@@ -56,28 +82,12 @@ export function getBasicPieLayout(seriesModel: PieSeriesModel, api: ExtensionAPI
const r0 = parsePercent(radius[0], size / 2);
const r = parsePercent(radius[1], size / 2);
- let cx: number;
- let cy: number;
- const coordSys = seriesModel.coordinateSystem;
- if (coordSys) {
- // percentage is not allowed when coordinate system is specified
- const point = coordSys.dataToPoint(center);
- cx = point[0] || 0;
- cy = point[1] || 0;
- }
- else {
- if (!zrUtil.isArray(center)) {
- center = [center, center];
- }
- cx = parsePercent(center[0], width) + viewRect.x;
- cy = parsePercent(center[1], height) + viewRect.y;
- }
-
return {
- cx,
- cy,
+ cx: center[0],
+ cy: center[1],
r0,
- r
+ r,
+ viewRect,
};
}
@@ -89,9 +99,8 @@ export default function pieLayout(
ecModel.eachSeriesByType(seriesType, function (seriesModel: PieSeriesModel) {
const data = seriesModel.getData();
const valueDim = data.mapDimension('value');
- const viewRect = getViewRect(seriesModel, api);
- const { cx, cy, r, r0 } = getBasicPieLayout(seriesModel, api);
+ const { cx, cy, r, r0, viewRect } = getBasicPieLayout(seriesModel, api);
let startAngle = -seriesModel.get('startAngle') * RADIAN;
let endAngle = seriesModel.get('endAngle');
@@ -132,6 +141,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);
@@ -141,6 +154,7 @@ export default function pieLayout(
let currentAngle = startAngle;
+ // Requird by `pieLabelLayout`.
data.setLayout({ viewRect, r });
data.each(valueDim, function (value: number, idx: number) {
@@ -274,4 +288,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/scatter/ScatterSeries.ts b/src/chart/scatter/ScatterSeries.ts
index 5e2995eca..505937b03 100644
--- a/src/chart/scatter/ScatterSeries.ts
+++ b/src/chart/scatter/ScatterSeries.ts
@@ -82,7 +82,7 @@ class ScatterSeriesModel extends SeriesModel<ScatterSeriesOption> {
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/component/axisPointer/modelHelper.ts b/src/component/axisPointer/modelHelper.ts
index 2b03d2010..a0635b9c6 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 6cc727475..50c1d556d 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'];
@@ -536,7 +536,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 a6545fdb3..bf4573a15 100644
--- a/src/component/dataZoom/SliderZoomView.ts
+++ b/src/component/dataZoom/SliderZoomView.ts
@@ -224,17 +224,18 @@ 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()};
// 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 - DEFAULT_LOCATION_EDGE_GAP - moveHandleSize),
+ right: refContainer.width - coordRect.x - coordRect.width,
+ top: (refContainer.height - DEFAULT_FILLER_SIZE - DEFAULT_LOCATION_EDGE_GAP - moveHandleSize),
width: coordRect.width,
height: DEFAULT_FILLER_SIZE
}
@@ -258,7 +259,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/helper/listComponent.ts b/src/component/helper/listComponent.ts
index 39d063bc4..9faabbb61 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<ItemStyleOption, 'color' | 'opacity'>
+};
-export function makeBackground(rect, componentModel) {
+export function makeBackground(rect: RectLike, componentModel: Model<BackgroundRelatedOption>): 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/legend/LegendView.ts b/src/component/legend/LegendView.ts
index 35b0779f2..fb34487c7 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';
@@ -132,11 +133,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);
@@ -146,7 +147,7 @@ class LegendView extends ComponentView {
width: mainRect.width,
height: mainRect.height
}, positionInfo),
- viewportSize,
+ refContainer,
padding
);
this.group.x = layoutRect.x - mainRect.x;
@@ -155,7 +156,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<Omit<LegendOption, 'itemStyle'> & {itemStyle: ItemStyleOption}>
+ )
);
}
diff --git a/src/component/matrix/MatrixView.ts b/src/component/matrix/MatrixView.ts
index fbcbafba5..6aa65dc90 100644
--- a/src/component/matrix/MatrixView.ts
+++ b/src/component/matrix/MatrixView.ts
@@ -17,10 +17,35 @@
* under the License.
*/
-import MatrixModel from '../../coord/matrix/MatrixModel';
+import MatrixModel, { MatrixBaseCellOption, MatrixCellStyleOption, MatrixOption } from '../../coord/matrix/MatrixModel';
import ComponentView from '../../view/Component';
import { createTextStyle } from '../../label/labelStyle';
-import * as graphic from '../../util/graphic';
+import { MatrixCellLayoutInfo, MatrixDim, MatrixXYLocator } from '../../coord/matrix/MatrixDim';
+import Model from '../../model/Model';
+import { NullUndefined, OrdinalRawValue } 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, WH, setTooltipConfig, expandOrShrinkRect } from '../../util/graphic';
+import { ListIterator } from '../../util/model';
+import { isNumber, normalizeCssArray, retrieve2 } from 'zrender/src/core/util';
+import { createNaNRectLike } from '../../coord/matrix/matrixCoordHelper';
+import { MatrixBodyCorner, MatrixBodyOrCornerKind } from '../../coord/matrix/MatrixBodyCorner';
+import { ElementTextConfig } from 'zrender/src/Element';
+
+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 {
@@ -29,152 +54,359 @@ class MatrixView extends ComponentView {
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;
- group.removeAll();
+ renderDimensionCells(
+ group,
+ matrixModel
+ );
- this._renderTable(matrixModel);
- }
+ createBodyAndCorner(
+ group,
+ matrixModel,
+ xDim,
+ yDim
+ );
- protected _renderTable(matrixModel: MatrixModel) {
- const coordSys = matrixModel.coordinateSystem;
- const xDim = coordSys.getDim('x');
- const yDim = coordSys.getDim('y');
- const xModel = matrixModel.getModel('x');
- const yModel = matrixModel.getModel('y');
- const xLabelModel = xModel.getModel('label');
- const yLabelModel = yModel.getModel('label');
- const xItemStyle = xModel.getModel('itemStyle').getItemStyle();
- const yItemStyle = yModel.getModel('itemStyle').getItemStyle();
+ const borderZ2Option = matrixModel.getShallow('borderZ2', true);
+ const outerBorderZ2 = retrieve2(borderZ2Option, Z2_OUTER_BORDER);
+ const dividerLineZ2 = outerBorderZ2 - 1;
- const rect = coordSys.getRect();
- const xLeavesCnt = xDim.getLeavesCount();
- const yLeavesCnt = yDim.getLeavesCount();
- const xCells = xDim.getCells();
- const xHeight = xDim.getHeight();
- const yCells = yDim.getCells();
- const yHeight = yDim.getHeight();
- const cellWidth = rect.width / (xLeavesCnt + yHeight);
- const cellHeight = rect.height / (yLeavesCnt + xHeight);
-
- const xLeft = rect.x + cellWidth * yHeight;
- if (xModel.get('show')) {
- for (let i = 0; i < xCells.length; i++) {
- const cell = xCells[i];
- const width = cellWidth * cell.colSpan;
- const height = cellHeight * cell.rowSpan;
- const left = xLeft + cellWidth * cell.colId;
- const top = rect.y + cellHeight * cell.rowId;
-
- const cellRect = new graphic.Rect({
- shape: {
- x: left,
- y: top,
- width: width,
- height: height
+ // 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,
},
- style: xItemStyle
- });
- this.group.add(cellRect);
-
- if (xLabelModel.get('show')) {
- cellRect.setTextConfig({
- position: 'inside'
- });
- cellRect.setTextContent(
- new graphic.Text({
- style: createTextStyle(xLabelModel, {
- text: cell.value,
- verticalAlign: 'middle',
- align: 'center'
- }),
- silent: xLabelModel.get('silent')
- })
- );
- }
+ 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,
+ ));
}
}
+ }
+}
- const yTop = rect.y + cellHeight * xHeight;
- if (yModel.get('show')) {
- for (let i = 0; i < yCells.length; i++) {
- const cell = yCells[i];
- const width = cellWidth * cell.rowSpan;
- const height = cellHeight * cell.colSpan;
- const left = rect.x + cellWidth * cell.rowId;
- const top = yTop + cellHeight * cell.colId;
-
- this.group.add(new graphic.Rect({
- shape: {
- x: left,
- y: top,
- width: width,
- height: height
- },
- style: yItemStyle
- }));
- if (yLabelModel.get('show')) {
- this.group.add(new graphic.Text({
- style: createTextStyle(yLabelModel, {
- text: cell.value,
- x: left + width / 2,
- y: top + height / 2,
- verticalAlign: 'middle',
- align: 'center'
- })
- }));
+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<TBodyOrCornerKind extends MatrixBodyOrCornerKind>(
+ bodyCornerOptionRoot: TBodyOrCornerKind,
+ bodyOrCorner: MatrixBodyCorner<TBodyOrCornerKind>,
+ 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<MatrixCellLayoutInfo>();
+ const ity = new ListIterator<MatrixCellLayoutInfo>();
+ 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
+}
- // Inner cells
- const innerBackgroundStyle = matrixModel
- .getModel('innerBackgroundStyle')
- .getItemStyle();
- for (let i = 0; i < xLeavesCnt; i++) {
- for (let j = 0; j < yLeavesCnt; j++) {
- const left = xLeft + cellWidth * i;
- const top = yTop + cellHeight * j;
- this.group.add(new graphic.Rect({
- shape: {
- x: left,
- y: top,
- width: cellWidth,
- height: cellHeight
- },
- style: innerBackgroundStyle
- }));
+function createMatrixCell(
+ xyLocator: MatrixXYLocator[],
+ matrixModel: MatrixModel,
+ group: Group,
+ cellOption: MatrixBaseCellOption | NullUndefined,
+ parentItemStyleModel: Model<MatrixCellStyleOption['itemStyle']>,
+ parentLabelModel: Model<MatrixCellStyleOption['label']>,
+ parentCellModel: Model<MatrixCellStyleOption>,
+ 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;
+ let labelShown = false;
+
+ 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;
+
+ if (textValue != null) {
+ const text = textValue + '';
+ _tmpCellLabelModel.option = cellOption ? cellOption.label : null;
+ _tmpCellLabelModel.parentModel = parentLabelModel;
+ if (_tmpCellLabelModel.getShallow('show')) {
+ labelShown = true;
+
+ const padding = decideCellPadding(_tmpCellModel);
+ const lineWidth = cellRect && cellRect.style && cellRect.style.lineWidth;
+
+ BoundingRect.copy(_tmpContentRect, shape);
+ expandOrShrinkRect(_tmpContentRect, lineWidth, true, true);
+ expandOrShrinkRect(_tmpContentRect, padding, true, true);
+
+ // Be conservative, currently do not use `labelStyle.ts#createTextConfig`
+ // to support all `LabelOption`.
+ const textConfig: ElementTextConfig = {};
+ textConfig.position = _tmpCellLabelModel.getShallow('position') || 'inside';
+ textConfig.distance = retrieve2(_tmpCellLabelModel.getShallow('distance'), 5);
+ cellRect.setTextConfig(textConfig);
+
+ cellRect.setTextContent(
+ cellText = new Text({
+ style: createTextStyle(_tmpCellLabelModel, {
+ text,
+ width: makeCellLabelWH(_tmpContentRect, _tmpCellLabelModel, 0),
+ height: makeCellLabelWH(_tmpContentRect, _tmpCellLabelModel, 1),
+ }),
+ z2: z2 + 1,
+ })
+ );
+ }
+ setTooltipConfig({ // At least for text overflow.
+ el: cellRect,
+ componentModel: matrixModel,
+ itemName: text,
+ itemTooltipOption: tooltipOption,
+ formatterParamsExtra: {
+ xyLocator: xyLocator.slice()
}
+ });
+ }
+
+ let rectSilent = _tmpCellModel.get('silent');
+ 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 && labelShown);
+ }
+ if (rectSilent == null) {
+ rectSilent = labelSilent || (
+ !cellRect.style || cellRect.style.fill === 'none' || !cellRect.style.fill
+ );
+ }
+ cellRect.silent = rectSilent;
+ if (cellText) {
+ // FIXME: but currently label is impl as `textContent`, where it's not
+ // supported that it's owner is silent but text is not silent.
+ cellText.silent = labelSilent;
+ }
+}
+const _tmpCellModel = new Model<MatrixCellStyleOption>();
+const _tmpCellItemStyleModel = new Model<MatrixCellStyleOption['itemStyle']>();
+const _tmpCellLabelModel = new Model<MatrixCellStyleOption['label']>();
+const _tmpContentRect = createNaNRectLike();
+
+
+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<LineShape, 'percent'>, 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,
+ });
+}
- // Outer border
- const backgroundStyle = matrixModel
- .getModel('backgroundStyle')
- .getItemStyle();
- this.group.add(new graphic.Rect({
- shape: rect,
- style: backgroundStyle
- }));
- // Header border
- this.group.add(new graphic.Line({
- shape: {
- x1: rect.x,
- y1: yTop,
- x2: rect.x + rect.width,
- y2: yTop
- },
- style: backgroundStyle
- }));
- this.group.add(new graphic.Line({
- shape: {
- x1: xLeft,
- y1: rect.y,
- x2: xLeft,
- y2: rect.y + rect.height
- },
- style: backgroundStyle
- }));
+function makeCellLabelWH(
+ contentRect: RectLike, cellLabelModel: Model<MatrixCellStyleOption['label']>, dimIdx: number
+): number {
+ // - To add some spacing between the text and cell border, using percentage value for `label.width/height`
+ // is not ideal, becuase the padding would vary with the cell size, making the layout inconsistent.
+ // - `lineWith` is not considered deliberatedly, since it may be modified with user interaction.
+ let size = cellLabelModel.getShallow(WH[dimIdx]);
+ if (!isNumber(size)) {
+ size = contentRect[WH[dimIdx]];
}
+ return size;
+}
+
+export function decideCellPadding(cellModel: Model<MatrixCellStyleOption>): number[] {
+ const paddingOption = retrieve2(cellModel.getShallow('padding'), DEFAULT_CELL_PADDING_IF_TEXT);
+ // If cell is used to fill other series, such as heatmap, it's preferable no padding,
+ // but it's irrelevant since currently `dataToLayout` does not consider padding.
+ return normalizeCssArray(paddingOption);
}
+// Consider text overflow truncate
+const DEFAULT_CELL_PADDING_IF_TEXT = 3;
export default MatrixView;
diff --git a/src/component/timeline/SliderTimelineView.ts b/src/component/timeline/SliderTimelineView.ts
index f993e5713..0a547abd7 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 54fbb0e6e..fc7316290 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,
@@ -130,7 +130,6 @@ class TitleModel extends ComponentModel<TitleOption> {
};
}
-
// View
class TitleView extends ComponentView {
@@ -209,11 +208,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 071c009bd..b9214323b 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';
type IconPath = ToolboxFeatureModel['iconPaths'][string];
@@ -292,7 +293,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/VisualMapView.ts b/src/component/visualMap/VisualMapView.ts
index 1c2431707..76cf41f0f 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 8c778ab83..8a933cc77 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<CoordinateSystem['dataToPoint']>[0],
+ opt?: unknown
+ ): ReturnType<CoordinateSystem['dataToPoint']> | 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<CoordinateSystem['dataToLayout']>[0],
+ opt?: unknown
+ ): ReturnType<CoordinateSystem['dataToLayout']> | 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<CoordinateSystem['pointToData']>[0],
+ opt?: unknown
+ ): ReturnType<CoordinateSystem['pointToData']> | 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.<number>} 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 60fbb0318..43d02ee8d 100644
--- a/src/coord/View.ts
+++ b/src/coord/View.ts
@@ -282,19 +282,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/calendar/Calendar.ts b/src/coord/calendar/Calendar.ts
index 3a0909a00..381a9400e 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<SeriesOption & SeriesOnCalendarOptionMixin>) {
- 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 98e904934..1962600a4 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';
export interface CalendarMonthLabelFormatterCallbackParams {
nameMap: string
@@ -152,12 +153,14 @@ export interface CalendarOption extends ComponentOption, BoxLayoutOptionMixin {
}
}
-class CalendarModel extends ComponentModel<CalendarOption> {
+class CalendarModel extends ComponentModel<CalendarOption> implements CoordinateSystemHostModel {
static type = 'calendar';
type = CalendarModel.type;
coordinateSystem: Calendar;
+ static layoutMode = 'box' as const;
+
/**
* @override
*/
@@ -185,7 +188,9 @@ class CalendarModel extends ComponentModel<CalendarOption> {
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 abd1436a9..b17903dc4 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<Calendar['dataToPoint']>[0],
+ clamp?: Parameters<Calendar['dataToPoint']>[1]
+ ): ReturnType<Calendar['dataToPoint']> {
return coordSys.dataToPoint(data, clamp);
+ },
+ layout: function (
+ data: Parameters<Calendar['dataToLayout']>[0],
+ clamp?: Parameters<Calendar['dataToLayout']>[1]
+ ): ReturnType<Calendar['dataToLayout']> {
+ return coordSys.dataToLayout(data, clamp);
}
}
};
diff --git a/src/coord/cartesian/Cartesian2D.ts b/src/coord/cartesian/Cartesian2D.ts
index 4072684d1..2528c7695 100644
--- a/src/coord/cartesian/Cartesian2D.ts
+++ b/src/coord/cartesian/Cartesian2D.ts
@@ -158,8 +158,8 @@ class Cartesian2D extends Cartesian<Axis2D> 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 d22ddef55..1b62f5a4e 100644
--- a/src/coord/cartesian/Grid.ts
+++ b/src/coord/cartesian/Grid.ts
@@ -24,7 +24,7 @@
*/
import {isObject, each, indexOf, retrieve3, keys} from 'zrender/src/core/util';
-import {getLayoutRect, LayoutRect} from '../../util/layout';
+import {createBoxLayoutReference, getLayoutRect, LayoutRect} from '../../util/layout';
import {
createScaleByModel,
ifAxisCrossZero,
@@ -46,13 +46,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';
type Cartesian2DDimensionName = 'x' | 'y';
@@ -169,16 +170,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;
@@ -468,7 +465,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;
@@ -535,36 +535,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 5e2f43860..7318a4949 100644
--- a/src/coord/cartesian/GridModel.ts
+++ b/src/coord/cartesian/GridModel.ts
@@ -19,11 +19,15 @@
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';
-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 6f977bf2a..9c746aecb 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 736eafee9..ca4bdcbac 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<SeriesModel>();
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 130d95916..e4ff951f8 100644
--- a/src/coord/geo/Geo.ts
+++ b/src/coord/geo/Geo.ts
@@ -215,32 +215,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/geoCreator.ts b/src/coord/geo/geoCreator.ts
index f51700ae0..ee5f88619 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';
export type resizeGeoType = typeof resizeGeo;
@@ -95,8 +95,9 @@ function resizeGeo(this: Geo, geoModel: ComponentModel<GeoOption | MapSeriesOpti
const centerOption = geoModel.get('layoutCenter');
const sizeOption = geoModel.get('layoutSize');
- const viewWidth = api.getWidth();
- const viewHeight = api.getHeight();
+ // Laying out geo on `dataCoordSys`, such as cartesian, works theoretically but not supported yet.
+ // Therefore here we only handle cases that laying out on `boxCoordSys`, such as matrix/calendar.
+ const {refContainer} = layout.createBoxLayoutReference(geoModel, api);
const aspect = rect.width / rect.height * this.aspectScale;
@@ -106,10 +107,10 @@ function resizeGeo(this: Geo, geoModel: ComponentModel<GeoOption | MapSeriesOpti
if (centerOption && sizeOption) {
center = [
- numberUtil.parsePercent(centerOption[0], viewWidth),
- numberUtil.parsePercent(centerOption[1], viewHeight)
+ numberUtil.parsePercent(centerOption[0], refContainer.width) + refContainer.x,
+ numberUtil.parsePercent(centerOption[1], refContainer.height) + refContainer.y
];
- size = numberUtil.parsePercent(sizeOption, Math.min(viewWidth, viewHeight));
+ size = numberUtil.parsePercent(sizeOption, Math.min(refContainer.width, refContainer.height));
if (!isNaN(center[0]) && !isNaN(center[1]) && !isNaN(size)) {
useCenterAndSize = true;
@@ -139,13 +140,9 @@ function resizeGeo(this: Geo, geoModel: ComponentModel<GeoOption | MapSeriesOpti
else {
// Use left/top/width/height
const boxLayoutOption = geoModel.getBoxLayoutParams() as Parameters<typeof layout.getLayoutRect>[0];
-
boxLayoutOption.aspect = aspect;
- viewRect = layout.getLayoutRect(boxLayoutOption, {
- width: viewWidth,
- height: viewHeight
- });
+ viewRect = layout.getLayoutRect(boxLayoutOption, refContainer);
}
this.setViewRect(viewRect.x, viewRect.y, viewRect.width, viewRect.height);
@@ -201,13 +198,18 @@ class GeoCreator implements CoordinateSystemCreator {
});
ecModel.eachSeries(function (seriesModel) {
- const coordSys = seriesModel.get('coordinateSystem');
- if (coordSys === 'geo') {
- const geoIndex = (
- seriesModel as SeriesModel<SeriesOption & SeriesOnGeoOptionMixin>
- ).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;
+ }
+ });
});
// If has map series
diff --git a/src/coord/matrix/Matrix.ts b/src/coord/matrix/Matrix.ts
index 37dd9a60e..451e4e04f 100644
--- a/src/coord/matrix/Matrix.ts
+++ b/src/coord/matrix/Matrix.ts
@@ -18,31 +18,67 @@
*/
import { RectLike } from 'zrender/src/core/BoundingRect';
-import type SeriesModel from '../../model/Series';
-import type { SeriesOnMatrixOptionMixin, SeriesOption } from '../../util/types';
-import { CoordinateSystem, CoordinateSystemMaster } from '../CoordinateSystem';
+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 from './MatrixModel';
+import MatrixModel, {
+ MatrixCoordRangeOption,
+ MatrixDimensionCellOption, MatrixDimensionLevelOption, MatrixDimensionModel
+} from './MatrixModel';
import { LayoutRect, getLayoutRect } from '../../util/layout';
-import { MatrixDim } from './MatrixDim';
-import { ParsedModelFinder, ParsedModelFinderKnown } from '../../util/model';
+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 ['x', 'y', 'value'];
+ 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;
- private _xDim: MatrixDim;
- private _yDim: MatrixDim;
- private _lineWidth: number;
static create(ecModel: GlobalModel, api: ExtensionAPI) {
const matrixList: Matrix[] = [];
@@ -53,119 +89,387 @@ class Matrix implements CoordinateSystem, CoordinateSystemMaster {
matrixModel.coordinateSystem = matrix;
});
- ecModel.eachSeries(function (matrixSeries: SeriesModel<SeriesOption & SeriesOnMatrixOptionMixin>) {
- if (matrixSeries.get('coordinateSystem') === 'matrix') {
- // Inject coordinate system
- matrixSeries.coordinateSystem = matrixList[matrixSeries.get('matrixIndex') || 0];
- }
+ // 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;
- this._xDim = new MatrixDim(matrixModel.get('x'));
- this._yDim = new MatrixDim(matrixModel.get('y'));
+ 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;
}
- getDim(dim: 'x' | 'y'): MatrixDim {
- return dim === 'x' ? this._xDim : this._yDim;
- }
+ private _resize(matrixModel: MatrixModel, api: ExtensionAPI) {
+ const dims = this._dims;
+ const dimModels = this._dimModels;
+ const viewportSize = new Point(api.getWidth(), api.getHeight());
+
+ const rect = this._rect = getLayoutRect(matrixModel.getBoxLayoutParams(), {
+ width: viewportSize.x,
+ height: viewportSize.y,
+ });
+
+ layOutUnitsOnDimension(dimModels, dims, rect, viewportSize, 0);
+ layOutUnitsOnDimension(dimModels, dims, rect, viewportSize, 1);
- update(ecModel: GlobalModel, api: ExtensionAPI) {
- this.resize(this._model, api);
+ layOutDimCellsRestInfoByUnit(0, dims);
+ layOutDimCellsRestInfoByUnit(1, dims);
+
+ layOutBodyCornerCellMerge(this._model.getBody(), dims);
+ layOutBodyCornerCellMerge(this._model.getCorner(), dims);
}
- resize(matrixModel: MatrixModel, api: ExtensionAPI) {
- const boxLayoutParams = matrixModel.getBoxLayoutParams();
- const gridRect = getLayoutRect(
- boxLayoutParams, {
- width: api.getWidth(),
- height: api.getHeight()
- });
- this._rect = gridRect;
- this._lineWidth = matrixModel.getModel('backgroundStyle')
- .getItemStyle().lineWidth || 0;
+ /**
+ * @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<Matrix['dataToLayout']>[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;
}
- dataToPoint(data: [string, string]): number[] {
- const xCell = this._xDim.getCell(data[0]);
- const yCell = this._yDim.getCell(data[1]);
- if (!xCell || !yCell) {
- // Point not found
- return [NaN, NaN];
+ /**
+ * @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` currently until real requirements come, because it's
+ * allowed to input non-leaf dimension 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.
+ */
+ 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;
}
- const xLeavesCnt = this._xDim.getLeavesCount();
- const yLeavesCnt = this._yDim.getLeavesCount();
- const xHeight = this._xDim.getHeight();
- const yHeight = this._yDim.getHeight();
- const cellWidth = this._rect.width / (xLeavesCnt + yHeight) * xCell.colSpan;
- const cellHeight = this._rect.height / (yLeavesCnt + xHeight) * yCell.rowSpan;
- return [
- this._rect.x + this._rect.width / (xLeavesCnt + yHeight)
- * (xCell.colId + yHeight) + cellWidth / 2,
- this._rect.y + this._rect.height / (yLeavesCnt + xHeight)
- * (yCell.colId + xHeight) + cellHeight / 2
- ];
- }
+ parseCoordRangeOption(
+ outLocRange,
+ null,
+ data,
+ dims,
+ retrieve2(opt && opt.clamp, MatrixClampOption.none)
+ );
- dataToRect(data: [string, string]): RectLike {
- const xCell = this._xDim.getCell(data[0]);
- const yCell = this._yDim.getCell(data[1]);
- const xLeavesCnt = this._xDim.getLeavesCount();
- const yLeavesCnt = this._yDim.getLeavesCount();
- const xHeight = this._xDim.getHeight();
- const yHeight = this._yDim.getHeight();
- const cellWidth = this._rect.width / (xLeavesCnt + yHeight) * xCell.colSpan;
- const cellHeight = this._rect.height / (yLeavesCnt + xHeight) * yCell.rowSpan;
- const halfLineWidth = this._lineWidth / 2;
- return {
- x: this._rect.x + this._rect.width / (xLeavesCnt + yHeight)
- * (xCell.colId + yHeight) + halfLineWidth,
- y: this._rect.y + this._rect.height / (yLeavesCnt + xHeight)
- * (yCell.colId + xHeight) + halfLineWidth,
- width: cellWidth - halfLineWidth * 2,
- height: cellHeight - halfLineWidth * 2
- };
+ 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;
}
- pointToData(point: number[]): number[] {
- const xLeavesCnt = this._xDim.getLeavesCount();
- const yLeavesCnt = this._yDim.getLeavesCount();
- const xHeight = this._xDim.getHeight();
- const yHeight = this._yDim.getHeight();
- const cellWidth = this._rect.width / (xLeavesCnt + yHeight);
- const cellHeight = this._rect.height / (yLeavesCnt + xHeight);
- const xIdx = Math.floor((point[0] - this._rect.x) / cellWidth);
- const yIdx = Math.floor((point[1] - this._rect.y) / cellHeight);
+ /**
+ * 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);
+ }
- const xCell = this._xDim.getCellByColId(xIdx - yHeight);
- const yCell = this._yDim.getCellByColId(yIdx - xHeight);
+ return out;
+ }
- return [xCell.colId, yCell.rowId, xCell.colSpan, yCell.rowSpan];
+ convertToPixel(
+ ecModel: GlobalModel,
+ finder: ParsedModelFinder,
+ value: Parameters<Matrix['dataToPoint']>[0],
+ opt?: Parameters<Matrix['dataToPoint']>[1],
+ ): ReturnType<Matrix['dataToPoint']> | NullUndefined {
+ const coordSys = getCoordSys(finder);
+ return coordSys === this ? coordSys.dataToPoint(value, opt) : undefined;
}
- convertToPixel(ecModel: GlobalModel, finder: ParsedModelFinder, value: [string, string]) {
+ convertToLayout(
+ ecModel: GlobalModel,
+ finder: ParsedModelFinder,
+ value: Parameters<Matrix['dataToLayout']>[0],
+ opt?: Parameters<Matrix['dataToLayout']>[1],
+ ): ReturnType<Matrix['dataToLayout']> | NullUndefined {
const coordSys = getCoordSys(finder);
- return coordSys === this ? coordSys.dataToPoint(value) : null;
+ return coordSys === this ? coordSys.dataToLayout(value, opt) : undefined;
}
- convertFromPixel(ecModel: GlobalModel, finder: ParsedModelFinder, pixel: number[]) {
+ convertFromPixel(
+ ecModel: GlobalModel,
+ finder: ParsedModelFinder,
+ pixel: Parameters<Matrix['pointToData']>[0],
+ opt?: Parameters<Matrix['pointToData']>[1],
+ ): ReturnType<Matrix['pointToData']> | NullUndefined {
const coordSys = getCoordSys(finder);
- return coordSys === this ? coordSys.pointToData(pixel) : null;
+ 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<MatrixCellLayoutInfo>();
+const _ptdDimCellIt = new ListIterator<MatrixDimensionCell>();
+
+
+function layOutUnitsOnDimension(
+ dimModels: Matrix['_dimModels'],
+ dims: MatrixDimPair,
+ matrixRect: RectLike,
+ viewportSize: Point,
+ 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<MatrixDimensionLevelOption>();
+ 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<MatrixDimensionCellOption>();
+ 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, viewportSize);
+ 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<MatrixCellLayoutInfo>();
+
+ // 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<MatrixBodyOrCornerKind>, 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;
- containPoint(point: number[]): boolean {
- console.warn('Not implemented.');
- return false;
+ 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,
+ viewportSize: Point
+): number {
+ const sizeNum = parsePositionSizeOption(sizeOption, viewportSize[XY[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;
@@ -179,4 +483,150 @@ function getCoordSys(finder: ParsedModelFinderKnown): Matrix {
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 000000000..e0e3be8ac
--- /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<MatrixBodyCornerCell, string>;
+
+export type MatrixBodyOrCornerKind = 'body' | 'corner';
+type MatrixBodyOrCornerOption<TKind extends MatrixBodyOrCornerKind> =
+ ('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<TKind extends MatrixBodyOrCornerKind> {
+
+ /**
+ * Be sparse, item exists only if needed.
+ */
+ private _cellMap: MatrixModelBodyCornerCellMap | NullUndefined;
+ private _cellMergeOwnerList: MatrixBodyCornerCell[];
+
+ private _model: Model<MatrixBodyOrCornerOption<TKind>>;
+ private _dims: MatrixDimPair;
+ private _kind: TKind;
+
+ constructor(
+ kind: TKind,
+ bodyOrCornerModel: Model<MatrixBodyOrCornerOption<TKind>>,
+ 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
index 9261500dc..a13b10827 100644
--- a/src/coord/matrix/MatrixDim.ts
+++ b/src/coord/matrix/MatrixDim.ts
@@ -1,206 +1,460 @@
-/*
-* 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 { reduce, isString } from 'zrender/src/core/util';
-import { ParsedValue } from '../../util/types';
-
-export type MatrixNodeOption = {
- value?: string;
- children?: MatrixNodeOption[];
-};
+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 type MatrixNodeRawOption = string | MatrixNodeOption;
-export interface MatrixDimOption {
- show?: boolean;
- data?: MatrixNodeOption[];
+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 interface MatrixDimRawOption {
- data?: MatrixNodeRawOption[];
+
+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;
}
-export interface MatrixCell {
- value: string;
- rowId: number;
- rowSpan: number;
- colId: number;
- colSpan: number;
+/**
+ * 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 _option: MatrixDimOption;
- private _cells: MatrixCell[];
- private _height: number;
private _leavesCount: number;
- constructor(option: MatrixDimOption) {
- this._option = option || { data: [] };
- if (!this._option.data) {
- this._option.data = [];
- }
- this._initCells();
- }
+ private _model: MatrixDimensionModel;
+ private _ordinalMeta: OrdinalMeta;
+ // Only for uniformly parsing.
+ private _scale: Ordinal;
- getLeavesCount() {
- if (this._leavesCount != null) {
- return this._leavesCount;
- }
- const data = this._option.data;
- if (!data) {
- this._leavesCount = 0;
- return 0;
- }
- if (isString(data)) {
- this._leavesCount = 1;
- return 1;
- }
- let cnt = 0;
- for (let i = 0; i < data.length; i++) {
- cnt += this._countLeaves(data[i]);
- }
- this._leavesCount = cnt;
- return cnt;
- }
+ private _uniqueValueGen: ReturnType<typeof createUniqueValueGenerator>;
- getHeight() {
- if (!this._option.show) {
- return 0;
- }
- if (this._height != null) {
- return this._height;
- }
- const data = this._option.data;
- if (!data) {
- return 0;
+ 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 (isString(data)) {
- return 1;
+ if (dimModelData) {
+ this._initByDimModelData(dimModelData);
}
- let height = 0;
- for (let i = 0; i < data.length; i++) {
- height = Math.max(height, this._countHeight(data[i]));
+ else {
+ this._initBySeriesData();
}
- this._height = height;
- return height;
}
- getCells() {
- return this._cells;
- }
+ 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();
- getCell(value: ParsedValue) {
- for (let i = 0; i < this._cells.length; i++) {
- // value can be number while this._cells[i].value is string
- // eslint-disable-next-line eqeqeq
- if (this._cells[i].value == value) {
- return this._cells[i];
+ 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;
}
- }
- getCellByColId(id: number) {
- for (let i = 0; i < this._cells.length; i++) {
- if (this._cells[i].colId === id) {
- return this._cells[i];
+ 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 _initCells(): void {
- this._cells = [];
- const data = this._option.data;
- for (let i = 0, rowId = 0, colId = 0; i < data.length; i++) {
- const node = data[i];
- const result = this._traverseInitCells(node, rowId, colId);
- rowId = result.rowId;
- colId = result.colId;
- }
+ 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 _traverseInitCells(
- node: MatrixNodeOption,
- rowId: number,
- colId: number = 0
- ): { rowId: number, colId: number } {
- if (typeof node === 'string') {
- // When node is a string, it's a leaf with colSpan of 1
- this._cells.push({
- value: node,
- rowId,
- colId,
- rowSpan: 1,
- colSpan: 1
- });
- return { rowId, colId: colId + 1 };
- }
+ private _setCellId(cell: MatrixDimensionCell) {
+ const levelsLen = this._levels.length;
+ const dimIdx = this.dimIdx;
+ setDimXYValue(cell.id, dimIdx, cell.firstLeafLocator, cell.level - levelsLen);
+ }
- let currentColId = colId;
- let totalColSpan = 0;
- const childrenColSpans = [];
+ private _initCellsId() {
+ const levelsLen = this._levels.length;
+ const dimIdx = this.dimIdx;
+ each(this._cells, cell => {
+ setDimXYValue(cell.id, dimIdx, cell.firstLeafLocator, cell.level - levelsLen);
+ });
+ }
- if (node.children && node.children.length) {
- for (const child of node.children) {
- const result = this._traverseInitCells(child, rowId + 1, currentColId);
- const childColSpan = result.colId - currentColId;
- childrenColSpans.push(childColSpan);
- currentColId = result.colId;
- }
- totalColSpan = reduce(childrenColSpans, (a, b) => a + b, 0);
- }
- else {
- // If no children, it's a leaf node with colSpan of 1
- totalColSpan = 1;
- }
+ private _initLevelIdOptions() {
+ const levelsLen = this._levels.length;
+ const dimIdx = this.dimIdx;
+ let levelOptionList = this._model.get('levels', true);
+ levelOptionList = isArray(levelOptionList) ? levelOptionList : [];
- // Create cell for the current node
- this._cells.push({
- value: node.value,
- rowId,
- colId,
- rowSpan: 1,
- colSpan: totalColSpan
+ each(this._levels, (levelCfg, level) => {
+ setDimXYValue(levelCfg.id, dimIdx, 0, level - levelsLen);
+ levelCfg.option = levelOptionList[level];
});
+ }
- return { rowId, colId: colId + totalColSpan };
+ shouldShow(): boolean {
+ return !!this._model.getShallow('show', true);
}
- private _countHeight(node: MatrixNodeOption): number {
- if (typeof node === 'string' || !node.children) {
- return 1;
+ /**
+ * Iterate leaves (they are layout units) if dimIdx === this.dimIdx.
+ * Iterate levels if dimIdx !== this.dimIdx.
+ */
+ resetLayoutIterator(
+ it: ListIterator<MatrixCellLayoutInfo> | NullUndefined,
+ dimIdx: number,
+ startLocator?: MatrixXYLocator | NullUndefined,
+ count?: number | NullUndefined,
+ ): ListIterator<MatrixCellLayoutInfo> {
+ it = it || new ListIterator<MatrixCellLayoutInfo>();
+ 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);
}
- let height = 0;
- for (let i = 0; i < node.children.length; i++) {
- height = Math.max(height, this._countHeight(node.children[i]));
+ 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 height + 1;
+ return it;
+ }
+
+ resetCellIterator(
+ it?: ListIterator<MatrixDimensionCell>
+ ): ListIterator<MatrixDimensionCell> {
+ return (it || new ListIterator<MatrixDimensionCell>()).reset(this._cells, 0);
+ }
+
+ resetLevelIterator(
+ it?: ListIterator<MatrixDimensionLevelInfo>
+ ): ListIterator<MatrixDimensionLevelInfo> {
+ return (it || new ListIterator<MatrixDimensionLevelInfo>()).reset(this._levels, 0);
}
- private _countLeaves(node: MatrixNodeOption): number {
- if (typeof node === 'string' || !node.children) {
- return 1;
+ 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);
}
- let cnt = 0;
- for (let i = 0; i < node.children.length; i++) {
- cnt += this._countLeaves(node.children[i]);
+ }
+
+ 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<true, string>();
+ 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 cnt;
}
+ return {calcDupBase, ensureValueUnique};
}
diff --git a/src/coord/matrix/MatrixModel.ts b/src/coord/matrix/MatrixModel.ts
index 979e15413..7b1f17b9b 100644
--- a/src/coord/matrix/MatrixModel.ts
+++ b/src/coord/matrix/MatrixModel.ts
@@ -17,72 +17,300 @@
* under the License.
*/
+import OrdinalMeta from '../../data/OrdinalMeta';
import ComponentModel from '../../model/Component';
-import { BoxLayoutOptionMixin, ComponentOption, ItemStyleOption, LabelOption } from '../../util/types';
+import Model from '../../model/Model';
+import {
+ BoxLayoutOptionMixin, CommonTooltipOption, ComponentOption, ItemStyleOption, LabelOption,
+ LineStyleOption,
+ NullUndefined, OrdinalNumber, OrdinalRawValue,
+ PositionSizeOption
+} from '../../util/types';
import Matrix from './Matrix';
-import { MatrixNodeOption } from './MatrixDim';
+import { MatrixDim, MatrixXYLocator } from './MatrixDim';
+import { MatrixBodyCorner } from './MatrixBodyCorner';
+import { CoordinateSystemHostModel } from '../CoordinateSystem';
+
export interface MatrixOption extends ComponentOption, BoxLayoutOptionMixin {
mainType?: 'matrix';
- x?: {
- show?: boolean;
- data?: MatrixNodeOption[];
- label?: LabelOption;
- itemStyle?: ItemStyleOption;
- }
- y?: {
- show?: boolean;
- data?: MatrixNodeOption[];
- label?: LabelOption;
- itemStyle?: ItemStyleOption;
- }
+ 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;
- innerBackgroundStyle?: ItemStyleOption;
+ // Used on the outer border and the divider line.
+ borderZ2?: number;
+ tooltip?: CommonTooltipOption<MatrixTooltipFormatterParams>;
+}
+
+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 applied in `matrix.dataToPoint` and `matrix.dataToLayout`.)
+ * 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 corner of the body as the orgin 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.
+ 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.
+ levelSize?: PositionSizeOption;
+ // Other level specific options may added if needed, such as border-bottom/right style.
+}
+
+export interface MatrixDimensionModel extends Model<MatrixDimensionOption> {
}
-const defaultDimOption = {
+/**
+ * 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 {
+ label?: LabelOption;
+ itemStyle?: ItemStyleOption;
+ // Padding for the inner content (label / series / other coord sys).
+ padding?: number | number[];
+ cursor?: string;
+ // By default, auto decide whether to be silent, considering tooltip.
+ silent?: boolean | NullUndefined;
+ // Used when style conflict - espetially thick border style.
+ z2?: number;
+}
+
+export interface MatrixTooltipFormatterParams {
+ componentType: 'matrix'
+ matrixIndex: number
+ name: string
+ $vars: ['name', 'xyLocator']
+}
+
+const defaultLabelOption: LabelOption = {
show: true,
- data: [] as MatrixNodeOption[],
- label: {
- show: true,
- color: '#333'
+ color: '#333',
+ overflow: 'truncate',
+};
+function makeDefaultCellItemStyleOption(isCorner: boolean) {
+ return {
+ color: 'none',
+ borderWidth: 1,
+ borderColor: isCorner ? 'transparent' : '#ccc',
+ };
+};
+const defaultDimOption: MatrixDimensionOption = {
+ show: true,
+ label: defaultLabelOption,
+ itemStyle: makeDefaultCellItemStyleOption(false),
+ silent: undefined,
+ dividerLineStyle: {
+ width: 1,
+ color: '#aaa',
},
- itemStyle: {
+};
+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: '#aaa',
borderWidth: 1,
- borderColor: '#ccc'
- }
+ },
};
-class MatrixModel extends ComponentModel<MatrixOption> {
+
+class MatrixModel extends ComponentModel<MatrixOption> implements CoordinateSystemHostModel {
static type = 'matrix';
type = MatrixModel.type;
coordinateSystem: Matrix;
- static defaultOption: MatrixOption = {
- z: 2,
- left: '10%',
- top: '10%',
- right: '10%',
- bottom: '10%',
- x: defaultDimOption,
- y: defaultDimOption,
- backgroundStyle: {
- color: 'none',
- borderColor: '#aaa',
- borderWidth: 1,
- borderType: 'solid',
- opacity: 1
- },
- innerBackgroundStyle: {
- color: 'none',
- borderColor: '#ccc',
- borderWidth: 1,
- borderType: 'solid',
- opacity: 1
- }
+ 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<MatrixDimensionOption> {
+ 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 000000000..4605d74d2
--- /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
index b014c22ba..b3ef772a9 100644
--- a/src/coord/matrix/prepareCustom.ts
+++ b/src/coord/matrix/prepareCustom.ts
@@ -17,7 +17,7 @@
* under the License.
*/
-import Matrix from './Matrix';
+import type Matrix from './Matrix';
export default function matrixPrepareCustom(coordSys: Matrix) {
const rect = coordSys.getRect();
@@ -31,12 +31,17 @@ export default function matrixPrepareCustom(coordSys: Matrix) {
height: rect.height
},
api: {
- coord: function (data: [string, string]) {
- return coordSys.dataToPoint(data);
+ coord: function (
+ data: Parameters<Matrix['dataToPoint']>[0],
+ opt?: Parameters<Matrix['dataToPoint']>[1]
+ ): ReturnType<Matrix['dataToPoint']> {
+ return coordSys.dataToPoint(data, opt);
},
- size: function (data: [string, string]) {
- const rect = coordSys.dataToRect(data);
- return [rect.width, rect.height];
+ layout: function (
+ data: Parameters<Matrix['dataToLayout']>[0],
+ opt?: Parameters<Matrix['dataToLayout']>[1]
+ ): ReturnType<Matrix['dataToLayout']> {
+ return coordSys.dataToLayout(data, opt);
}
}
};
diff --git a/src/coord/parallel/Parallel.ts b/src/coord/parallel/Parallel.ts
index c694f55ff..5108bbf91 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 e7b9db6cc..a0d294f38 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;
}
/**
@@ -238,12 +239,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 fe29f3ed1..224de9c3b 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 732eceb84..6ce88ec75 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 c9226b5aa..80e2ac04c 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 b1f59222c..273c24b1c 100644
--- a/src/core/CoordinateSystem.ts
+++ b/src/core/CoordinateSystem.ts
@@ -21,42 +21,329 @@ 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;
+}): boolean {
+ const {
+ targetModel,
+ coordSysType,
+ coordSysProvider,
+ isDefaultDataCoordSys,
+ } = 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__) {
+ 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/echarts.ts b/src/core/echarts.ts
index 1c7e4e038..351429414 100644
--- a/src/core/echarts.ts
+++ b/src/core/echarts.ts
@@ -107,7 +107,10 @@ import {
ScaleDataValue,
ZRElementEventName,
ECElementEvent,
- AnimationOption
+ AnimationOption,
+ CoordinateSystemDataLayout,
+ NullUndefined,
+ CoordinateSystemDataCoord,
} from '../util/types';
import Displayable from 'zrender/src/graphic/Displayable';
import { seriesSymbolTask, dataSymbolTask } from '../visual/symbol';
@@ -278,12 +281,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;
@@ -940,21 +960,61 @@ class ECharts extends Eventful<ECEventDefinition> {
/**
* 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);
}
/**
@@ -1849,12 +1909,34 @@ class ECharts extends Eventful<ECEventDefinition> {
}
};
- 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;
@@ -1868,7 +1950,7 @@ class ECharts extends Eventful<ECEventDefinition> {
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;
}
@@ -1880,6 +1962,7 @@ class ECharts extends Eventful<ECEventDefinition> {
);
}
};
+ doConvertPixel = doConvertPixelImpl;
updateStreamModes = function (ecIns: ECharts, ecModel: GlobalModel): void {
const chartsMap = ecIns._chartsMap;
diff --git a/src/data/OrdinalMeta.ts b/src/data/OrdinalMeta.ts
index 441e012af..85f08b75c 100644
--- a/src/data/OrdinalMeta.ts
+++ b/src/data/OrdinalMeta.ts
@@ -33,18 +33,46 @@ class OrdinalMeta {
private _map: HashMap<OrdinalNumber>;
- 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/label/labelStyle.ts b/src/label/labelStyle.ts
index fd44f45e9..162ec49d5 100644
--- a/src/label/labelStyle.ts
+++ b/src/label/labelStyle.ts
@@ -323,7 +323,7 @@ export function createTextStyle(
return textStyle;
}
export function createTextConfig(
- textStyleModel: Model,
+ textStyleModel: Model<LabelOption<{positionExtra: 'outside'}>>,
opt?: Pick<TextCommonParams, 'defaultOutsidePosition' | 'inheritColor'>,
isNotNormal?: boolean
) {
@@ -336,7 +336,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;
@@ -357,6 +357,7 @@ export function createTextConfig(
: 'auto';
return textConfig;
}
+
/**
* The uniform entry of set text style, that is, retrieve style definitions
* from `model` and set to `textStyle` object.
diff --git a/src/model/Component.ts b/src/model/Component.ts
index 889159ff4..9c20b276a 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<Opt extends ComponentOption = ComponentOption> 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<Opt extends ComponentOption = ComponentOption> extends Mode
__viewId: string;
__requireNewView: boolean;
+
static protoInitialize = (function () {
const proto = ComponentModel.prototype;
proto.type = 'component';
diff --git a/src/model/Series.ts b/src/model/Series.ts
index 89be20c7d..6ede6cc7c 100644
--- a/src/model/Series.ts
+++ b/src/model/Series.ts
@@ -146,7 +146,8 @@ class SeriesModel<Opt extends SeriesOption = SeriesOption> 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 a3c62c890..e9010a983 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<AxisBaseOptionCommon, 'type'>>
+ & Pick<AxisModelExtendedInCreator, 'getOrdinalMeta'>;
type Fetcher = (
seriesModel: SeriesModel,
result: CoordSysInfo,
- axisMap: HashMap<AxisBaseModel>,
- categoryAxisMap: HashMap<AxisBaseModel>
+ axisMap: HashMap<FetcherAxisModel>,
+ categoryAxisMap: HashMap<FetcherAxisModel>
) => 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<SupportedCoordSys, Fetcher> = {
cartesian2d: function (
@@ -202,7 +210,27 @@ const fetchers: Record<SupportedCoordSys, Fetcher> = {
}
}
});
- }
+ },
+
+ 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/util/graphic.ts b/src/util/graphic.ts
index a7e26e1d9..08511e2e5 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,8 @@ import {
ZRRectLike,
ZRStyleProps,
CommonTooltipOption,
- ComponentItemTooltipLabelFormatterParams
+ ComponentItemTooltipLabelFormatterParams,
+ NullUndefined
} from './types';
import {
extend,
@@ -63,7 +64,8 @@ import {
keys,
each,
hasOwn,
- isArray
+ isArray,
+ isNumber
} from 'zrender/src/core/util';
import { getECData } from './innerStore';
import ComponentModel from '../model/Component';
@@ -91,6 +93,9 @@ const _customShapeMap: Dictionary<{ new(): Path }> = {};
type ExtendShapeOpt = Parameters<typeof Path.extend>[0];
type ExtendShapeReturn = ReturnType<typeof Path.extend>;
+export const XY = ['x', 'y'] as const;
+export const WH = ['width', 'height'] as const;
+
/**
* Extend shape with parameters
*/
@@ -281,16 +286,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 +567,87 @@ function nearZero(val: number) {
return val <= (1e-6) && val >= -(1e-6);
}
+/**
+ * FIXME: merge this method with `applyMarginToRect` in other git branch.
+ *
+ * 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).
+ */
+export function expandOrShrinkRect(
+ rect: RectLike,
+ delta: number[] | number | NullUndefined,
+ shrinkOrExpand: boolean,
+ noNegative: boolean
+): void {
+ if (delta == null) {
+ return;
+ }
+ 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);
+}
+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,
diff --git a/src/util/layout.ts b/src/util/layout.ts
index 31445d83e..b237077a1 100644
--- a/src/util/layout.ts
+++ b/src/util/layout.ts
@@ -20,13 +20,17 @@
// Layout helpers for each component positioning
import * as zrUtil from 'zrender/src/core/util';
-import BoundingRect from 'zrender/src/core/BoundingRect';
+import BoundingRect, { RectLike } from 'zrender/src/core/BoundingRect';
import {parsePercent} from './number';
import * as formatUtil from './format';
-import { BoxLayoutOptionMixin, ComponentLayoutMode } from './types';
+import { BoxLayoutOptionMixin, ComponentLayoutMode, NullUndefined } from './types';
import Group from 'zrender/src/graphic/Group';
import Element from 'zrender/src/Element';
import { Dictionary } from 'zrender/src/core/types';
+import { ComponentModel } from '../echarts.all';
+import ExtensionAPI from '../core/ExtensionAPI';
+import { error } from './log';
+import { BoxCoordinateSystemCoordFrom, getCoordForBoxCoordSys } from '../core/CoordinateSystem';
const each = zrUtil.each;
@@ -189,6 +193,13 @@ export function getAvailableSize(
};
}
+type GetLayoutRectInputContainerRect = {
+ x?: number, // 0 by default
+ y?: number, // 0 by default
+ width: number,
+ height: number,
+};
+
/**
* Parse position info.
*/
@@ -196,7 +207,7 @@ export function getLayoutRect(
positionInfo: BoxLayoutOptionMixin & {
aspect?: number // aspect is width / height
},
- containerRect: {width: number, height: number},
+ containerRect: GetLayoutRectInputContainerRect,
margin?: number | number[]
): LayoutRect {
margin = formatUtil.normalizeCssArray(margin || 0);
@@ -287,11 +298,105 @@ 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;
return rect;
}
+type CreateBoxLayoutReferenceOpt<TEnableByCenter extends boolean = false> = {
+ // 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> = 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<TEnableByCenter extends boolean = false>(
+ model: ComponentModel,
+ api: ExtensionAPI,
+ opt?: CreateBoxLayoutReferenceOpt<TEnableByCenter>
+): BoxLayoutReferenceResult<TEnableByCenter> {
+
+ 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<TEnableByCenter>;
+}
+
/**
* Position a zr element in viewport
@@ -336,7 +441,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],
diff --git a/src/util/model.ts b/src/util/model.ts
index 9b3e65ff3..dbeee76fd 100644
--- a/src/util/model.ts
+++ b/src/util/model.ts
@@ -45,14 +45,15 @@ 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';
function interpolateNumber(p0: number, p1: number, percent: number): number {
return (p1 - p0) * percent + p0;
@@ -952,16 +953,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,
@@ -1095,3 +1112,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<TItem> {
+
+ 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<TItem> {
+ 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 cd6f42994..10053afc5 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): 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);
+}
- 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): number {
+ if (zrUtil.isString(option)) {
+ if (_trim(option).match(/%$/)) {
+ return parseFloat(option) / 100 * percentBase;
+ }
+ 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 9045eadc4..af351b36e 100644
--- a/src/util/types.ts
+++ b/src/util/types.ts
@@ -345,9 +345,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.
@@ -387,6 +388,23 @@ 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 interface ScaleTick {
level?: number,
value: number
@@ -421,6 +439,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;
@@ -771,17 +805,24 @@ 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 {
// Can be percent
@@ -1018,7 +1059,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
@@ -1040,14 +1081,16 @@ export interface LabelFormatterCallback<T = CallbackDataParams> {
* 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[]
@@ -1071,8 +1114,11 @@ export interface LabelOption extends TextCommonOption {
rich?: Dictionary<TextCommonOption>
}
-export interface SeriesLabelOption<T extends CallbackDataParams = CallbackDataParams> extends LabelOption {
- formatter?: string | LabelFormatterCallback<T>
+export interface SeriesLabelOption<
+ TCallbackDataParams extends CallbackDataParams = CallbackDataParams,
+ TNuance extends {positionExtra: unknown} = {positionExtra: never}
+> extends LabelOption<TNuance> {
+ formatter?: string | LabelFormatterCallback<TCallbackDataParams>
}
/**
@@ -1462,8 +1508,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';
/**
@@ -1595,10 +1658,6 @@ export interface SeriesOption<
progressive?: number | false
progressiveThreshold?: number
progressiveChunkMode?: 'mod'
- /**
- * Not available on every series
- */
- coordinateSystem?: string
hoverLayerThreshold?: number
@@ -1665,7 +1724,7 @@ export interface SeriesOnCalendarOptionMixin {
calendarId?: string
}
-export interface SeriesOnMatrixOptionMixin {
+export interface ComponentOnMatrixOptionMixin {
matrixIndex?: number
matrixId?: string
}
diff --git a/test/calendar-other-coord-sys.html b/test/calendar-other-coord-sys.html
new file mode 100644
index 000000000..b03f0a4c6
--- /dev/null
+++ b/test/calendar-other-coord-sys.html
@@ -0,0 +1,748 @@
+
+<!--
+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.
+-->
+
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>calendar</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <script src="lib/simpleRequire.js"></script>
+ <script src="lib/config.js"></script>
+ <script src="lib/jquery.min.js"></script>
+ <script src="lib/facePrint.js"></script>
+ <script src="lib/testHelper.js"></script>
+ <!-- <script src="lib/canteen.js"></script> -->
+ <!-- <script src="lib/draggable.js"></script> -->
+ <link rel="stylesheet" href="lib/reset.css" />
+ </head>
+ <body>
+ <style>
+ </style>
+
+ <div id="main0"></div>
+ <div id="main_custom_series"></div>
+
+
+
+ <script>
+ require([
+ 'echarts',
+ './data/czech_geo.json',
+ 'data/svg/Beef_cuts_France.svg',
+ ], function (echarts, czech_geo, Beef_cuts_France) {
+ echarts.registerMap('czech_geo', czech_geo);
+ echarts.registerMap('Beef_cuts_France', {
+ svg: Beef_cuts_France
+ });
+
+ const option = {
+ calendar: [{
+ id: 'calendar0',
+ orient: 'vertical',
+ width: 100,
+ right: 30,
+ height: '10%',
+ bottom: 30,
+ itemStyle: {
+ borderWidth: 1,
+ },
+ yearLabel: {
+ show: false,
+ textStyle: {
+ fontSize: 30
+ }
+ },
+ dayLabel: {
+ firstDay: 1,
+ // nameMap: ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
+ },
+ monthLabel: {
+ show: false
+ },
+ range: ['2017-01']
+ }, {
+ id: 'calendar1',
+ orient: 'vertical',
+ left: 30,
+ right: 30,
+ top: 70,
+ bottom: 30,
+ itemStyle: {
+ borderWidth: 1,
+ },
+ yearLabel: {
+ show: false,
+ textStyle: {
+ fontSize: 30
+ }
+ },
+ dayLabel: {
+ margin: 20,
+ firstDay: 1,
+ nameMap: ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
+ },
+ monthLabel: {
+ show: false
+ },
+ range: ['2017-02']
+ }],
+ tooltip : {
+ // trigger: 'axis',
+ },
+ title: [{
+ text: 'This is toolbox',
+ left: 'center',
+ coordinateSystem: 'calendar',
+ calendarId: 'calendar1',
+ coord: '2017-02-02',
+ textStyle: {
+ fontSize: 10
+ }
+ }],
+ legend: {
+ bottom: 20,
+ data: ['line_q', 'line_p'],
+ coordinateSystem: 'calendar',
+ calendarId: 'calendar1',
+ coord: '2017-02-01',
+ bottom: 3,
+ textStyle: {
+ fontSize: 9,
+ },
+ itemHeight: 5,
+ itemWidth: 10,
+ itemGap: 3,
+ padding: 2,
+ },
+ xAxis: [{
+ id: 'cartesian_a',
+ axisTick: {show: false},
+ axisLabel: {fontSize: 9, margin: 3},
+ splitLine: {show: false},
+ }],
+ yAxis: [{
+ id: 'cartesian_a',
+ axisTick: {show: false},
+ axisLine: {show: false},
+ axisLabel: {fontSize: 9, margin: 6},
+ }],
+ grid: [{
+ id: 'cartesian_a',
+ coordinateSystem: 'calendar',
+ calendarId: 'calendar1',
+ coord: '2017-02-01',
+ containLabel: true,
+ top: 10,
+ bottom: 22,
+ left: 8,
+ right: 8,
+ tooltip: {
+ trigger: 'axis',
+ }
+ }],
+ toolbox: {
+ show: true,
+ feature: {
+ dataZoom: {
+ yAxisIndex: 'none'
+ },
+ // dataView: { readOnly: false },
+ restore: {},
+ saveAsImage: {}
+ },
+ top: 'center',
+ left: 'center',
+ coordinateSystem: 'calendar',
+ calendarId: 'calendar1',
+ coord: '2017-02-02',
+ },
+ angleAxis: {
+ type: 'category',
+ data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
+ axisLabel: {fontSize: 9},
+ },
+ radiusAxis: {
+ axisLabel: {fontSize: 9},
+ },
+ polar: {
+ // center: ['40%', '50%'],
+ radius: '50%',
+ coordinateSystem: 'calendar',
+ calendarId: 'calendar1',
+ coord: '2017-02-03',
+ },
+ radar: {
+ axisName: {
+ color: '#333',
+ fontSize: 9,
+ },
+ nameGap: 0,
+ radius: '80%',
+ indicator : [
+ {text: 'IE8-', max: 400},
+ {text: 'IE9+', max: 400},
+ {text: 'Safari', max: 400},
+ {text: 'Firefox', max: 400},
+ {text: 'Chrome', max: 400}
+ ],
+ coordinateSystem: 'calendar',
+ calendarId: 'calendar1',
+ coord: '2017-02-04',
+ },
+ singleAxis: {
+ type: 'value',
+ // height: height,
+ splitArea: {
+ show: true
+ },
+ axisTick: {show: false},
+ axisLabel: {fontSize: 9, margin: 3},
+ bottom: 12,
+ coordinateSystem: 'calendar',
+ calendarId: 'calendar1',
+ coord: '2017-02-05',
+ },
+ parallelAxis: [
+ {dim: 0, name: 'f0'},
+ {dim: 1, name: 'f1'},
+ {dim: 2, name: 'f2'}
+ ],
+ parallel: {
+ left: 10,
+ right: 30,
+ top: 30,
+ bottom: 10,
+ coordinateSystem: 'calendar',
+ calendarId: 'calendar1',
+ coord: '2017-02-06',
+ parallelAxisDefault: {
+ nameGap: 5,
+ }
+ },
+ geo: [{
+ map: 'czech_geo',
+ id: 'czech_geo_component',
+ coordinateSystem: 'calendar',
+ calendarId: 'calendar1',
+ coord: '2017-02-07',
+ // left: '50%',
+ // right: 10,
+ // layoutCenter: [10, 10],
+ // layoutSize: 100,
+ }],
+ visualMap: [{
+ type: 'continuous',
+ left: 'center',
+ bottom: 0,
+ // width: '50%',
+ itemHeight: 90,
+ itemWidth: 10,
+ min: 5,
+ max: 100,
+ // text: ['Price'],
+ realtime: true,
+ calculable: true,
+ orient: 'horizontal',
+ seriesId: ['Beef_cuts_France', 'czech_geo_series'],
+ inRange: {
+ color: ['#dbac00', '#db6e00', '#cf0000']
+ },
+ coordinateSystem: 'calendar',
+ calendarId: 'calendar1',
+ coord: '2017-02-08',
+ }, {
+ type: 'piecewise',
+ right: 0,
+ bottom: 0,
+ top: 'center',
+ // width: '50%',
+ // itemHeight: 90,
+ itemWidth: 10,
+ itemGap: 5,
+ min: 5,
+ max: 100,
+ // text: ['Price'],
+ realtime: true,
+ calculable: true,
+ textStyle: {
+ fontSize: 9,
+ },
+ textGap: 3,
+ // orient: 'horizontal',
+ seriesId: ['Beef_cuts_France', 'czech_geo_series'],
+ inRange: {
+ color: ['#dbac00', '#db6e00', '#cf0000']
+ },
+ coordinateSystem: 'calendar',
+ calendarId: 'calendar1',
+ coord: '2017-02-07',
+ }],
+ dataZoom: [{
+ type: 'slider',
+ xAxisId: 'cartesian_a',
+ coordinateSystem: 'calendar',
+ calendarId: 'calendar1',
+ coord: '2017-02-10',
+ bottom: 0,
+ height: 20,
+ left: 'center',
+ top: 'center',
+ }],
+ series: [{
+ type: 'line',
+ name: 'line_q',
+ xAxisId: 'cartesian_a',
+ yAxisId: 'cartesian_a',
+ data: [[12, 321], [34, 516], [56, 198]]
+ }, {
+ type: 'line',
+ name: 'line_p',
+ xAxisId: 'cartesian_a',
+ yAxisId: 'cartesian_a',
+ data: [[12, 121], [34, 216], [56, 798]]
+ }, {
+ type: 'bar',
+ data: [1, 2, '-', 3, 0, 5, 7],
+ coordinateSystem: 'polar',
+ barMaxWidth: '50%',
+ barMinHeight: 10
+ }, {
+ type: 'radar',
+ symbol: 'none',
+ lineStyle: {
+ color: 'rgba(180,100,120)',
+ width: 1,
+ },
+ emphasis: {
+ areaStyle: {
+ color:'rgba(0,250,0,0.3)'
+ }
+ },
+ data: (function () {
+ const dataList = [];
+ for (let i = 0; i < 10; i++) {
+ dataList.push({
+ value:[
+ (40 - i) * 10,
+ (38 - i) * 4 + 60,
+ i * 15 + 10,
+ i * 30,
+ i * 50 / 2
+ ],
+ name: i + 2000 + ''
+ });
+ }
+ return dataList;
+ })()
+ }, {
+ type: 'scatter',
+ coordinateSystem: 'singleAxis',
+ symbolSize: function (val) {
+ return val[1] / 2;
+ },
+ data: (function () {
+ const dataList = [];
+ for (let i = 0; i < 30; i++) {
+ dataList.push([Math.random() * 100, Math.random() * 30]);
+ }
+ return dataList;
+ })()
+ }, {
+ type: 'parallel',
+ smooth: 0,
+ lineStyle: {color: 'red'},
+ data: (function () {
+ function generate_data(n, m, lb, ub) {
+ let s = [];
+ for (let i = 0; i < n; i++) {
+ let vec = [];
+ for (let j = 0; j < m; j++) {
+ vec.push(lb + (Math.random() * (ub - lb)));
+ }
+ s.push(vec);
+ }
+ return s;
+ }
+ data = generate_data(20, 3, 0.0, 1.0);
+ return data;
+ })(),
+ }, {
+ name: 'French Beef Cuts',
+ id: 'Beef_cuts_France',
+ type: 'map',
+ map: 'Beef_cuts_France',
+ coordinateSystem: 'calendar',
+ calendarId: 'calendar1',
+ coordinateSystemUsage: 'box',
+ coord: '2017-02-08',
+ roam: true,
+ // top: 0,
+ bottom: 0,
+ aspectScale: 1,
+ zoom: 1,
+ // layoutSize: '30%',
+ showLegendSymbol: false,
+ emphasis: {
+ label: {
+ show: false
+ }
+ },
+ data: [
+ { name: 'Queue', value: 15 },
+ { name: 'Langue', value: 35 },
+ { name: 'Plat de joue', value: 15 },
+ { name: 'Gros bout de poitrine', value: 25 },
+ { name: 'Jumeau à pot-au-feu', value: 45 },
+ { name: 'Onglet', value: 85 },
+ { name: 'Plat de tranche', value: 25 },
+ { name: 'Araignée', value: 15 },
+ { name: 'Gîte à la noix', value: 55 },
+ { name: 'Bavette d\'aloyau', value: 25 },
+ { name: 'Tende de tranche', value: 65 },
+ { name: 'Rond de gîte', value: 45 },
+ { name: 'Bavettede de flanchet', value: 85 },
+ { name: 'Flanchet', value: 35 },
+ { name: 'Hampe', value: 75 },
+ { name: 'Plat de côtes', value: 65 },
+ { name: 'Tendron Milieu de poitrine', value: 65 },
+ { name: 'Macreuse à pot-au-feu', value: 85 },
+ { name: 'Rumsteck', value: 75 },
+ { name: 'Faux-filet', value: 65 },
+ { name: 'Côtes Entrecôtes', value: 55 },
+ { name: 'Basses côtes', value: 45 },
+ { name: 'Collier', value: 85 },
+ { name: 'Jumeau à biftek', value: 15 },
+ { name: 'Paleron', value: 65 },
+ { name: 'Macreuse à bifteck', value: 45 },
+ { name: 'Gîte', value: 85 },
+ { name: 'Aiguillette baronne', value: 65 },
+ { name: 'Filet', value: 95 },
+ ]
+ }, {
+ type: 'map',
+ map: 'czech_geo',
+ coordinateSystem: 'calendar',
+ coordinateSystemUsage: 'box',
+ calendarId: 'calendar1',
+ coord: '2017-02-09',
+ // left: '50%',
+ // right: 10,
+ // layoutCenter: [10, 10],
+ // layoutSize: 100,
+ }, {
+ type: 'map',
+ geoId: 'czech_geo_component',
+ id: 'czech_geo_series',
+ geoIndex: 0,
+ data: [
+ {name: 'Praha', value: 23},
+ {name: 'Plzeňský kraj', value: 51},
+ {name: 'Středočeský kraj', value: 91}
+ ]
+ }, {
+ type: 'pie',
+ id: 'pie_a',
+ coordinateSystem: 'calendar',
+ calendarId: 'calendar1',
+ coord: '2017-02-11',
+ radius: '50%',
+ label: {overflow: 'none'},
+ labelLayout: {hideOverlap: false},
+ labelLine: {length: 2, length2: 2},
+ data: [
+ {name: 'a1', value: 123},
+ {name: 'no_center', value: 234},
+ {name: 'c1', value: 345},
+ ],
+ }, {
+ type: 'pie',
+ id: 'pie_b',
+ coordinateSystem: 'calendar',
+ calendarId: 'calendar1',
+ coord: '2017-02-12',
+ center: [15, 15],
+ radius: '20%',
+ label: {overflow: 'none'},
+ labelLayout: {hideOverlap: false},
+ labelLine: {length: 2, length2: 2},
+ data: [
+ {name: 'a2', value: 123},
+ {name: 'center_15', value: 234},
+ {name: 'c2', value: 345},
+ ],
+ }, {
+ type: 'pie',
+ id: 'pie_c',
+ coordinateSystem: 'calendar',
+ calendarId: 'calendar1',
+ coord: '2017-02-13',
+ radius: '50%',
+ label: {overflow: 'none'},
+ labelLayout: {hideOverlap: false},
+ labelLine: {length: 2, length2: 2},
+ data: [
+ {name: 'a', value: 123},
+ {name: 'b', value: 234},
+ {name: 'no_coord', value: 345},
+ ],
+ }, {
+ type: 'pie',
+ id: 'pie_d',
+ coordinateSystem: 'calendar',
+ calendarId: 'calendar1',
+ coord: '2017-02-14',
+ left: '50%',
+ radius: '50%',
+ label: {overflow: 'none'},
+ labelLayout: {hideOverlap: false},
+ labelLine: {length: 2, length2: 2},
+ data: [
+ {name: 'a', value: 123},
+ {name: 'b', value: 234},
+ {name: 'left:50%', value: 345},
+ ],
+ }, {
+ type: 'funnel',
+ id: 'funnel_a',
+ coordinateSystem: 'calendar',
+ calendarId: 'calendar1',
+ coord: '2017-02-15',
+ gap: 3,
+ left: 0,
+ right: 0,
+ top: 0,
+ bottom: 0,
+ label: {position: 'inside'},
+ sort: 'ascending',
+ data:[
+ {value:60, name:'a'},
+ {value:40, name:'b'},
+ {value:20, name:'c'},
+ {value:80, name:'d'},
+ ]
+ }, {
+ type: 'graph',
+ id: 'graph_a',
+ layout: 'none',
+ coordinateSystem: 'calendar',
+ coordinateSystemUsage: 'box',
+ calendarId: 'calendar1',
+ coord: '2017-02-16',
+ symbolSize: 15,
+ roam: true,
+ label: {show: true, fontSize: 10},
+ edgeSymbol: ['circle', 'arrow'],
+ edgeSymbolSize: [4, 7],
+ lineStyle: {opacity: 0.9, width: 2, curveness: 0},
+ data: [
+ {name: 'Node 0', x: 300, y: 300},
+ {name: 'Node 1', x: 800, y: 300},
+ {name: 'all in one cell', x: 550, y: 100},
+ {name: 'Node 3', x: 550, y: 500}
+ ],
+ links: [
+ {source: 0, target: 1},
+ {source: 1, target: 2},
+ {source: 0, target: 3}
+ ],
+ }, {
+ type: 'graph',
+ id: 'graph_b',
+ layout: 'none',
+ coordinateSystem: 'calendar',
+ calendarId: 'calendar1',
+ // no coord, but use coord sys by data.
+ symbolSize: 15,
+ roam: true,
+ label: {show: true, fontSize: 10},
+ edgeSymbol: ['circle', 'arrow'],
+ edgeSymbolSize: [4, 7],
+ lineStyle: {opacity: 0.9, width: 2, curveness: 0},
+ data: [
+ {name: 'Node 0', value: ['2017-02-17', 100]},
+ {name: 'Node 1', value: ['2017-02-18', 200]},
+ {name: 'cross cells', value: ['2017-02-19', 300]},
+ ],
+ links: [
+ {source: 0, target: 1},
+ {source: 1, target: 2},
+ {source: 0, target: 3}
+ ],
+ }] // End of series
+ };
+
+
+ var chart = testHelper.create(echarts, 'main0', {
+ title: [
+ 'Declaratively lay out other components and coord sys',
+ ],
+ option: option,
+ height: 550,
+ inputStyle: 'compact',
+ inputs: [{
+ text: 'border style:',
+ type: 'select',
+ options: [{
+ value: 'default',
+ }, {
+ text: 'thick width',
+ input: {type: 'range', value: 10, min: 0, max: 20}
+ }],
+ onchange() {
+ const opt = this.value;
+ let itemStyle;
+ let splitLineStyle;
+ if (opt === 'default') {
+ itemStyle = {
+ borderColor: '#ccc',
+ borderWidth: 1,
+ };
+ splitLineStyle = {
+ color: '#111',
+ };
+ }
+ else {
+ itemStyle = {
+ borderColor: '#ccc',
+ borderWidth: this.value
+ };
+ splitLineStyle = {
+ color: '#ccc',
+ };
+ }
+ chart.setOption({
+ calendar: {
+ id: 'calendar1',
+ itemStyle,
+ splitLine: {lineStyle: splitLineStyle}
+ }
+ });
+ }
+ }]
+ });
+
+ });
+ </script>
+
+
+
+
+
+
+
+
+
+ <script>
+ require(['echarts'], function (echarts) {
+ function renderItem(params, api) {
+ const timeVal = api.value(0);
+ const contentRect = api.layout(timeVal).contentRect;
+ const rect = api.layout(timeVal).rect;
+ const center = api.coord(timeVal);
+ return {
+ type: 'group',
+ children: [{
+ type: 'rect',
+ shape: contentRect,
+ style: api.style({
+ fill: 'none',
+ stroke: 'blue',
+ lineWidth: 2,
+ // opacity: 0.4,
+ }),
+ }, {
+ type: 'rect',
+ shape: rect,
+ style: api.style({
+ fill: 'none',
+ stroke: 'green',
+ lineWidth: 2,
+ // opacity: 0.4,
+ }),
+ }, {
+ type: 'rect',
+ shape: {x: center[0] - 5, y: center[1] - 5, width: 10, height: 10},
+ style: {
+ fill: 'green',
+ opacity: 0.8,
+ }
+ }]
+ };
+ };
+
+ var option = {
+ dataset: {
+ source: [
+ ['2017-02-01', 1212, 2321, 1412],
+ ['2017-02-02', 7181, 2114, 1402],
+ ['2017-02-03', 2763, 4212, 8172],
+ ['2017-02-04', 6122, 2942, 6121],
+ ['2017-02-05', 4221, 3411, 1987],
+ ['2017-02-06', 7221, 5121, 1303],
+ ]
+ },
+ calendar: {
+ id: 'calendar1',
+ orient: 'vertical',
+ left: 100,
+ right: 100,
+ top: 70,
+ bottom: 30,
+ itemStyle: {
+ borderWidth: 10,
+ },
+ yearLabel: {
+ show: false,
+ textStyle: {
+ fontSize: 30
+ }
+ },
+ dayLabel: {
+ margin: 20,
+ firstDay: 1,
+ nameMap: ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
+ },
+ monthLabel: {
+ show: false
+ },
+ range: ['2017-02']
+ },
+ series: [{
+ type: 'custom',
+ coordinateSystem: 'calendar',
+ encode: {time: 0},
+ renderItem: renderItem,
+ }],
+ };
+
+ var chart = testHelper.create(echarts, 'main_custom_series', {
+ title: [
+ 'custom series, test api.coord and api.layout'
+ ],
+ height: 350,
+ option: option
+ });
+ });
+ </script>
+
+
+
+ </body>
+</html>
diff --git a/test/matrix.html b/test/matrix.html
index 41b8f938a..20e744c2e 100644
--- a/test/matrix.html
+++ b/test/matrix.html
@@ -42,9 +42,9 @@ under the License.
<div id="main0-2"></div>
<div id="main1"></div>
<div id="main2"></div>
- <div id="main3"></div>
<div id="main4"></div>
-
+ <div id="main_collect"></div>
+ <div id="main_series_data_use_ordinal_number"></div>
@@ -53,8 +53,6 @@ under the License.
<script>
require([
'echarts',
- // 'map/js/china',
- // './data/nutrients.json'
], function (echarts) {
var option;
@@ -303,8 +301,6 @@ under the License.
<script>
require([
'echarts',
- // 'map/js/china',
- // './data/nutrients.json'
], function (echarts) {
var option;
option = {
@@ -320,6 +316,13 @@ under the License.
},
y: {
data: ['U', 'V']
+ },
+ body: {
+ data: [{
+ // coord: ['A31', 'V']
+ coord: ['A3', 'V'],
+ mergeCells: true,
+ }]
}
},
series: [{
@@ -359,74 +362,9 @@ under the License.
-
-
- <script>
- require([
- 'echarts',
- // 'map/js/china',
- // './data/nutrients.json'
- ], function (echarts) {
- var option;
- option = {
- matrix: {
- x: {
- data: [{
- value: 'A',
- children: ['A1', 'A2', {
- value: 'A3',
- children: ['A31', 'A32']
- }]
- }]
- },
- y: {
- data: ['U', 'V']
- }
- },
- series: [{
- type: 'scatter',
- coordinateSystem: 'matrix',
- symbolSize: 30,
- data: [
- ['A1', 'U', 10], ['A1', 'V', 20], ['A2', 'U', 30]
- ],
- label: {
- show: true,
- formatter: params => {
- return params.data[2];
- }
- }
- }, {
- type: 'effectScatter',
- coordinateSystem: 'matrix',
- symbolSize: function (val) {
- return val[2] / 3;
- },
- data: [
- ['A2', 'V', 40], ['A31', 'U', 50], ['A3', 'V', 60]
- ],
- }]
- };
-
- var chart = testHelper.create(echarts, 'main3', {
- title: [
- 'Scatter and effectScatter with Matrix'
- ],
- option: option
- });
- });
- </script>
-
-
-
-
-
-
<script>
require([
'echarts',
- // 'map/js/china',
- // './data/nutrients.json'
], function (echarts) {
var option;
option = {
@@ -459,22 +397,19 @@ under the License.
renderItem: function (params, api) {
const x = api.value(0);
const y = api.value(1);
- const center = api.coord([x, y]);
- const size = api.size([x, y]);
+ const rect = api.layout([x, y]).rect;
const fontSize = 20;
const padding = 10;
- const left = center[0] - size[0] / 2;
- const top = center[1] - size[1] / 2;
const value = api.value(2);
return {
type: 'group',
children: [{
type: 'rect',
shape: {
- x: left + padding,
- y: top + padding,
- width: (size[0] - padding * 2) / 100 * value,
- height: size[1] - padding * 2
+ x: rect.x + padding,
+ y: rect.y + padding,
+ width: (rect.width - padding * 2) / 100 * value,
+ height: rect.height - padding * 2
},
style: {
fill: value > 90 ? 'green'
@@ -484,8 +419,8 @@ under the License.
}, {
type: 'text',
style: {
- x: left + (size[0] - padding * 2) / 100 * value - padding,
- y: center[1] - fontSize / 2,
+ x: rect.x + (rect.width - padding * 2) / 100 * value - padding,
+ y: rect.y + rect.height / 2 - fontSize / 2,
text: api.value(3),
fontSize: fontSize,
textBaseline: 'middle',
@@ -494,8 +429,8 @@ under the License.
}, {
type: 'text',
style: {
- x: left + 30,
- y: center[1] - fontSize / 2,
+ x: rect.x + 30,
+ y: rect.y + rect.height / 2 - fontSize / 2,
text: api.value(2),
fontSize: fontSize,
textBaseline: 'middle',
@@ -516,6 +451,145 @@ under the License.
});
});
</script>
+
+
+
+
+ <script>
+ require(['echarts'], function (echarts) {
+ const makeRenderItem = (xDim, yDim, valDim, dataExtent) => (params, api) => {
+ const xval = api.value(xDim);
+ const yval = api.value(yDim);
+ const labelVal = api.value(valDim);
+ const rect = api.layout([xval, yval]).rect;
+ const height = rect.height * 0.3;
+ const y = rect.y + (rect.height - height) / 2;
+ const x = rect.x + rect.width * 0.3;
+ const widthMax = rect.width * 0.5;
+ const width = linearMap(labelVal, dataExtent, [0, widthMax])
+ return {
+ type: 'group',
+ children: [{
+ type: 'rect',
+ shape: {x, y, width, height},
+ style: api.style({
+ fill: '#47f',
+ }),
+ }, {
+ type: 'text',
+ x: rect.x + 15,
+ y: rect.y + rect.height / 2,
+ style: {
+ text: labelVal + '',
+ fill: '#333',
+ textVerticalAlign: 'middle',
+ },
+ }]
+ };
+ };
+
+ function linearMap(val, domain, range) {
+ const d0 = domain[0];
+ const d1 = domain[1];
+ const r0 = range[0];
+ const r1 = range[1];
+ const subDomain = d1 - d0;
+ const subRange = r1 - r0;
+
+ return subDomain === 0 ? subRange === 0 ? r0 : (r0 + r1) / 2
+ : val === d0 ? r0
+ : val === d1 ? r1
+ : (val - d0) / subDomain * subRange + r0;
+ }
+
+ const _dataExtent = [0, 10000];
+
+ var option = {
+ dataset: {
+ source: [
+ ['2021-02-01', 'amount', 1212, 'file', 2321, 'Q', 1412],
+ ['2021-02-02', 'amount', 7181, 'file', 2114, 'Q', 1402],
+ ['2021-02-03', 'amount', 2763, 'file', 4212, 'Q', 8172],
+ ['2021-02-04', 'amount', 6122, 'file', 2942, 'Q', 6121],
+ ['2021-02-05', 'amount', 4221, 'file', 3411, 'Q', 1987],
+ ['2021-02-06', 'amount', 7221, 'file', 5121, 'Q', 1303],
+ ]
+ },
+ matrix: {},
+ series: [{
+ type: 'custom',
+ coordinateSystem: 'matrix',
+ encode: {x: 1, y: 0},
+ renderItem: makeRenderItem(1, 0, 2, _dataExtent),
+ }, {
+ type: 'custom',
+ coordinateSystem: 'matrix',
+ symbol: 'rect',
+ encode: {x: 3, y: 0},
+ renderItem: makeRenderItem(3, 0, 4, _dataExtent),
+ }, {
+ type: 'custom',
+ coordinateSystem: 'matrix',
+ encode: {x: 5, y: 0},
+ renderItem: makeRenderItem(5, 0, 6, _dataExtent),
+ }],
+ };
+
+ var chart = testHelper.create(echarts, 'main_collect', {
+ title: [
+ 'No matrix.x.data, matrix.y.data declared, collect form dataset.'
+ ],
+ option: option
+ });
+ });
+ </script>
+
+
+
+
+
+
+ <script>
+ require([
+ 'echarts',
+ ], function (echarts) {
+
+ var option = {
+ matrix: {
+ x: {data: ['fruit', 'bread', 'milk']},
+ y: {data: ['good', 'medium', 'bad']},
+ },
+ series: {
+ type: 'scatter',
+ coordinateSystem: 'matrix',
+ label: {show: true},
+ encode: {label: 2},
+ symbolSize: 20,
+ data: [
+ [0, 0, 1223],
+ [1, 0, 323],
+ [2, 0, 142],
+ [0, 1, 63],
+ [1, 1, 91],
+ [2, 1, 45],
+ [0, 2, 55],
+ [1, 2, 15],
+ [2, 2, 53],
+ ]
+ }
+ };
+
+ var chart = testHelper.create(echarts, 'main_series_data_use_ordinal_number', {
+ title: [
+ 'series.data specify ordinal number rather than text'
+ ],
+ option: option
+ });
+ });
+ </script>
+
+
+
</body>
</html>
diff --git a/test/matrix2.html b/test/matrix2.html
new file mode 100644
index 000000000..eef040576
--- /dev/null
+++ b/test/matrix2.html
@@ -0,0 +1,1802 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+
+<html>
+ <head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <script src="lib/simpleRequire.js"></script>
+ <script src="lib/config.js"></script>
+ <script src="lib/jquery.min.js"></script>
+ <script src="lib/facePrint.js"></script>
+ <script src="lib/testHelper.js"></script>
+ <script src="lib/draggable.js"></script>
+ <!-- <script src="ut/lib/canteen.js"></script> -->
+ <link rel="stylesheet" href="lib/reset.css" />
+ </head>
+ <body>
+ <style>
+ </style>
+
+
+ <div id="main_layout"></div>
+ <div id="main_edge_cases"></div>
+ <div id="main_self_layout"></div>
+ <div id="main_dim_data_edge_cases"></div>
+
+
+ <script>
+ require([
+ 'echarts',
+ ], function (echarts) {
+
+ const MatrixClampOption = {
+ none: 0,
+ all: 1,
+ body: 2,
+ corner: 3,
+ };
+
+ var LEVEL_SIZE_1 = 15;
+ var LEVEL_SIZE_2 = 20;
+ var LEVEL_SIZE_3 = '10%';
+ var LEVEL_SIZE_4 = '0%';
+ var CELL_SIZE_1 = 30;
+ var CELL_SIZE_2 = 0;
+ var BODY_COMMON_COLOR = 'rgba(200, 150, 20, 0.1)';
+ var BODY_R2_A2_COLOR = 'rgba(200, 155, 0)';
+ var BODY_R1_B2_COLOR = 'rgba(200, 115, 200, 0.3)';
+ var X_COMMON_COLOR = 'rgba(22, 188, 212, 0.1)';
+ var X_T1_COLOR = 'rgba(12,122,250,0.8)';
+
+ var bodyData = [
+ {
+ coord: ['R2', 'A3'],
+ mergeCells: true,
+ },
+ {
+ coord: [['R1', 'S'], 'A'],
+ itemStyle: {
+ color: 'rgb(220,240,130)',
+ }
+ },
+ {
+ coord: ['R1', ['B4', 'C']],
+ mergeCells: true,
+ },
+ {
+ coord: [['R1', 'R2'], ['B2', 'B3']],
+ itemStyle: {
+ borderWidth: 4,
+ borderColor: 'orange',
+ color: 'red',
+ },
+ mergeCells: true,
+ },
+ {
+ coord: ['R2', 'A2'],
+ itemStyle: {
+ borderWidth: 4,
+ borderColor: 'blue',
+ color: BODY_R2_A2_COLOR,
+ },
+ z2: 200,
+ },
+ {
+ coord: [['R2', 'T'], 'B2'], // Overlap span, should be ignored.
+ mergeCells: true,
+ itemStyle: {
+ borderWidth: 4,
+ borderColor: 'rgb(200, 152, 222)',
+ color: BODY_R1_B2_COLOR,
+ },
+ _desc: 'expand to cover all merged cells'
+ },
+ {
+ coord: ['T', ['A1', 'A31']], // Overlap span, should be ignored.
+ mergeCells: true,
+ itemStyle: {
+ borderColor: 'rgb(123,134,10)',
+ borderWidth: 3,
+ color: 'rgb(220,230,170)',
+ },
+ z2: 200,
+ },
+ {
+ coord: ['R2', 'E'],
+ value: 'very long long long text',
+ label: {
+ color: 'red',
+ }
+ },
+ {
+ coord: ['U', null],
+ itemStyle: {
+ color: 'rgb(220,230,170)',
+ },
+ },
+ {
+ coord: [NaN, 'F'],
+ mergeCells: true,
+ }
+ ];
+
+ var cornerData = [
+ {
+ coord: [null, -2],
+ value: 'some',
+ mergeCells: true,
+ }
+ ]
+
+ const MATRIX_BACKGROUND_STYLE_MAP = {
+ do_nothing: {
+ backgroundStyle: {},
+ x: {
+ itemStyle: {},
+ },
+ },
+ style1: {
+ backgroundStyle: {
+ borderWidth: 5,
+ },
+ x: {
+ itemStyle: {
+ borderWidth: 2,
+ color: 'red',
+ borderColor: 'rgba(11,222,33)'
+ }
+ }
+ },
+ style2: {
+ backgroundStyle: {
+ borderWidth: 1,
+ },
+ x: {
+ itemStyle: {
+ borderWidth: 1,
+ color: 'none',
+ borderColor: 'blue'
+ }
+ }
+ },
+ };
+
+ // --- state ------
+ let _xShowing = true;
+ let _yShowing = true;
+ let _convertFromPixelClampOpt = undefined;
+ let _convertToPointCaseItem = null;
+ let _matrixBgStyleIndex = 0;
+ let _textOverflow = 'truncate'; // internal default value.
+ // ----------------
+
+ var option;
+ option = {
+ backgroundColor: '#eee',
+ tooltip: {},
+ title: {
+ text: '',
+ right: 'center',
+ },
+ matrix: {
+ left: 120,
+ right: '29%',
+ tooltip: {
+ show: true,
+ formatter(params) {
+ return params.name;
+ }
+ },
+ x: {
+ label: {
+ },
+ itemStyle: {
+ color: X_COMMON_COLOR
+ },
+ levels: [null, {levelSize: LEVEL_SIZE_1}],
+ data: [
+ {
+ value: 'R',
+ children: ['R1', {value: 'R2', size: CELL_SIZE_1}]
+ },
+ {
+ value: 'S'
+ },
+ {
+ value: 'T',
+ children: [
+ {
+ value: 'T1',
+ itemStyle: {
+ borderWidth: 3,
+ borderColor: 'rgba(12,12,190)',
+ color: X_T1_COLOR,
+ },
+ label: {
+ fontSize: 22,
+ color: '#fff',
+ textBorderColor: '#333',
+ textBorderWidth: 2,
+ },
+ children: ['T11']
+ }
+ ]
+ },
+ {
+ value: 'long long header text 1',
+ label: {
+ color: 'rgb(211,111,0)',
+ },
+ children: [{
+ value: 'long long header text 2',
+ label: {
+ color: 'rgb(211,81,0)',
+ }
+ }]
+ },
+ {
+ value: 'U',
+ size: 20,
+ },
+ {
+ value: 'V',
+ size: 20,
+ }
+ ]
+ },
+ y: {
+ levelSize: LEVEL_SIZE_2,
+ levels: [
+ null,
+ {levelSize: LEVEL_SIZE_4},
+ null,
+ null,
+ {levelSize: LEVEL_SIZE_3}
+ ],
+ data: [{
+ value: 'LLL',
+ children: [{
+ value: 'LL',
+ children: [
+ {
+ value: 'A',
+ children: ['A1', 'A2', {
+ value: 'A3',
+ children: ['A31', 'A32']
+ }]
+ }, {
+ value: 'B',
+ children: ['B1', 'B2', 'B3', 'B4']
+ }, {
+ value: 'C',
+ }, {
+ value: 'D',
+ size: CELL_SIZE_2,
+ }
+ ]
+ }, {
+ value: 'E',
+ }, {
+ value: 'F',
+ itemStyle: {
+ color: '#bbb'
+ }
+ }]
+ }]
+ },
+ body: {
+ data: bodyData,
+ label: {
+ },
+ itemStyle: {
+ color: BODY_COMMON_COLOR
+ }
+ },
+ corner: {
+ itemStyle: {
+ borderWidth: 1,
+ },
+ data: cornerData,
+ }
+ },
+ graphic: {
+ elements: [{
+ right: 5,
+ y: 5,
+ id: 'info_bodyData',
+ type: 'text',
+ style: {
+ text: 'Check body cell merge and style:\n' + testHelper.printObject(bodyData, {lineBreakMaxColumn: 40}),
+ fontSize: 7,
+ }
+ }, {
+ left: 5,
+ y: 5,
+ id: 'info_cornerData',
+ type: 'text',
+ style: {
+ text: 'Check corner cell merge and style:\n' + testHelper.printObject(cornerData, {lineBreakMaxColumn: 40}),
+ fontSize: 7,
+ }
+ }, {
+ x: 120,
+ y: 5,
+ id: 'info2',
+ type: 'text',
+ style: {
+ text: [
+ 'Check background: ',
+ ` BODY_COMMON_COLOR = ${BODY_COMMON_COLOR}`,
+ ` BODY_R2_A2_COLOR = ${BODY_R2_A2_COLOR}`,
+ ` BODY_R1_B2_COLOR = ${BODY_R1_B2_COLOR}`,
+ ` X_COMMON_COLOR = ${X_COMMON_COLOR}`,
+ ` X_T1_COLOR = ${X_T1_COLOR}`,
+ ].join('\n'),
+ fontSize: 7,
+ }
+ }]
+ },
+ grid: [{
+ id: 'aa',
+ coordinateSystem: 'matrix',
+ coord: ['R2', 'B3'],
+ top: 5,
+ left: 5,
+ right: 5,
+ bottom: 5,
+ containLabel: true,
+ }, {
+ id: 'bb',
+ coordinateSystem: 'matrix',
+ coord: ['R1', 'B4'],
+ top: 5,
+ left: 5,
+ right: 5,
+ bottom: 5,
+ containLabel: true,
+ }],
+ xAxis: [{
+ id: 'aa',
+ gridId: 'aa',
+ axisLabel: {fontSize: 9}
+ }, {
+ id: 'bb',
+ gridId: 'bb',
+ data: ['bb1', 'bb2', 'bb3'],
+ axisLabel: {fontSize: 9}
+ }],
+ yAxis: [{
+ id: 'aa',
+ gridId: 'aa',
+ interval: Infinity,
+ axisLabel: {fontSize: 9, showMinLabel: true, showMaxLabel: true,},
+ }, {
+ id: 'bb',
+ gridId: 'bb',
+ interval: Infinity,
+ axisLabel: {fontSize: 9, showMinLabel: true, showMaxLabel: true,},
+ }],
+ series: [{
+ type: 'scatter',
+ coordinateSystem: 'matrix',
+ symbolSize: 30,
+ data: [
+ ['R1', 'A1', 10], ['R2', 'A1', 20], ['R1', 'A2', 30]
+ ],
+ label: {
+ show: true,
+ formatter: params => {
+ return params.data[2];
+ }
+ }
+ }, {
+ // type: 'effectScatter',
+ type: 'scatter',
+ coordinateSystem: 'matrix',
+ symbolSize: function (val) {
+ return val[2] / 3;
+ },
+ data: [
+ ['R2', 'A2', 40], ['R1', 'A31', 50], ['R2', 'A3', 60]
+ ],
+ }, {
+ type: 'scatter',
+ name: 'right desc',
+ silent: true,
+ coordinateSystem: 'matrix',
+ symbolSize: 0,
+ symbolOffset: [0, 10],
+ label: {show: true, overflow: 'break', minMargin: 15},
+ labelLine: {show: true},
+ labelLayout: {
+ x: '73%',
+ moveOverlap: 'shiftY',
+ width: 70,
+ },
+ encode: {label: [0, 1, 2]},
+ data: [
+ ['T1', -2, `x.levels[3].levelSize: ${LEVEL_SIZE_1}`],
+ ['T11', -1, 'levelSize: average (not specified)'],
+ ['R2', -1, `size: ${CELL_SIZE_1}`],
+ ]
+ }, {
+ type: 'scatter',
+ name: 'left desc',
+ silent: true,
+ coordinateSystem: 'matrix',
+ symbolSize: 0,
+ symbolOffset: [0, 10],
+ label: {show: true, overflow: 'break', minMargin: 15},
+ labelLine: {show: true},
+ labelLayout: {x: '1%', width: 110, moveOverlap: 'shiftY'},
+ encode: {label: [0, 1, 2]},
+ data: [
+ [-3, 'A', `y.levelSize: ${LEVEL_SIZE_2}`],
+ [-2, 'A3', `y.levelSize: ${LEVEL_SIZE_2}`],
+ [-1, 'A31', `y.levels[4].levelSize: ${LEVEL_SIZE_3}`],
+ [-1, 'D', `size: ${CELL_SIZE_2}`],
+ [-4, 'E', `y.levels[2].levelSize: ${LEVEL_SIZE_4}`]
+ ]
+ },
+ {
+ type: 'line',
+ xAxisId: 'aa',
+ yAxisId: 'aa',
+ data: [[11, 22], [33, 44], [55, 66]],
+ }, {
+ type: 'bar',
+ xAxisId: 'bb',
+ yAxisId: 'bb',
+ data: [987, 583, 671],
+ }]
+ };
+
+ var chart = testHelper.create(echarts, 'main_layout', {
+ title: [
+ 'Locator testing.',
+ 'scatter, cartesian, with Matrix.',
+ 'cell merging: should display **on the center** of the merged cell.',
+ 'click on cell to call convertFromPixel - especially on span cell.',
+ 'click on corner should correct.',
+ 'tooltip can be displayed for text (consider overflow).',
+ ],
+ height: 500,
+ option: option,
+ inputsStyle: 'compact',
+ inputs: [
+ {
+ text: 'matrix.x show:',
+ type: 'select',
+ values: [true, false],
+ onchange() {
+ _xShowing = this.value;
+ chart.setOption({
+ matrix: {
+ x: {show: _xShowing},
+ },
+ });
+ updateCaseView();
+ }
+ },
+ {
+ text: 'matrix.y show:',
+ type: 'select',
+ values: [true, false],
+ onchange() {
+ _yShowing = this.value;
+ chart.setOption({
+ matrix: {
+ y: {show: _yShowing},
+ },
+ });
+ updateCaseView();
+ }
+ },
+ {
+ text: 'convertFromPixel clamp:',
+ type: 'select',
+ options: [
+ {value: undefined}
+ ].concat(Object.keys(MatrixClampOption).map(key => ({
+ text: key,
+ value: MatrixClampOption[key],
+ }))),
+ onchange() {
+ _convertFromPixelClampOpt = this.value;
+ updateCaseView();
+ },
+ },
+ {
+ type: 'select',
+ text: 'text overflow:',
+ values: ['truncate', 'break', 'none'],
+ onchange() {
+ _textOverflow = this.value;
+ chart.setOption({
+ matrix: {
+ x: {label: {overflow: _textOverflow}},
+ y: {label: {overflow: _textOverflow}},
+ body: {label: {overflow: _textOverflow}},
+ corner: {label: {overflow: _textOverflow}},
+ },
+ });
+ updateCaseView();
+ }
+ },
+ {
+ type: 'select',
+ text: 'switch style:',
+ values: Object.keys(MATRIX_BACKGROUND_STYLE_MAP),
+ onchange() {
+ const preset = MATRIX_BACKGROUND_STYLE_MAP[this.value];
+ chart.setOption({
+ matrix: {
+ backgroundStyle: preset.backgroundStyle,
+ x: {
+ itemStyle: preset.x.itemStyle,
+ }
+ }
+ });
+ updateCaseView();
+ }
+ },
+ {
+ type: 'select',
+ text: 'convertToPixel test cases:',
+ values: [
+ 'do_nothing',
+ ['R2', 'A31'],
+ ['not_exists', 'A1'],
+ ['S', Infinity],
+ [1, 3],
+ ['R1', 'A'],
+ ['R1', 'A2'],
+ ['R1', 'B4'],
+ ['R1', 'B2'],
+ ['R1', 'C'],
+ ['R2', 'B3'],
+ ],
+ onchange() {
+ const caseItem = this.value;
+ if (caseItem === 'do_nothing') {
+ return;
+ }
+ _convertToPointCaseItem = this.value;
+ updateCaseView();
+ }
+ },
+ ]
+ });
+
+ function updateCaseView() {
+ if (!_convertToPointCaseItem) {
+ return;
+ }
+ const result = chart.convertToPixel({matrixIndex: 0}, _convertToPointCaseItem);
+ updateConvertToPixelResult(result);
+ }
+
+ function updateConvertToPixelResult(result) {
+ console.log(result);
+ chart.setOption({
+ title: {
+ text: 'convertToPixel result: ' + testHelper.printObject(result)
+ },
+ graphic: {
+ elements: [{
+ id: 'result',
+ type: 'circle',
+ shape: {
+ cx: result[0],
+ cy: result[1],
+ r: 20,
+ },
+ ignore: isNaN(result[0]) || isNaN(result[1]),
+ z: 1000,
+ style: {
+ stroke: 'red',
+ fill: null,
+ lineWidth: 5,
+ opacity: 0.8,
+ },
+ textContent: {
+ style: {
+ text: '⬅ convertToPixel result',
+ fill: 'red',
+ },
+ z: 1000,
+ },
+ textConfig: {
+ position: 'right'
+ },
+ }]
+ }
+ });
+ }
+
+ if (chart) {
+ chart.getZr().on('click', function (event) {
+ const point = [event.offsetX, event.offsetY];
+ let opt = null;
+ if (_convertFromPixelClampOpt != null) {
+ opt = {clamp: _convertFromPixelClampOpt};
+ }
+ const result = chart.convertFromPixel({matrixIndex: 0}, point, opt);
+ console.log('point', point, 'result', result);
+ chart.setOption({
+ title: {
+ text: 'convertFromPixel result: ' + testHelper.printObject(result)
+ }
+ });
+ });
+ }
+
+ });
+
+ </script>
+
+
+
+
+
+
+
+ <script>
+ require([
+ 'echarts',
+ ], function (echarts) {
+
+ var LEVEL_SIZE_1 = 15;
+ var LEVEL_SIZE_2 = 20;
+ var LEVEL_SIZE_3 = '10%';
+ var LEVEL_SIZE_4 = '0%';
+ var CELL_SIZE_1 = 30;
+ var CELL_SIZE_2 = 0;
+ var BODY_COMMON_COLOR = 'rgba(200, 150, 20, 0.1)';
+ var BODY_R2_A2_COLOR = 'rgba(200, 155, 0)';
+ var BODY_R1_B2_COLOR = 'rgba(200, 115, 200, 0.3)';
+ var X_COMMON_COLOR = 'rgba(22, 188, 212, 0.1)';
+ var X_T1_COLOR = 'rgba(12,122,250,0.8)';
+
+ var _bodyData = [
+ {
+ coord: ['R2', 'A3'],
+ mergeCells: true,
+ value: 'merged',
+ },
+ {
+ coord: ['R1', ['B4', 'C']],
+ mergeCells: true,
+ value: 'merged',
+ },
+ {
+ coord: [['R1', 'R2'], ['B2', 'B3']],
+ mergeCells: true,
+ value: 'merged',
+ },
+ {
+ coord: ['O', ['B2', 'not_exist']],
+ mergeCells: true,
+ value: 'merged',
+ __expect: 'column O merged'
+ },
+ {
+ coord: ['T', ['A1', 'A31']],
+ mergeCells: true,
+ value: 'merged',
+ },
+ {
+ coord: ['S', null],
+ mergeCells: true,
+ value: 'merged',
+ __expect: 'column S merged.'
+ },
+ ];
+ var _cornerData = [
+ {
+ coord: [[-1, -2], -1],
+ mergeCells: true,
+ value: 'merged',
+ }
+ ];
+
+ const _xData = [
+ {
+ value: 'R',
+ children: ['R1', {value: 'R2', size: CELL_SIZE_1}]
+ },
+ {
+ value: 'S'
+ },
+ {
+ value: 'T',
+ children: [
+ {
+ value: 'T1',
+ children: ['T11'],
+ }
+ ]
+ },
+ 'O',
+ 'P',
+ ];
+ const _yData = [
+ {
+ value: 'A',
+ children: ['A1', 'A2', {
+ value: 'A3',
+ children: ['A31', 'A32']
+ }]
+ }, {
+ value: 'B',
+ children: ['B1', 'B2', 'B3', 'B4']
+ }, {
+ value: 'C',
+ }, {
+ value: 'D',
+ }
+ ];
+ const MatrixClampOption = {
+ none: 0,
+ all: 1,
+ body: 2,
+ corner: 3,
+ };
+
+ // --- state ------
+ let _xShowing = true;
+ let _yShowing = true;
+ let _convertToLayoutClampOpt = undefined;
+ let _ignoreMergeCells = undefined;
+ let _testCase = null;
+ // ----------------
+
+ function calcMeta() {
+ const xMeta = calcOneDim(_xData);
+ const yMeta = calcOneDim(_yData);
+ function calcOneDim(xyData) {
+ let levelCount = 0;
+ let leafCount = 0;
+ travel(xyData, 0, 0);
+ function travel(list, locatorStart, level) {
+ levelCount = Math.max(levelCount, level);
+ leafCount = Math.max(leafCount, locatorStart);
+ if (!list) {
+ return;
+ }
+ list.forEach((item, idx) => {
+ travel(item.children, locatorStart + idx, level + 1);
+ });
+ }
+ return {levelCount, leafCount};
+ }
+ const xUnitCount = xMeta.leafCount + (_yShowing ? yMeta.levelCount : 0);
+ const yUnitCount = yMeta.leafCount + (_xShowing ? xMeta.levelCount : 0);
+ return {xUnitCount, yUnitCount};
+ }
+
+ var option;
+ option = {
+ backgroundColor: '#eee',
+ tooltip: {},
+ title: {
+ text: '',
+ right: 'center',
+ },
+ matrix: [{
+ // matrix 0
+ right: 10,
+ bottom: 10,
+ width: 180,
+ height: 80,
+ x: {
+ data: ['X1', 'X2', 'X3'],
+ },
+ y: {
+ data: ['Y1', 'Y2', 'Y3', 'Y4'],
+ },
+ body: {
+ data: [{
+ coord: [[0, 2], [1, 2]],
+ value: 'should be right-bottom',
+ mergeCells: true,
+ }]
+ },
+ }, {
+ // matrix 1
+ id: 'main_matrix',
+ right: 10,
+ left: 230,
+ top: 50,
+ bottom: 120,
+ tooltip: {
+ show: true,
+ formatter(params) {
+ return params.name;
+ }
+ },
+ x: {
+ label: {
+ },
+ itemStyle: {
+ color: X_COMMON_COLOR
+ },
+ levels: [null, {levelSize: LEVEL_SIZE_1}],
+ data: _xData
+ },
+ y: {
+ levelSize: 40,
+ data: _yData,
+ },
+ body: {
+ data: _bodyData,
+ label: {
+ },
+ itemStyle: {
+ color: BODY_COMMON_COLOR
+ }
+ },
+ corner: {
+ data: _cornerData,
+ itemStyle: {
+ borderWidth: 1,
+ },
+ }
+ }],
+ series: [{
+ type: 'scatter',
+ coordinateSystem: 'matrix',
+ matrixIndex: 1,
+ symbolSize: 30,
+ data: [
+ ['R1', 'A1', 10], ['R2', 'A1', 20], ['R1', 'A2', 30]
+ ],
+ label: {
+ show: true,
+ formatter: params => {
+ return params.data[2];
+ }
+ }
+ }],
+ graphic: {
+ elements: [{
+ left: 5,
+ y: 5,
+ id: 'info_bodyData',
+ type: 'text',
+ style: {
+ text: 'Check body cell merge and style:\n' + testHelper.printObject(_bodyData, {lineBreakMaxColumn: 40}),
+ fontSize: 7,
+ }
+ }, {
+ left: 140,
+ y: 5,
+ id: 'info_cornerData',
+ type: 'text',
+ style: {
+ text: 'Check corner cell merge and style:\n' + testHelper.printObject(_cornerData, {lineBreakMaxColumn: 40}),
+ fontSize: 7,
+ }
+ }, {
+ id: 'result_convertToLayout',
+ type: 'rect',
+ shape: {x: 0, y: 0, width: 0, height: 0},
+ ignore: true,
+ silent: true,
+ style: {
+ fill: 'rgba(210,20,100,0.3)',
+ stroke: 'rgba(210,20,100,0.8)',
+ lineWidth: 3,
+ }
+ }, {
+ id: 'result_assertionFailure',
+ type: 'text',
+ ignore: true,
+ silent: true,
+ left: 10,
+ bottom: 10,
+ style: {
+ text: 'Some Case Failed',
+ fill: 'red',
+ fontSize: 20,
+ }
+ }]
+ },
+ };
+
+ var chart = testHelper.create(echarts, 'main_edge_cases', {
+ title: [
+ 'Edge cases.',
+ 'Test both when **show/hide** matrix.x/y; different **clamp**; **ignoreCellMerge** or not.',
+ 'drag to resize to test cell border alignment',
+ '(must not affect by subpixel optimize or rounding error)',
+ ],
+ height: 350,
+ option: option,
+ inputsStyle: 'compact',
+ draggable: true,
+ inputs: [
+ {
+ text: 'matrix.x show:',
+ type: 'select',
+ values: [true, false],
+ onchange() {
+ _xShowing = this.value;
+ chart.setOption({
+ matrix: {
+ id: 'main_matrix',
+ x: {show: _xShowing},
+ },
+ });
+ updateCaseView();
+ }
+ },
+ {
+ text: 'matrix.y show:',
+ type: 'select',
+ values: [true, false],
+ onchange() {
+ _yShowing = this.value;
+ chart.setOption({
+ matrix: {
+ id: 'main_matrix',
+ y: {show: _yShowing},
+ },
+ });
+ updateCaseView();
+ }
+ },
+ {
+ text: 'convertToLayout clamp:',
+ type: 'select',
+ options: [
+ {value: undefined}
+ ].concat(Object.keys(MatrixClampOption).map(key => ({
+ text: key,
+ value: MatrixClampOption[key],
+ }))),
+ onchange() {
+ _convertToLayoutClampOpt = this.value;
+ updateCaseView();
+ },
+ },
+ {
+ text: 'ignoreMergeCells:',
+ type: 'select',
+ values: [undefined, false, true],
+ onchange() {
+ _ignoreMergeCells = this.value;
+ updateCaseView();
+ },
+ },
+ {
+ type: 'br',
+ },
+ {
+ type: 'select',
+ text: 'convertToLayout test cases:',
+ options: [{value: 'do_nothing'}].concat([
+ {
+ input: [],
+ expectDefault: {
+ rectNaN: {x: true, y: true, width: true, height: true},
+ matrixXYLocatorRange: [[NaN, NaN], [NaN, NaN]]
+ }
+ },
+ {
+ input: ['R2', null],
+ expectDefault: {
+ rectNaN: {y: true, height: true},
+ matrixXYLocatorRange: [[1, 1], [NaN, NaN]]
+ }
+ },
+ {
+ input: [['S', 'O'], NaN],
+ expectDefault: {
+ rectNaN: {y: true, height: true},
+ matrixXYLocatorRange: [[2, 4], [NaN, NaN]]
+ }
+ },
+ {
+ input: [['R'], NaN],
+ expectDefault: {
+ rectNaN: {y: true, height: true},
+ matrixXYLocatorRange: [[0, 1], [NaN, NaN]]
+ }
+ },
+ {
+ input: [NaN, 'A31'],
+ expectDefault: {
+ rectNaN: {x: true, width: true},
+ matrixXYLocatorRange: [[NaN, NaN], [2, 2]]
+ }
+ },
+ {
+ input: [undefined, ['A32', 'B3']],
+ expectDefault: {
+ rectNaN: {x: true, width: true},
+ matrixXYLocatorRange: [[NaN, NaN], [3, 6]]
+ }
+ },
+ {
+ input: [null, 'B'],
+ expectDefault: {
+ rectNaN: {x: true, width: true},
+ matrixXYLocatorRange: [[NaN, NaN], [4, 7]]
+ }
+ },
+ {
+ input: ['S', 'A32'],
+ expectDefault: {
+ rectNaN: {},
+ matrixXYLocatorRange: [[2, 2], [0, 9]]
+ }
+ },
+ {
+ input: ['T', 'B1'],
+ expectDefault: {
+ rectNaN: {},
+ matrixXYLocatorRange: [[3, 3], [4, 4]]
+ }
+ },
+ {
+ input: [[-1, 1], [-1, 2]],
+ expectDefault: {
+ rectNaN: {},
+ matrixXYLocatorRange: [[-2, 1], [-1, 3]]
+ }
+ },
+ {
+ input: ['R', -2],
+ expectDefault: {
+ rectNaN: {},
+ matrixXYLocatorRange: [[0, 1], [-2, -2]]
+ }
+ },
+ ].map(item => ({
+ text: testHelper.printObject(item.input),
+ value: item,
+ }))),
+ onchange() {
+ if (this.value === 'do_nothing') {
+ return;
+ }
+ _testCase = this.value;
+ updateCaseView();
+ } // End of onchange
+ } // End of convertToLayout test cases
+ ]
+ });
+
+ function updateCaseView() {
+ performTest(function (assert) {
+ if (!_testCase) {
+ return;
+ }
+ const {input, expectDefault} = _testCase;
+ const isDefaultCase =
+ (_convertToLayoutClampOpt == null || _convertToLayoutClampOpt === MatrixClampOption.none)
+ && !_ignoreMergeCells;
+
+ const meta = calcMeta();
+ let opt = undefined;
+ if (_convertToLayoutClampOpt != null) {
+ opt = opt || {};
+ opt.clamp = _convertToLayoutClampOpt;
+ }
+ if (_ignoreMergeCells != null) {
+ opt = opt || {};
+ opt.ignoreMergeCells = _ignoreMergeCells;
+ }
+
+ const result = chart.convertToLayout({matrixIndex: 1}, input, opt);
+ console.log('Call convertToLayout', 'input:', input, 'opt:', opt, 'result:', result);
+
+ assert(testHelper.getType(result) === 'object');
+ assert(testHelper.getType(result.rect) === 'object');
+ assert(testHelper.getType(result.rect.x) === 'number');
+ assert(testHelper.getType(result.rect.y) === 'number');
+ assert(testHelper.getType(result.rect.width) === 'number');
+ assert(testHelper.getType(result.rect.height) === 'number');
+ assert(testHelper.getType(result.matrixXYLocatorRange) === 'array');
+ assert(testHelper.getType(result.matrixXYLocatorRange[0]) === 'array');
+ assert(testHelper.getType(result.matrixXYLocatorRange[1]) === 'array');
+ assert(testHelper.getType(result.matrixXYLocatorRange[0][0]) === 'number');
+ assert(testHelper.getType(result.matrixXYLocatorRange[0][1]) === 'number');
+ assert(testHelper.getType(result.matrixXYLocatorRange[1][0]) === 'number');
+ assert(testHelper.getType(result.matrixXYLocatorRange[1][1]) === 'number');
+
+ ['x', 'y', 'width', 'height'].forEach(prop => {
+ if (isDefaultCase) {
+ if (expectDefault.rectNaN[prop]) {
+ assert(eqNaN(result.rect[prop]));
+ }
+ else {
+ assert(isFinite(result.rect[prop]));
+ }
+ }
+ else {
+ assert(isFinite(result.rect[prop]));
+ }
+ });
+
+ if (isDefaultCase) {
+ assert(numEq(expectDefault.matrixXYLocatorRange[0][0], result.matrixXYLocatorRange[0][0]));
+ assert(numEq(expectDefault.matrixXYLocatorRange[0][1], result.matrixXYLocatorRange[0][1]));
+ assert(numEq(expectDefault.matrixXYLocatorRange[1][0], result.matrixXYLocatorRange[1][0]));
+ assert(numEq(expectDefault.matrixXYLocatorRange[1][1], result.matrixXYLocatorRange[1][1]));
+ }
+
+ return result;
+ });
+
+ function performTest(cb) {
+ let testFailed = false;
+ function assert(condition) {
+ if (!condition) {
+ testFailed = true;
+ console.error('assertion failed', new Error());
+ }
+ }
+
+ let result;
+ try {
+ result = cb(assert);
+ }
+ catch (err) {
+ testFailed = true;
+ console.error(err);
+ }
+
+ const COVER_SIZE = 10000; // Just a big enough number.
+ let indicatorShape = {x: 0, y: 0, width: 0, height: 0};
+ if (result) {
+ indicatorShape = {};
+ indicatorShape.x = eqNaN(result.rect.x) ? -COVER_SIZE / 2 : result.rect.x;
+ indicatorShape.y = eqNaN(result.rect.y) ? -COVER_SIZE / 2 : result.rect.y;
+ indicatorShape.width = eqNaN(result.rect.width) ? COVER_SIZE : result.rect.width;
+ indicatorShape.height = eqNaN(result.rect.height) ? COVER_SIZE : result.rect.height;;
+ }
+
+ chart.setOption({
+ graphic: {
+ elements: [{
+ id: 'result_convertToLayout',
+ shape: indicatorShape,
+ ignore: testFailed,
+ }, {
+ id: 'result_assertionFailure',
+ ignore: !testFailed,
+ }]
+ },
+ });
+ }
+ }
+
+ function eqNaN(value) {
+ return value !== value;
+ }
+
+ function numEq(expect, actual) {
+ return eqNaN(expect)
+ ? eqNaN(actual)
+ : expect === actual;
+ }
+
+ });
+
+ </script>
+
+
+
+
+
+
+
+
+
+ <script>
+
+ var option;
+
+ require([
+ 'echarts'
+ ], function (echarts) {
+
+ const layoutList = [
+ {
+ layout: {
+ left: 10,
+ bottom: 10,
+ width: 250,
+ height: 50,
+ },
+ expect: 'left-bottom',
+ },
+ {
+ layout: {
+ right: 10,
+ bottom: 10,
+ width: 250,
+ height: 50,
+ },
+ expect: 'right-bottom'
+ },
+ {
+ layout: {
+ },
+ expect: 'default place'
+ },
+ ];
+
+ option = {
+ backgroundColor: '#eee',
+ matrix: [
+
+ // Layout test (r/l/t/b/w/h)
+ ...layoutList.map(item => ({
+ ...item.layout,
+ backgroundStyle: {
+ color: '#ddd'
+ },
+ x: {show: false, data: ['X1']},
+ y: {show: false, data: ['Y1']},
+ body: {
+ label: {fontSize: 10},
+ data: [{
+ coord: [0, 0],
+ value: `expect matrix on ${item.expect} due to\nl/r/t/b/w/h settings are:\n`
+ + JSON.stringify(item.layout)
+ }]
+ }
+ })),
+
+ // z-order test
+ ...[{
+ matrixLayout: {
+ top: 10,
+ left: 10,
+ width: 300,
+ height: 50,
+ },
+ borderZ2: undefined,
+ }, {
+ matrixLayout: {
+ top: 'center',
+ left: 10,
+ width: 300,
+ height: 50,
+ },
+ borderZ2: 500,
+ }].map(({matrixLayout, borderZ2}) => ({
+ ...matrixLayout,
+ backgroundStyle: {
+ color: 'yellow',
+ borderColor: 'orange',
+ borderWidth: 5,
+ shadowColor: '#111',
+ shadowBlur: 15,
+ },
+ borderZ2,
+ x: {
+ data: ['X1', 'X2', 'X3'],
+ dividerLineStyle: {
+ color: 'rgb(0,200,200)',
+ width: 5,
+ },
+ },
+ y: {
+ data: ['Y1', 'Y2', 'Y3'],
+ dividerLineStyle: {
+ color: 'rgb(100,150,150)',
+ width: 5,
+ },
+ },
+ body: {
+ label: {
+ fontSize: 10,
+ },
+ itemStyle: {
+ borderWidth: 1,
+ borderColor: 'blue'
+ },
+ data: [{
+ coord: [0, 0],
+ itemStyle: {
+ borderColor: 'red',
+ borderWidth: 3
+ },
+ value: 'red border',
+ }, {
+ coord: [[1, 2], 0],
+ itemStyle: {
+ borderColor: 'green',
+ borderWidth: 3
+ },
+ mergeCells: true,
+ value: 'green border',
+ }, {
+ coord: [[1, 2], 2],
+ mergeCells: true,
+ label: {
+ position: 'insideRight'
+ },
+ value: `borderZ2: ${borderZ2}`,
+ }]
+ },
+ z: 10
+ }))
+ ]
+ };
+
+ var chart = testHelper.create(echarts, 'main_self_layout', {
+ height: 200,
+ title: [
+ 'matrix self layout test',
+ 'z-index test'
+ ],
+ option: option,
+ });
+ });
+
+ </script>
+
+
+
+
+
+ <script>
+ require([
+ 'echarts',
+ ], function (echarts) {
+ const MatrixClampOption = {
+ none: 0,
+ all: 1,
+ body: 2,
+ corner: 3,
+ };
+
+ function makeMatrixOption({x, y, xLevels, yLevels}, {xShow, yShow}) {
+ return {
+ top: 90,
+ bottom: 100,
+ left: 160,
+ x: {
+ show: !!xShow,
+ data: x,
+ levels: xLevels ? xLevels : undefined,
+ itemStyle: {color: '#111'},
+ label: {color: '#eee'},
+ },
+ y: {
+ show: !!yShow,
+ data: y,
+ levels: yLevels ? yLevels : undefined,
+ itemStyle: {color: '#111'},
+ label: {color: '#eee'},
+ },
+ corner: {
+ itemStyle: {color: '#333'},
+ label: {color: '#eee'},
+ }
+ };
+ }
+
+ function makeFailedOption(failed) {
+ return {
+ id: 'success_or_fail',
+ type: 'text',
+ ignore: failed == null,
+ style: {
+ text: failed ? 'Case Failed' : 'Case Succeeded',
+ fontSize: 18,
+ fill: failed ? 'red' : 'green',
+ }
+ }
+ }
+
+ const _matrixXYDataMap = {
+ test_d: {
+ desc: 'Different subtree have duplicated text, expect display the original text',
+ data: {
+ x: [{value: 'a', children: ['o', 'p', 'q']}, {value: 'b', children: ['o', 'p', 'q']}],
+ y: [{value: 'M', children: [new Date(), /a/]}]
+ },
+ testList: [{
+ testType: 'convertToLayout',
+ cases: [{
+ input: ['o', 'Y1'],
+ expect: {matrixXYLocatorRange: [[0, 0], [1, 1]]},
+ }, {
+ input: [3, 0],
+ expect: {matrixXYLocatorRange: [[3, 3], [0, 0]]},
+ }, {
+ input: [7, 0],
+ expect: {matrixXYLocatorRange: [[3, 5], [0, 0]]},
+ }, {
+ input: [7, 0],
+ opt: {clamp: MatrixClampOption.body},
+ expect: {matrixXYLocatorRange: [[3, 5], [0, 0]]},
+ }]
+ }]
+ },
+ test_a: {
+ desc: 'matrix.y.data is empty array, expect no error.',
+ data: {
+ x: ['a', 'X2', null, 'd'],
+ y: [],
+ },
+ testList: [{
+ testType: 'convertToLayout',
+ cases: [{
+ input: ['X2', null],
+ expect: {matrixXYLocatorRange: [[1, 1], [NaN, NaN]]},
+ }, {
+ input: [null, 0],
+ expect: {matrixXYLocatorRange: [[NaN, NaN], [NaN, NaN]]},
+ }, {
+ input: [null, null],
+ opt: {clamp: MatrixClampOption.body},
+ expect: {matrixXYLocatorRange: [[0, 3], [NaN, NaN]]},
+ }]
+ }]
+ },
+ test_a2: {
+ desc: 'matrix.x.data is empty array, expect no error.',
+ data: {
+ x: [],
+ y: ['a', 'X2', null, 'd'],
+ },
+ },
+ test_b: {
+ desc: '[null] is allowed; number is illegal in matrix.x/y.data and replaced by auto-gen text.',
+ data: {
+ x: [null, null],
+ y: [1, 2, 3],
+ },
+ testList: [{
+ testType: 'convertToLayout',
+ cases: [{
+ input: ['X1', 0],
+ expect: {matrixXYLocatorRange: [[1, 1], [0, 0]]},
+ }, {
+ input: [-1, -1],
+ expect: {matrixXYLocatorRange: [[-1, -1], [-1, -1]]},
+ }, {
+ input: [null, null],
+ opt: {clamp: MatrixClampOption.body},
+ expect: {matrixXYLocatorRange: [[0, 1], [0, 2]]},
+ }, {
+ input: [null, null],
+ opt: {clamp: MatrixClampOption.corner},
+ expect: {matrixXYLocatorRange: [[-1, -1], [-1, -1]]},
+ }, {
+ input: [0, 0],
+ opt: {clamp: MatrixClampOption.corner},
+ expect: {matrixXYLocatorRange: [[-1, -1], [-1, -1]]},
+ }]
+ }]
+ },
+ test_c: {
+ desc: 'No matrix.x/y.data, expect no error',
+ data: {
+ x: [],
+ y: [],
+ },
+ testList: [{
+ testType: 'convertToLayout',
+ cases: [{
+ input: [0, 0],
+ expect: {matrixXYLocatorRange: [[NaN, NaN], [NaN, NaN]]},
+ }, {
+ input: [null, null],
+ opt: {clamp: MatrixClampOption.body},
+ expect: {matrixXYLocatorRange: [[NaN, NaN], [NaN, NaN]]},
+ }]
+ }]
+ },
+ test_e: {
+ desc: 'All col/row width specified but not touch or overflow the bounary',
+ data: {
+ x: [{value: 'a1', size: 30}, {value: 'a2', size: 30}],
+ y: [{value: 'b1', size: 300}, {value: 'b2', size: 300}],
+ yLevels: [{levelSize: 50}]
+ },
+ },
+ test_f: {
+ desc: 'collect all from series.data (has null/NaN/undefined/illegal, expect display)',
+ data: {
+ x: undefined,
+ y: undefined,
+ seriesData: [
+ ['fruit', undefined, 1223],
+ ['bread', 'good', 323],
+ ['milk', 'good', 142],
+ [null, 'medium', 63],
+ ['bread', 'medium', 91],
+ ['milk', 9999, 45],
+ ['fruit', NaN, 55],
+ ['bread', 'bad', 15],
+ ['milk', 'bad', 53],
+ ],
+ },
+ },
+ test_g: {
+ desc: 'no series.data and no matrix.x/y',
+ data: {
+ x: undefined,
+ y: undefined,
+ seriesData: [],
+ },
+ },
+ };
+
+ let _ctx = {
+ xyData: 'test_d',
+ testResult: null, // {result: unknown, failed: boolean}
+ xShow: true,
+ yShow: true,
+ convertFromPixelOpt: null,
+ };
+
+ function updateView() {
+ const option = {
+ matrix: makeMatrixOption(_matrixXYDataMap[_ctx.xyData].data, _ctx),
+ series: {
+ data: _matrixXYDataMap[_ctx.xyData].data.seriesData
+ },
+ };
+ chart.setOption(option, {replaceMerge: 'matrix'});
+
+ chart.setOption({
+ graphic: {
+ elements: [{
+ id: 'convert_result',
+ style: {
+ text: _ctx.testResult ? _ctx.testResult.result : '',
+ }
+ }, {
+ id: 'desc_text',
+ style: {
+ text: '--- expect ---\n' + (_matrixXYDataMap[_ctx.xyData].desc || '')
+ + '\n--- current matrix data ---\n'
+ + testHelper.printObject(
+ {
+ x: {
+ data: _matrixXYDataMap[_ctx.xyData].data.x,
+ levels: _matrixXYDataMap[_ctx.xyData].data.xLevels,
+ },
+ y: {
+ data: _matrixXYDataMap[_ctx.xyData].data.y,
+ levels: _matrixXYDataMap[_ctx.xyData].data.yLevels,
+ },
+ },
+ {lineBreakMaxColumn: 150}
+ )
+ + '\n--- current series data ---\n' + (
+ _matrixXYDataMap[_ctx.xyData].data.seriesData
+ ? testHelper.printObject(
+ _matrixXYDataMap[_ctx.xyData].data.seriesData,
+ {lineBreakMaxColumn: 30}
+ )
+ : 'undefined'
+ )
+ }
+ },
+ makeFailedOption(_ctx.testResult ? !!_ctx.testResult.failed : undefined)
+ ]
+ }
+ });
+ }
+
+ const option = {
+ tooltip: {},
+ backgroundColor: '#aee',
+ matrix: makeMatrixOption(_matrixXYDataMap[_ctx.xyData].data, _ctx),
+ series: {
+ type: 'scatter',
+ coordinateSystem: 'matrix',
+ symbolSize: 20,
+ encode: {label: 2},
+ label: {show: true},
+ data: _matrixXYDataMap[_ctx.xyData].data.seriesData
+ },
+ graphic: {
+ elements: [{
+ bottom: 5,
+ left: 'center',
+ id: 'convert_result',
+ type: 'text',
+ style: {
+ text: '',
+ fill: 'blue',
+ fontSize: 11,
+ },
+ silent: true
+ }, {
+ left: 5,
+ top: 5,
+ id: 'desc_text',
+ type: 'text',
+ style: {
+ fontSize: 12,
+ fill: '#333',
+ },
+ silent: true
+ }, {
+ right: 5,
+ top: 5,
+ id: 'success_or_fail',
+ type: 'text',
+ style: {
+ fontSize: 20,
+ fill: 'red',
+ },
+ silent: true
+ }]
+ }
+ };
+
+ var chart = testHelper.create(echarts, 'main_dim_data_edge_cases', {
+ title: [
+ 'edge case',
+ '**click** on matrix inside and outside to test clamp.'
+ ],
+ option: option,
+ height: 400,
+ inputsStyle: 'compact',
+ inputs: [{
+ text: 'test cases:',
+ type: 'select',
+ options: Object.keys(_matrixXYDataMap).map(key => {
+ let text = _matrixXYDataMap[key].desc;
+ const MAX_STR = 100;
+ if (text.length > MAX_STR) {
+ text = text.slice(0, MAX_STR) + ' ...';
+ }
+ return {text: text, value: key};
+ }),
+ onchange() {
+ const key = this.value;
+ _ctx.xyData = key;
+ _ctx.testResult = null;
+ chart.__testHelper.switchGroup(key);
+ updateView();
+ }
+ }, {
+ type: 'br',
+ }, {
+ text: 'matrix.x show:',
+ type: 'select',
+ values: [true, false],
+ onchange() {
+ _ctx.xShow = this.value;
+ updateView();
+ }
+ }, {
+ text: 'matrix.y show:',
+ type: 'select',
+ values: [true, false],
+ onchange() {
+ _ctx.yShow = this.value;
+ updateView();
+ }
+ }, {
+ text: 'convertFromPixel opt:',
+ type: 'select',
+ options: [
+ {value: undefined},
+ {text: 'clamp body', value: {clamp: MatrixClampOption.body}},
+ {text: 'clamp corner', value: {clamp: MatrixClampOption.corner}},
+ {text: 'clamp all', value: {clamp: MatrixClampOption.all}},
+ {text: 'clamp none', value: {clamp: MatrixClampOption.none}},
+ ],
+ onchange() {
+ _ctx.convertFromPixelOpt = this.value;
+ }
+ }, {
+ type: 'groupset',
+ inputsStyle: 'compact',
+ inputsHeight: 70,
+ groups: Object.keys(_matrixXYDataMap).map(key => {
+ const textList = _matrixXYDataMap[key].testList || [];
+ const inputs = [];
+ textList.forEach(testItem => {
+ if (testItem.testType === 'convertToLayout') {
+ const cases = testItem.cases;
+ cases.forEach(caseItem => {
+ inputs.push({
+ text: `convertToLayout(${testHelper.printObject(caseItem.input)}, ${testHelper.printObject(caseItem.opt)})`,
+ onclick() {
+ let expect = caseItem.expect;
+ let failed = false;
+ let actual;
+ try {
+ actual = chart.convertToLayout({matrixIndex: 0}, caseItem.input, caseItem.opt);
+ console.log(actual);
+ if (!numEq(expect.matrixXYLocatorRange[0][0], actual.matrixXYLocatorRange[0][0])
+ || !numEq(expect.matrixXYLocatorRange[0][1], actual.matrixXYLocatorRange[0][1])
+ || !numEq(expect.matrixXYLocatorRange[1][0], actual.matrixXYLocatorRange[1][0])
+ || !numEq(expect.matrixXYLocatorRange[1][1], actual.matrixXYLocatorRange[1][1])
+ ) {
+ failed = true;
+ }
+ }
+ catch (err) {
+ console.error(err);
+ failed = true;
+ }
+ const result = 'convertToLayout result: ' + testHelper.printObject(actual);
+ _ctx.testResult = {result, failed};
+ updateView();
+ }
+ });
+ });
+ }
+ else {
+ throw new Error('illegal');
+ }
+ });
+
+ return {
+ id: key,
+ text: '[' + key + '] test cases:',
+ inputs,
+ };
+ })
+ }] // End of inputs
+ });
+
+ if (chart) {
+ updateView();
+
+ chart.getZr().on('click', function (event) {
+ const point = [event.offsetX, event.offsetY];
+ const data = chart.convertFromPixel({matrixIndex: 0}, point, _ctx.convertFromPixelOpt);
+ const layout = chart.convertToLayout({matrixIndex: 0}, data);
+
+ _ctx.testResult = {
+ result: 'convertFromPixel result: ' + testHelper.printObject(data) + '\n'
+ + 'Then use it convertToLayout result: ' + testHelper.printObject(layout),
+ failed: false
+ };
+ updateView();
+ });
+ }
+ });
+
+ function eqNaN(value) {
+ return value !== value;
+ }
+
+ function numEq(expect, actual) {
+ return eqNaN(expect)
+ ? eqNaN(actual)
+ : expect === actual;
+ }
+ </script>
+
+
+
+ </body>
+</html>
+
diff --git a/test/matrix3.html b/test/matrix3.html
new file mode 100644
index 000000000..a4e3c115a
--- /dev/null
+++ b/test/matrix3.html
@@ -0,0 +1,858 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+
+<html>
+ <head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <script src="lib/simpleRequire.js"></script>
+ <script src="lib/config.js"></script>
+ <script src="lib/jquery.min.js"></script>
+ <script src="lib/facePrint.js"></script>
+ <script src="lib/testHelper.js"></script>
+ <script src="lib/draggable.js"></script>
+ <!-- <script src="ut/lib/canteen.js"></script> -->
+ <link rel="stylesheet" href="lib/reset.css" />
+ </head>
+ <body>
+ <style>
+ </style>
+
+
+ <div id="main_layout_components_series"></div>
+
+ <div id="main_layout_timeline"></div>
+
+
+
+ <script>
+ require([
+ 'echarts',
+ './data/czech_geo.json',
+ 'data/svg/Beef_cuts_France.svg',
+ ], function (echarts, czech_geo, Beef_cuts_France) {
+
+ echarts.registerMap('czech_geo', czech_geo);
+ echarts.registerMap('Beef_cuts_France', {
+ svg: Beef_cuts_France
+ });
+
+ // --- state ------
+ let _xShowing = true;
+ let _yShowing = true;
+ let _testCase = null;
+ // ----------------
+
+ var option;
+ option = {
+ backgroundColor: '#eee',
+ tooltip: {},
+ title: {
+ text: '',
+ right: 'center',
+ },
+ matrix: [{
+ // matrix 0
+ id: 'matrix0',
+ left: 10,
+ bottom: 10,
+ width: '30%',
+ top: 40,
+ x: {
+ levelSize: 20,
+ data: ['X1', 'X2', 'X3'],
+ },
+ y: {
+ levelSize: 30,
+ data: ['Y1', 'Y2', 'Y3', 'Y4'],
+ },
+ body: {
+ data: [{
+ coord: [[0, 2], 1],
+ mergeCells: true,
+ }, {
+ coord: [['X1', 'X2'], 'Y1'],
+ mergeCells: true,
+ }, {
+ coord: [['X1', 'X2'], 'Y3'],
+ mergeCells: true,
+ }, {
+ coord: [['X1', 'X2'], 'Y4'],
+ mergeCells: true,
+ }]
+ },
+ }, {
+ // matrix 1
+ id: 'matrix1',
+ right: 10,
+ left: '35%',
+ top: 20,
+ bottom: 30,
+ tooltip: {
+ show: true,
+ formatter(params) {
+ return params.name;
+ }
+ },
+ x: {
+ label: {
+ color: '#eee'
+ },
+ itemStyle: {
+ color: 'rgba(10,10,0,0.9)'
+ },
+ levels: [null, {levelSize: 30}],
+ data: [{
+ value: 'R',
+ children: ['R1', {value: 'R2'}]
+ }, {
+ value: 'S'
+ }, {
+ value: 'T',
+ children: [
+ {
+ value: 'T1',
+ children: ['T11'],
+ }
+ ]
+ },
+ 'O',
+ 'P',
+ 'Q',
+ ]
+ },
+ y: {
+ levelSize: 30,
+ label: {
+ color: '#eee'
+ },
+ itemStyle: {
+ color: 'rgba(10,10,0,0.9)'
+ },
+ data: [{
+ value: 'A',
+ children: ['A1', 'A2', {
+ value: 'A3',
+ children: ['A31', 'A32']
+ }]
+ }, {
+ value: 'B',
+ children: ['B1', 'B2', 'B3', 'B4']
+ }, {
+ value: 'C',
+ }, {
+ value: 'D',
+ }],
+ },
+ body: {
+ label: {
+ },
+ itemStyle: {
+ // color: 'rgba(240,240,140,0.5)'
+ },
+ data: [{
+ coord: ['R2', 'A3'],
+ mergeCells: true,
+ }, {
+ coord: ['R1', ['B4', 'C']],
+ mergeCells: true,
+ }, {
+ coord: [['R1', 'R2'], ['B2', 'B3']],
+ mergeCells: true,
+ }, {
+ coord: [['O', 'P'], ['B3', 'C']],
+ mergeCells: true,
+ }, {
+ coord: ['T', ['A1', 'A31']],
+ mergeCells: true,
+ }, {
+ coord: [null, 'D'],
+ mergeCells: true,
+ }, {
+ coord: [['P', 'Q'], 'A1'],
+ mergeCells: true,
+ }],
+ },
+ corner: {
+ label: {
+ color: '#eee'
+ },
+ itemStyle: {
+ color: 'rgba(10,10,0,0.9)',
+ // borderWidth: 1,
+ },
+ data: [{
+ coord: [[-1, -3], -1],
+ mergeCells: true,
+ value: 'Y values',
+ }, {
+ coord: [[-1, -2], -2],
+ mergeCells: true,
+ value: 'X values',
+ label: {
+ position: 'insideRight'
+ }
+ }],
+ }
+ }],
+ toolbox: {
+ show: true,
+ feature: {
+ dataZoom: {
+ yAxisIndex: 'none'
+ },
+ // dataView: { readOnly: false },
+ restore: {},
+ saveAsImage: {}
+ },
+ top: 'center',
+ left: 'center',
+ coordinateSystem: 'matrix',
+ matrixId: 'matrix1',
+ coord: [['P', 'Q'], 'A1'],
+ },
+ angleAxis: {
+ type: 'category',
+ data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
+ axisLabel: {fontSize: 9},
+ },
+ radiusAxis: {
+ axisLabel: {fontSize: 9},
+ },
+ polar: {
+ // center: ['40%', '50%'],
+ radius: '50%',
+ coordinateSystem: 'matrix',
+ matrixId: 'matrix0',
+ coord: ['X3', 'Y3']
+ },
+ radar: {
+ axisName: {
+ color: '#333',
+ fontSize: 9,
+ },
+ nameGap: 0,
+ radius: '80%',
+ indicator : [
+ {text: 'IE8-', max: 400},
+ {text: 'IE9+', max: 400},
+ {text: 'Safari', max: 400},
+ {text: 'Firefox', max: 400},
+ {text: 'Chrome', max: 400}
+ ],
+ coordinateSystem: 'matrix',
+ matrixId: 'matrix0',
+ coord: ['X3', 'Y4'],
+ },
+ singleAxis: {
+ type: 'value',
+ // height: height,
+ splitArea: {
+ show: true
+ },
+ axisTick: {show: false},
+ axisLabel: {fontSize: 9, margin: 3},
+ bottom: 12,
+ coordinateSystem: 'matrix',
+ matrixId: 'matrix1',
+ coord: ['R1', 'D'],
+ },
+ parallelAxis: [
+ {dim: 0, name: 'f0'},
+ {dim: 1, name: 'f1'},
+ {dim: 2, name: 'f2'}
+ ],
+ parallel: {
+ left: 10,
+ right: 30,
+ top: 30,
+ bottom: 10,
+ coordinateSystem: 'matrix',
+ matrixId: 'matrix0',
+ coord: ['X1', 'Y4'],
+ parallelAxisDefault: {
+ nameGap: 5,
+ }
+ },
+ grid: [{
+ id: 'cartesian_a',
+ coordinateSystem: 'matrix',
+ matrixId: 'matrix1',
+ left: 3,
+ right: 3,
+ top: 3,
+ bottom: 3,
+ containLabel: true,
+ coord: ['R2', 'B2'],
+ }, {
+ id: 'cartesian_b',
+ coordinateSystem: 'matrix',
+ matrixId: 'matrix0',
+ left: 3,
+ right: 3,
+ top: 30,
+ bottom: 30,
+ containLabel: true,
+ coord: ['X1', 'Y3'],
+ }],
+ xAxis: [{
+ id: 'cartesian_a',
+ gridId: 'cartesian_a',
+ }, {
+ id: 'cartesian_b',
+ gridId: 'cartesian_b',
+ axisTick: {length: 3},
+ axisLabel: {fontSize: 9, margin: 5},
+ }],
+ yAxis: [{
+ id: 'cartesian_a',
+ gridId: 'cartesian_a',
+ }, {
+ id: 'cartesian_b',
+ gridId: 'cartesian_b',
+ axisTick: {length: 3},
+ axisLabel: {fontSize: 9, margin: 5},
+ }],
+ geo: [{
+ map: 'czech_geo',
+ id: 'czech_geo_component',
+ coordinateSystem: 'matrix',
+ matrixId: 'matrix0',
+ coord: ['X1', 'Y2'],
+ // left: '50%',
+ // right: 10,
+ // layoutCenter: [10, 10],
+ // layoutSize: 100,
+ }],
+ visualMap: [{
+ type: 'continuous',
+ left: 'center',
+ bottom: 0,
+ // width: '50%',
+ itemHeight: 90,
+ itemWidth: 10,
+ min: 5,
+ max: 100,
+ // text: ['Price'],
+ realtime: true,
+ calculable: true,
+ orient: 'horizontal',
+ seriesId: ['Beef_cuts_France', 'czech_geo_series'],
+ inRange: {
+ color: ['#dbac00', '#db6e00', '#cf0000']
+ },
+ coordinateSystem: 'matrix',
+ matrixId: 'matrix0',
+ coord: ['X1', 'Y1'],
+ }, {
+ type: 'piecewise',
+ right: 0,
+ bottom: 0,
+ top: 'center',
+ // width: '50%',
+ // itemHeight: 90,
+ itemWidth: 10,
+ itemGap: 5,
+ min: 5,
+ max: 100,
+ // text: ['Price'],
+ realtime: true,
+ calculable: true,
+ textStyle: {
+ fontSize: 9,
+ },
+ textGap: 3,
+ // orient: 'horizontal',
+ seriesId: ['Beef_cuts_France', 'czech_geo_series'],
+ inRange: {
+ color: ['#dbac00', '#db6e00', '#cf0000']
+ },
+ coordinateSystem: 'matrix',
+ matrixId: 'matrix0',
+ coord: ['X1', 'Y2'],
+ }],
+ dataZoom: [{
+ type: 'slider',
+ xAxisId: 'cartesian_b',
+ coordinateSystem: 'matrix',
+ matrixId: 'matrix0',
+ coord: ['X1', 'Y3'],
+ bottom: 0,
+ height: 20,
+ // left: 0,
+ left: 'center'
+ }],
+ legend: {
+ data: ['line_a', 'line_b'],
+ coordinateSystem: 'matrix',
+ coord: ['X1', 'Y3'],
+ textStyle: {
+ fontSize: 11,
+ },
+ itemHeight: 7,
+ itemWidth: 15,
+ },
+ title: [{
+ text: 'series-map (svg source)',
+ coordinateSystem: 'matrix',
+ coord: ['X1', 'Y1'],
+ textStyle: {
+ fontSize: 10
+ }
+ }, {
+ text: 'series-map\n(standalone)',
+ coordinateSystem: 'matrix',
+ coord: ['X3', 'Y1'],
+ textStyle: {
+ fontSize: 10
+ }
+ }, {
+ text: 'geo component\n(and serves a series.map)',
+ coordinateSystem: 'matrix',
+ coord: ['X3', 'Y2'],
+ textStyle: {
+ fontSize: 10
+ }
+ }],
+ series: [{
+ type: 'scatter',
+ coordinateSystem: 'matrix',
+ matrixIndex: 1,
+ symbolSize: 30,
+ data: [
+ ['R1', 'A1', 'scatter'], ['R2', 'A1', 20], ['R1', 'A2', 30]
+ ],
+ encode: {label: 2},
+ label: {show: true}
+ }, {
+ type: 'scatter',
+ xAxisId: 'cartesian_a',
+ yAxisId: 'cartesian_a',
+ data: [[12,56], [98,76], [37,82]],
+ }, {
+ type: 'line',
+ name: 'line_a',
+ xAxisId: 'cartesian_b',
+ yAxisId: 'cartesian_b',
+ data: [[12,16], [48, 86], [68, 16], [97, 62]],
+ }, {
+ type: 'line',
+ name: 'line_b',
+ xAxisId: 'cartesian_b',
+ yAxisId: 'cartesian_b',
+ data: [[16,26], [28, 46], [58, 96], [87, 52]],
+ }, {
+ type: 'pie',
+ id: 'pie_a',
+ coordinateSystem: 'matrix',
+ matrixId: 'matrix1',
+ coord: ['O', 'B3'],
+ radius: '50%',
+ label: {overflow: 'none'},
+ labelLayout: {hideOverlap: false},
+ labelLine: {length: 2, length2: 2},
+ data: [
+ {name: 'a1', value: 123},
+ {name: 'no_center', value: 234},
+ {name: 'c1', value: 345},
+ ],
+ }, {
+ type: 'pie',
+ id: 'pie_b',
+ coordinateSystem: 'matrix',
+ matrixId: 'matrix1',
+ coord: ['P', 'B2'],
+ center: [15, 15],
+ radius: '50%',
+ label: {overflow: 'none'},
+ labelLayout: {hideOverlap: false},
+ labelLine: {length: 2, length2: 2},
+ data: [
+ {name: 'a2', value: 123},
+ {name: 'center_15', value: 234},
+ {name: 'c2', value: 345},
+ ],
+ }, {
+ type: 'pie',
+ id: 'pie_c',
+ coordinateSystem: 'matrix',
+ matrixId: 'matrix1',
+ center: ['O', 'B2'],
+ radius: '50%',
+ label: {overflow: 'none'},
+ labelLayout: {hideOverlap: false},
+ labelLine: {length: 2, length2: 2},
+ data: [
+ {name: 'a', value: 123},
+ {name: 'b', value: 234},
+ {name: 'no_coord', value: 345},
+ ],
+ }, {
+ type: 'pie',
+ id: 'pie_d',
+ coordinateSystem: 'matrix',
+ matrixId: 'matrix1',
+ coord: ['Q', 'B2'],
+ left: '50%',
+ radius: '50%',
+ label: {overflow: 'none'},
+ labelLayout: {hideOverlap: false},
+ labelLine: {length: 2, length2: 2},
+ data: [
+ {name: 'a', value: 123},
+ {name: 'b', value: 234},
+ {name: 'left:50%', value: 345},
+ ],
+ }, {
+ type: 'funnel',
+ id: 'funnel_a',
+ coordinateSystem: 'matrix',
+ matrixId: 'matrix1',
+ coord: ['O', 'A1'],
+ gap: 3,
+ left: 0,
+ right: 0,
+ top: 0,
+ bottom: 0,
+ label: {position: 'inside'},
+ sort: 'ascending',
+ data:[
+ {value:60, name:'a'},
+ {value:40, name:'b'},
+ {value:20, name:'c'},
+ {value:80, name:'d'},
+ ]
+ }, {
+ type: 'graph',
+ id: 'graph_a',
+ layout: 'none',
+ coordinateSystem: 'matrix',
+ coordinateSystemUsage: 'box',
+ matrixId: 'matrix1',
+ coord: ['T', 'A1'],
+ symbolSize: 15,
+ roam: true,
+ label: {show: true, fontSize: 10},
+ edgeSymbol: ['circle', 'arrow'],
+ edgeSymbolSize: [4, 7],
+ lineStyle: {opacity: 0.9, width: 2, curveness: 0},
+ data: [
+ {name: 'Node 0', x: 300, y: 300},
+ {name: 'Node 1', x: 800, y: 300},
+ {name: 'all in one cell', x: 550, y: 100},
+ {name: 'Node 3', x: 550, y: 500}
+ ],
+ links: [
+ {source: 0, target: 1},
+ {source: 1, target: 2},
+ {source: 0, target: 3}
+ ],
+ }, {
+ type: 'graph',
+ id: 'graph_b',
+ layout: 'none',
+ coordinateSystem: 'matrix',
+ matrixId: 'matrix1',
+ // no coord, but use coord sys by data.
+ symbolSize: 15,
+ roam: true,
+ label: {show: true, fontSize: 10},
+ edgeSymbol: ['circle', 'arrow'],
+ edgeSymbolSize: [4, 7],
+ lineStyle: {opacity: 0.9, width: 2, curveness: 0},
+ data: [
+ {name: 'Node 0', value: ['T', 'A32']},
+ {name: 'Node 1', value: ['O', 'A32']},
+ {name: 'cross cells', value: ['O', 'A31']},
+ ],
+ links: [
+ {source: 0, target: 1},
+ {source: 1, target: 2},
+ {source: 0, target: 3}
+ ],
+ }, {
+ name: 'French Beef Cuts',
+ id: 'Beef_cuts_France',
+ type: 'map',
+ map: 'Beef_cuts_France',
+ coordinateSystem: 'matrix',
+ coordinateSystemUsage: 'box',
+ coord: ['X1', 'Y1'],
+ roam: true,
+ // top: 0,
+ bottom: 0,
+ aspectScale: 1,
+ zoom: 1,
+ // layoutSize: '30%',
+ showLegendSymbol: false,
+ emphasis: {
+ label: {
+ show: false
+ }
+ },
+ data: [
+ { name: 'Queue', value: 15 },
+ { name: 'Langue', value: 35 },
+ { name: 'Plat de joue', value: 15 },
+ { name: 'Gros bout de poitrine', value: 25 },
+ { name: 'Jumeau à pot-au-feu', value: 45 },
+ { name: 'Onglet', value: 85 },
+ { name: 'Plat de tranche', value: 25 },
+ { name: 'Araignée', value: 15 },
+ { name: 'Gîte à la noix', value: 55 },
+ { name: 'Bavette d\'aloyau', value: 25 },
+ { name: 'Tende de tranche', value: 65 },
+ { name: 'Rond de gîte', value: 45 },
+ { name: 'Bavettede de flanchet', value: 85 },
+ { name: 'Flanchet', value: 35 },
+ { name: 'Hampe', value: 75 },
+ { name: 'Plat de côtes', value: 65 },
+ { name: 'Tendron Milieu de poitrine', value: 65 },
+ { name: 'Macreuse à pot-au-feu', value: 85 },
+ { name: 'Rumsteck', value: 75 },
+ { name: 'Faux-filet', value: 65 },
+ { name: 'Côtes Entrecôtes', value: 55 },
+ { name: 'Basses côtes', value: 45 },
+ { name: 'Collier', value: 85 },
+ { name: 'Jumeau à biftek', value: 15 },
+ { name: 'Paleron', value: 65 },
+ { name: 'Macreuse à bifteck', value: 45 },
+ { name: 'Gîte', value: 85 },
+ { name: 'Aiguillette baronne', value: 65 },
+ { name: 'Filet', value: 95 },
+ ]
+ }, {
+ type: 'map',
+ map: 'czech_geo',
+ coordinateSystem: 'matrix',
+ coordinateSystemUsage: 'box',
+ matrixId: 'matrix0',
+ coord: ['X3', 'Y1'],
+ // left: '50%',
+ // right: 10,
+ // layoutCenter: [10, 10],
+ // layoutSize: 100,
+ }, {
+ type: 'map',
+ geoId: 'czech_geo_component',
+ id: 'czech_geo_series',
+ geoIndex: 0,
+ data: [
+ {name: 'Praha', value: 23},
+ {name: 'Plzeňský kraj', value: 51},
+ {name: 'Středočeský kraj', value: 91}
+ ]
+ }, {
+ type: 'bar',
+ data: [1, 2, '-', 3, 0, 5, 7],
+ coordinateSystem: 'polar',
+ barMaxWidth: '50%',
+ barMinHeight: 10
+ }, {
+ type: 'parallel',
+ smooth: 0,
+ lineStyle: {color: 'red'},
... 1484 lines suppressed ...
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@echarts.apache.org
For additional commands, e-mail: commits-help@echarts.apache.org
(echarts) 03/04: feat(visualMap): support specify `seriesId` on visualMap option; previously only seriesIndex can be specified, not convenient and not consistent with other components.
Posted by su...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
sushuang pushed a commit to branch feat-matrix-enhance
in repository https://gitbox.apache.org/repos/asf/echarts.git
commit 8ddd15d320bbab70e22b0dd87d725bb5160fa1c8
Author: 100pah <su...@gmail.com>
AuthorDate: Tue May 27 04:31:04 2025 +0800
feat(visualMap): support specify `seriesId` on visualMap option; previously only seriesIndex can be specified, not convenient and not consistent with other components.
---
src/component/visualMap/VisualMapModel.ts | 45 ++++++++++++++++++-------------
1 file changed, 27 insertions(+), 18 deletions(-)
diff --git a/src/component/visualMap/VisualMapModel.ts b/src/component/visualMap/VisualMapModel.ts
index 85b3162e5..74c791694 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';
@@ -67,12 +68,13 @@ export interface VisualMapOption<T extends VisualOptionBase = VisualOptionBase>
/**
* '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
@@ -237,23 +239,30 @@ class VisualMapModel<Opts extends VisualMapOption = VisualMapOption> extends Com
}
/**
- * @protected
- * @return {Array.<number>} 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);
}
/**
@@ -605,7 +614,7 @@ class VisualMapModel<Opts extends VisualMapOption = VisualMapOption> extends Com
// zlevel: 0,
z: 4,
- seriesIndex: 'all',
+ // seriesIndex: 'all',
min: 0,
max: 200,
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@echarts.apache.org
For additional commands, e-mail: commits-help@echarts.apache.org
(echarts) 01/04: test: sync test helper.
Posted by su...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
sushuang pushed a commit to branch feat-matrix-enhance
in repository https://gitbox.apache.org/repos/asf/echarts.git
commit 6bcfe1064322b6cda8f164a35b9d6858d990fa57
Author: 100pah <su...@gmail.com>
AuthorDate: Sat May 24 18:07:57 2025 +0800
test: sync test helper.
---
test/build/mktest-tpl.html | 137 ++-
test/lib/reset.css | 230 ++++-
test/lib/testHelper.js | 2121 +++++++++++++++++++++++++++++++++++++++++---
test/tmp-base.html | 113 ++-
4 files changed, 2451 insertions(+), 150 deletions(-)
diff --git a/test/build/mktest-tpl.html b/test/build/mktest-tpl.html
index 84218ef3f..4c6d69697 100644
--- a/test/build/mktest-tpl.html
+++ b/test/build/mktest-tpl.html
@@ -28,14 +28,19 @@ under the License.
<script src="lib/jquery.min.js"></script>
<script src="lib/facePrint.js"></script>
<script src="lib/testHelper.js"></script>
- <!-- <script src="ut/lib/canteen.js"></script> -->
+ <!-- <script src="lib/canteen.js"></script> -->
+ <!-- <script src="lib/draggable.js"></script> -->
<link rel="stylesheet" href="lib/reset.css" />
</head>
<body>
<style>
+ html {
+ /* Fix the line-height to integer to avoid it varying across clients and
+ causing visual test failures. Some clients may not support fractional px. */
+ line-height: 18px;
+ }
</style>
-
<!-- TPL_DOM_PLACE -->
<!-- TPL_JS_PLACE -->
@@ -48,33 +53,105 @@ under the License.
<!-- TPL_SEGMENT_DELIMITER -->
-
<script>
- require([
- 'echarts',
- // 'map/js/china',
- // './data/nutrients.json'
- ], function (echarts) {
- var option;
-
- option = {
- xAxis: {},
- yAxis: {},
- series: {
- type: 'line',
- data: [[11, 22], [33, 44]]
- }
- };
-
- var chart = testHelper.create(echarts, '{{TPL_DOM_ID}}', {
- title: [
- 'Test Case Description of {{TPL_DOM_ID}}',
- '(Muliple lines and **emphasis** are supported in description)'
- ],
- option: option
- // height: 300,
- // buttons: [{text: 'btn-txt', onclick: function () {}}],
- // recordCanvas: true,
- });
- });
+
+ require([
+ 'echarts',
+ // 'data/life-expectancy-table.json'
+ // 'theme/dark.js', // auto register if load.
+ ], function (echarts /*, data */) {
+
+ // // Data can be fetched by:
+ // $.getJSON('./data/nutrients.json', function (data) {
+ // });
+
+ var option = {
+ xAxis: {},
+ yAxis: {},
+ series: {
+ type: 'scatter',
+ symbolSize: 50,
+ label: {show: true, position: 'top'},
+ data: [[1, 2], [100, 200], [500, 50]]
+ }
+ };
+
+ var chart = testHelper.create(echarts, '{{TPL_DOM_ID}}', {
+ title: [
+ 'Test Case Description of {{TPL_DOM_ID}}',
+ '(Muliple lines and **emphasis** are supported in description)'
+ ],
+ option: option,
+ //
+ // -------------------------- Optional settings: --------------------------
+ // height: 400, // Optional. Specify a different chart height.
+ // draggable: true, // Optional. Add a draggable button to mutify the chart size.
+ // // This feature require "test/lib/draggable.js"
+ // recordCanvas: true, // Optional. Record canvas instructions. (for debug)
+ // // This feature requires "test/lib/canteen.js"
+ // boundingRect: true, // Optional. Show boundingRects of zrender elements (for debug).
+ //
+ // ------------------- Inputs (button/range/select/br/hr): ----------------
+ // inputsHeight: 30, // Optional. Fix the height of inputs area (scrollable if overflow)
+ inputsStyle: 'compact', // Optional.
+ inputs: [ // Optional. The following are sample inputs:
+ {
+ type: 'select',
+ text: '(sample) boundingRect:',
+ values: [false, true, undefined, {color: 'rgba(255,0,0,0.8)', silent: false}],
+ onchange: function () {
+ chart.__testHelper.boundingRect(this.value);
+ }
+ },
+ {
+ type: 'range',
+ text: '(sample) symbolSize:',
+ // min: -100, // Optional.
+ // max: 100, // Optional.
+ // value: 50, // Optional.
+ onchange: function () {
+ console.log('range changed:', this.value);
+ chart.setOption({series: {symbolSize: this.value}});
+ }
+ },
+ {
+ type: 'select',
+ text: '(sample range embedded select) grid.left:',
+ options: [
+ {value: undefined},
+ {value: 30},
+ {input: {type: 'range', min: -300, max: 300, value: 50}}
+ ],
+ onchange: function () {
+ var newVal = this.value;
+ console.log('select 2 changed:', newVal);
+ chart.setOption({grid: {left: newVal}});
+ }
+ },
+ {
+ type: 'br', // line break
+ },
+ {
+ text: '(sample) print failures to screen',
+ onclick: function () {
+ testHelper.printAssert('{{TPL_DOM_ID}}', function (assert) {
+ assert(true);
+ });
+ }
+ },
+ {
+ text: '(sample) copy option to clipboard',
+ onclick: function () {
+ // console.log(testHelper.printObject(option));
+ testHelper.clipboard(option);
+ }
+ }
+
+ ] // End of `inputs`
+
+ }); // End of `testHelper.create`
+
+ }); // End of `require`
+
+
</script>
diff --git a/test/lib/reset.css b/test/lib/reset.css
index 6a2ddce4e..645774184 100644
--- a/test/lib/reset.css
+++ b/test/lib/reset.css
@@ -51,9 +51,222 @@ body > .main {
padding-left: 2px;
padding-right: 2px;
}
-.test-buttons button {
+
+/*
+ * [ CAUTION ]
+ * The existing CSS class names below MUST NOT be modified;
+ * 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-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;
+ margin-bottom: 2px;
+}
+.test-inputs-slider {
+ white-space: nowrap;
+ display: inline-block;
+ margin: 10px 5px;
+ font-size: 14px;
+ padding: 0;
+}
+.test-inputs-style-compact .test-inputs-slider {
+ margin-top: 2px;
+ margin-bottom: 2px;
+}
+.test-inputs-slider-input {
+ 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: 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;
+ margin: 0 3px;
+}
+.test-inputs-slider-disabled span {
+ color: #aaa;
+}
+.test-inputs-slider input {
+ 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;
+ font-size: 14px;
margin: 10px 5px;
+ margin-right: 10px;
+}
+.test-inputs-style-compact .test-inputs-select {
+ margin-top: 2px;
+ margin-bottom: 2px;
+}
+.test-inputs-select span {
+ vertical-align: middle;
+ margin: 0 3px;
}
+.test-inputs-select-disabled span {
+ color: #aaa;
+}
+.test-inputs-select select {
+ vertical-align: middle;
+ margin: 0;
+ padding: 0;
+ 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;
}
@@ -73,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 c570529d3..7eb2052b2 100644
--- a/test/lib/testHelper.js
+++ b/test/lib/testHelper.js
@@ -47,8 +47,7 @@
var myRandom = new seedrandom('echarts-random');
// Fixed random generator
Math.random = function () {
- const val = myRandom();
- return val;
+ return myRandom();
};
});
}
@@ -58,25 +57,217 @@
/**
* @param {Object} opt
- * @param {string|Array.<string>} [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 <strong>abc</strong>.
- * @param {Option} opt.option
- * @param {Object} [opt.info] info object to display.
- * @param {string} [opt.infoKey='option']
- * @param {Object|Array} [opt.dataTable]
- * @param {Array.<Object|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 {Array.<Object>|Object} [opt.button] {text: ..., onClick: ...}, or an array of them.
- * @param {Array.<Object>|Object} [opt.buttons] {text: ..., onClick: ...}, or an array of them.
- * @param {boolean} [opt.recordCanvas] 'test/lib/canteen.js' is required.
- * @param {boolean} [opt.recordVideo]
- * @param {string} [opt.renderer] 'canvas' or 'svg'
+ * @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 {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] 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 {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}, ...]; }
+ * 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 <input type="range">).
+ * 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
+ * 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 <select>...</select>).
+ * 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.
+ * }}
+ * ],
+ * 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); }
+ * },
+ * {
+ * // 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.
+ * },
+ * // ...
+ * ]
+ * ----------------------------- 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.<Object|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);
@@ -85,24 +276,30 @@
return;
}
- var title = document.createElement('div');
+ var errMsgPrefix = '[testHelper dom: ' + domOrId + ']';
+
+ var titleContainer = document.createElement('div');
var left = document.createElement('div');
+ var chartContainerWrapper = document.createElement('div');
var chartContainer = document.createElement('div');
- var buttonsContainer = 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'));
- 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';
- buttonsContainer.className = 'test-buttons';
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';
@@ -113,77 +310,1226 @@
left.appendChild(recordCanvasContainer);
left.appendChild(recordVideoContainer);
- left.appendChild(buttonsContainer);
+ 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 = '<div class="test-title-inner">'
- + testHelper.encodeHTML(optTitle)
+ titleContainer.innerHTML = '<div class="test-title-inner">'
+ + encodeHTML(optTitle)
.replace(/\*\*([^*]+?)\*\*/g, '<strong>$1</strong>')
.replace(/\n/g, '<br>')
+ '</div>';
}
+ }
- chart = testHelper.createChart(echarts, chartContainer, opt.option, opt, opt.setOptionOpts);
+ function initUpdateInfo(opt, chart, infoContainer) {
+ assert(chart.__testHelper);
- var dataTables = opt.dataTables;
- if (!dataTables && opt.dataTable) {
- dataTables = [opt.dataTable];
+ if (opt.info) {
+ updateInfo(opt.info, opt.infoKey);
}
- if (dataTables) {
- var tableHTML = [];
- for (var i = 0; i < dataTables.length; i++) {
- tableHTML.push(createDataTableHTML(dataTables[i], opt));
+
+ function updateInfo(info, infoKey) {
+ infoContainer.innerHTML = createObjectHTML(info, infoKey || 'option');
+ }
+
+ chart.__testHelper.updateInfo = updateInfo;
+ }
+
+ function initInputs(chart, opt, inputsContainer, errMsgPrefix) {
+ assert(chart.__testHelper);
+
+ var NAMES_ON_INPUT_CHANGE = makeFlexibleNames([
+ 'input', 'on-input', 'change', 'on-change', 'changed', 'on-changed', 'select', 'on-select'
+ ]);
+ var NAMES_ON_CLICK = makeFlexibleNames([
+ 'click', 'on-click'
+ ]);
+ var NAMES_TYPE_BUTTON = makeFlexibleNames(['button', 'btn']);
+ var NAMES_TYPE_RANGE = makeFlexibleNames(['range', 'slider']);
+ var NAMES_TYPE_SELECT = makeFlexibleNames(['select', 'selection']);
+ var NAMES_TYPE_BR = makeFlexibleNames([
+ 'br', 'line-break', 'break', 'wrap', 'new-line', 'end-of-line',
+ 'carriage-return', 'line-feed', 'line-separator', 'next-line'
+ ]);
+ 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'
+ });
+
+ /**
+ * @param {string|Array.<string>?} opt.groupId
+ * @param {string|Array.<string>?} 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});
+ }
+ }
+ }
+ }
+
+ /**
+ * @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;
+ }
+
+ function outputInputsRecord(record) {
+ if (opt.outputType === 'console') {
+ console.log(testHelper.printObject(record, opt.printObjectOpt));
+ }
+ else {
+ testHelper.clipboard(record, opt.printObjectOpt);
+ }
}
- dataTableContainer.innerHTML = tableHTML.join('');
}
- var buttons = opt.buttons || opt.button;
- if (!(buttons instanceof Array)) {
- buttons = buttons ? [buttons] : [];
+ 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: []}.'
+ );
+ }
+ 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);
+ }
+ return listenerDefine.listener.apply(this, arguments);
+ };
+ }
}
- if (buttons.length) {
- for (var i = 0; i < buttons.length; i++) {
- var btnDefine = buttons[i];
- if (btnDefine) {
- var btn = document.createElement('button');
- btn.innerHTML = testHelper.encodeHTML(btnDefine.name || btnDefine.text || 'button');
- btn.addEventListener('click', btnDefine.onClick || btnDefine.onclick);
- buttonsContainer.appendChild(btn);
+
+ function setInputsState(state) {
+ var changedCreatedList = [];
+ for (var id in state) {
+ if (state.hasOwnProperty(id)) {
+ var inputCreated = findInputCreatedAndCheck(id, {log: true});
+ if (!inputCreated) {
+ continue;
+ }
+ if (shouldPrevent(id, NANES_PREVENT_INPUTS_STATE) || !inputCreated.setState) {
+ continue;
+ }
+ inputCreated.setState(state[id]);
+ changedCreatedList.push(inputCreated);
}
}
}
- if (opt.info) {
- updateInfo(opt.info, opt.infoKey);
+ function getInputsState() {
+ var result = {};
+ for (var id in _inputsDict) {
+ 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 updateInfo(info, infoKey) {
- infoContainer.innerHTML = createObjectHTML(info, infoKey || 'option');
+ function restoreInputsToInitialState() {
+ assert(
+ _initStateBackup != null,
+ 'opt.saveInputsInitialState must be true to use `restoreInputsToInitialState`.'
+ );
+ setInputsState(_initStateBackup);
}
- initRecordCanvas(opt, chart, recordCanvasContainer);
+ 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);
+ }
- if (opt.recordVideo) {
- testHelper.createRecordVideo(chart, recordVideoContainer);
+ 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(' ');
}
- chart.__testHelper = {
- updateInfo: updateInfo
- };
+ 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;
+ }
- return chart;
- };
+ 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);
+ }
+ if (!(defineList instanceof Array)) {
+ defineList = defineList ? [defineList] : [];
+ }
+ return defineList;
+ }
+
+ 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]]) {
+ return inputDefine[names[idx]];
+ }
+ }
+ }
+
+ function retrieveId(inputDefine, idPropName) {
+ 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.');
+ }
+ return inputDefine[idPropName] + '';
+ }
+ }
+
+ function createInputByDefine(inputDefine, inputRecorder) {
+ if (!inputDefine) {
+ return;
+ }
+ var inputType = inputDefine.hasOwnProperty('type') ? inputDefine.type : 'button';
+
+ if (arrayIndexOf(NAMES_TYPE_RANGE, inputType) >= 0) {
+ return createRangeInput(inputDefine, null, inputRecorder);
+ }
+ else if (arrayIndexOf(NAMES_TYPE_SELECT, inputType) >= 0) {
+ return createSelectInput(inputDefine, inputRecorder);
+ }
+ else if (arrayIndexOf(NAMES_TYPE_BR, inputType) >= 0) {
+ 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 createButtonInput(inputDefine, inputRecorder);
+ }
+ 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;
+
+ dealInitRangeInput();
+
+ return {
+ 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
+ }));
+ }
+ 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',
+ createRecordArgs: function () {
+ return [+this.value];
+ },
+ prepareReplay: function (recordArgs) {
+ _rangeInputEl.value = recordArgs[0];
+ return {
+ this: _rangeInputEl,
+ arguments: []
+ };
+ }
+ }));
+ _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 + '');
+ }
+ 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, inputRecorder) {
+ var selectCtx = {
+ _optionList: [],
+ _selectWrapperEl: null,
+ _selectEl: null,
+ _optionIdxToSubInput: [],
+ _el: null,
+ _disabled: false,
+ };
+
+ var _SAMPLE_SELECT_DEFINITION = [
+ '{',
+ ' type: "select",',
+ ' text?: "my select:",',
+ ' options: [',
+ ' {text?: string, value: any},',
+ ' {text?: string, input: {type: "range", ...}},',
+ ' ...,',
+ ' ],',
+ ' onchange() { ... },',
+ '}'
+ ].join('\n');
+
+ createSelectInputElements();
+
+ var _selectListener = getBtnEventListener(inputDefine, NAMES_ON_INPUT_CHANGE);
+ 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: []
+ };
+ }
+ }));
+
+ setSelectInputInitValue(inputDefine);
+ resetSelectInputDisabled(inputDefine);
+
+ return {
+ elList: [selectCtx._el],
+ disable: resetSelectInputDisabled,
+ getState: getSelectInputState,
+ setState: setSelectInputState,
+ };
+
+ function createSelectInputElements() {
+ var selectWrapperEl = document.createElement('span');
+ selectCtx._selectWrapperEl = selectWrapperEl;
+ resetSelectInputWrapperCSS(selectWrapperEl, false);
+
+ var textEl = document.createElement('span');
+ textEl.className = 'test-inputs-select-text';
+ textEl.innerHTML = getInputsTextHTML(inputDefine, '');
+ selectWrapperEl.appendChild(textEl);
+
+ var selectEl = document.createElement('select');
+ selectEl.className = 'test-inputs-select-select';
+ selectWrapperEl.appendChild(selectEl);
+
+ selectCtx._el = selectWrapperEl;
+ selectCtx._selectEl = selectEl;
+ }
+
+ 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) {
+ for (var optionIdx = 0; optionIdx < inputDefine.options.length; optionIdx++) {
+ var optionDef = inputDefine.options[optionIdx];
+ 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
+ : makeSelectInputTextByValue(optionDef);
+ selectCtx._optionList.push({
+ value: optionDef.value,
+ input: optionDef.input,
+ text: text
+ });
+ }
+ }
+ else if (inputDefine.values) {
+ for (var optionIdx = 0; optionIdx < inputDefine.values.length; optionIdx++) {
+ var value = inputDefine.values[optionIdx];
+ selectCtx._optionList.push({
+ value: value,
+ text: makeSelectInputTextByValue({value: value})
+ });
+ }
+ }
+ if (!selectCtx._optionList.length) {
+ 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 = 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).
+ optionEl.value = optionIdx;
+ selectCtx._selectEl.appendChild(optionEl);
+
+ if (optionDef.input) {
+ if (arrayIndexOf(NAMES_TYPE_RANGE, optionDef.input.type) < 0) {
+ throw new Error(errMsgPrefix + ' Sub input only supported for range input.');
+ }
+ var rangeInputCreated = createRangeInput(optionDef.input, {
+ text: '',
+ onchange: function () {
+ if (selectCtx._disabled) { return; }
+ triggerUserSelectChangedEvent();
+ }
+ }, inputRecorder);
+ for (var idx = 0; idx < rangeInputCreated.elList.length; idx++) {
+ selectCtx._el.appendChild(rangeInputCreated.elList[idx]);
+ }
+ selectCtx._optionIdxToSubInput[optionIdx] = rangeInputCreated;
+ }
+ }
+ }
+
+ 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 state;
+ }
+
+ 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;
+ }
+
+ var optionStateMap = state.optionStateMap || {};
+ for (var optionIdx in optionStateMap) {
+ if (state.optionStateMap.hasOwnProperty(optionIdx)) {
+ var subInput = selectCtx._optionIdxToSubInput[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 setSelectInputInitValue(inputDefine) {
+ var initOptionIdx = 0;
+ 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[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) {
+ found = true;
+ initOptionIdx = idx;
+ }
+ }
+ if (!found) {
+ throw new Error(errMsgPrefix + ' Value not found in select options: ' + inputDefine.value);
+ }
+ }
+ resetSelectInputOptionIndex(initOptionIdx);
+ }
+
+ function resetSelectInputOptionIndex(optionIdx) {
+ selectCtx._selectEl.value = optionIdx;
+ resetSelectInputSubInputsDisabled();
+ }
+
+ function getSelectInputOptionIndex() {
+ return +selectCtx._selectEl.value;
+ }
+
+ function getSelectInputValueByOptionIndex(optionIdx) {
+ return selectCtx._optionList[optionIdx].input
+ ? selectCtx._optionIdxToSubInput[optionIdx].getState().value
+ : selectCtx._optionList[optionIdx].value;
+ }
+
+ function triggerUserSelectChangedEvent() {
+ var optionIdx = getSelectInputOptionIndex();
+ var value = getSelectInputValueByOptionIndex(optionIdx);
+ var target = {value: value};
+ _selectListener.call(target, {target: target});
+ }
+
+ function resetSelectInputSubInputsDisabled() {
+ var optionIdx = getSelectInputOptionIndex();
+ for (var i = 0; i < selectCtx._optionIdxToSubInput.length; i++) {
+ var subInput = selectCtx._optionIdxToSubInput[i];
+ if (subInput) {
+ var disabled = selectCtx._disabled
+ ? true // Disable all options.
+ : i !== optionIdx // Disable all except current selected option.
+ subInput.disable({disabled: disabled});
+ }
+ }
+ }
+
+ function makeSelectInputTextByValue(optionDef) {
+ if (optionDef.hasOwnProperty('value')) {
+ return printObject(optionDef.value, {
+ arrayLineBreak: false, objectLineBreak: false, indent: 0, lineBreak: ''
+ });
+ }
+ else if (optionDef.input) {
+ 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 {elList: [document.createElement('br')]};
+ }
+
+ 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) {
@@ -239,6 +1585,156 @@
}
}
+ /**
+ * @param {EChartsInstance} chart
+ * @param {Parameter<testHelper.create, 2>['boundingRect']} opt.boundingRect
+ */
+ function initShowBoundingRects(chart, echarts, opt, boundingRectsContainer) {
+ assert(chart.__testHelper);
+
+ var _bRectZr;
+ var _bRectGroup;
+ // @type Parameter<testHelper.create, 2>['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';
@@ -250,7 +1746,7 @@
button.onclick = function () {
isRecording ? recorder.stop() : recorder.start();
- button.innerHTML = `${isRecording ? 'Start' : 'Stop'} Recording`;
+ button.innerHTML = (isRecording ? 'Start' : 'Stop') + ' Recording';
isRecording = !isRecording;
}
@@ -269,8 +1765,9 @@
* @param {number} opt.height
* @param {boolean} opt.draggable
* @param {string} opt.renderer 'canvas' or 'svg'
+ * @param {string} errMsgPrefix
*/
- testHelper.createChart = function (echarts, domOrId, option, opt) {
+ testHelper.createChart = function (echarts, domOrId, option, opt, errMsgPrefix) {
if (typeof opt === 'number') {
opt = {height: opt};
}
@@ -297,7 +1794,7 @@
if (opt.draggable) {
if (!window.draggable) {
throw new Error(
- 'Pleasse add the script in HTML: \n'
+ errMsgPrefix + ' Pleasse add the script in HTML: \n'
+ '<script src="lib/draggable.js"></script>'
);
}
@@ -370,9 +1867,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);
@@ -493,13 +1991,16 @@
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 (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);
@@ -555,7 +2056,7 @@
return '/' + resolvedPath;
};
- testHelper.encodeHTML = function (source) {
+ var encodeHTML = testHelper.encodeHTML = function (source) {
return String(source)
.replace(/&/g, '&')
.replace(/</g, '<')
@@ -564,6 +2065,38 @@
.replace(/'/g, ''');
};
+ var encodeJSObjectKey = function (source, quotationMark) {
+ source = '' + source;
+ if (!/^[a-zA-Z$_][a-zA-Z0-9$_]*$/.test(source)) {
+ source = convertStringToJSLiteral(source, quotationMark);
+ }
+ return source;
+ };
+
+ var convertStringToJSLiteral = function (str, quotationMark) {
+ // assert(getType(str) === 'string');
+ // assert(quotationMark === '"' || quotationMark === "'");
+ str = JSON.stringify(str); // escapse \n\r or others.
+ if (quotationMark === "'") {
+ str = "'" + str.slice(1, str.length - 1).replace(/'/g, "\\'") + "'";
+ }
+ return str;
+ }
+
+ /**
+ * @usage
+ * var result = retrieveValue(val, defaultVal);
+ * var result = retrieveValue(val1, val2, defaultVal);
+ */
+ var retrieveValue = testHelper.retrieveValue = function () {
+ for (var i = 0, len = arguments.length; i < len; i++) {
+ var val = arguments[i];
+ if (val != null) {
+ return val;
+ }
+ }
+ };
+
/**
* @public
* @return {string} Current url dir.
@@ -584,7 +2117,7 @@
return !!TYPED_ARRAY[objToString.call(value)]
? 'typedArray'
- : typeof type === 'function'
+ : typeof value === 'function'
? 'function'
: typeStr === '[object Array]'
? 'array'
@@ -619,11 +2152,14 @@
* @param {*} object
* @param {opt|string} [opt] If string, means key.
* @param {string} [opt.key=''] Top level key, if given, print like: 'someKey: [asdf]'
- * @param {string} [opt.objectLineBreak=true]
- * @param {string} [opt.arrayLineBreak=false]
+ * @param {number} [opt.lineBreakMaxColumn=80] If the content in a single line is greater than
+ * `maxColumn` (indent is not included), line break.
+ * @param {boolean} [opt.objectLineBreak=undefined] Whether to line break. undefined/null means auto.
+ * @param {boolean} [opt.arrayLineBreak=undefined] Whether to line break. undefined/null means auto.
* @param {string} [opt.indent=4]
+ * @param {string} [opt.marginLeft=0] Spaces number for margin left of the entire text.
* @param {string} [opt.lineBreak='\n']
- * @param {string} [opt.quotationMark='\'']
+ * @param {string} [opt.quotationMark="'"] "'" or '"'.
*/
var printObject = testHelper.printObject = function (obj, opt) {
opt = typeof opt === 'string'
@@ -632,16 +2168,28 @@
var indent = opt.indent != null ? opt.indent : 4;
var lineBreak = opt.lineBreak != null ? opt.lineBreak : '\n';
- var quotationMark = opt.quotationMark != null ? opt.quotationMark : '\'';
+ var quotationMark = ({'"': '"', "'": "'"})[opt.quotationMark] || "'";
+ var marginLeft = opt.marginLeft || 0;
+ var lineBreakMaxColumn = opt.lineBreakMaxColumn || 80;
+ var forceObjectLineBreak = opt.objectLineBreak === true || opt.objectLineBreak === false;
+ var forceArrayLineBreak = opt.arrayLineBreak === true || opt.arrayLineBreak === false;
- return doPrint(obj, opt.key, 0).str;
+ return (new Array(marginLeft + 1)).join(' ') + doPrint(obj, opt.key, 0).str;
function doPrint(obj, key, depth) {
- var codeIndent = (new Array(depth * indent + 1)).join(' ');
- var subCodeIndent = (new Array((depth + 1) * indent + 1)).join(' ');
+ var codeIndent = (new Array(depth * indent + marginLeft + 1)).join(' ');
+ var subCodeIndent = (new Array((depth + 1) * indent + marginLeft + 1)).join(' ');
var hasLineBreak = false;
-
- var preStr = key != null ? (key + ': ' ) : '';
+ // [
+ // 11, 22, 33, 44, 55, 66, // This is a partial break.
+ // 77, 88, 99
+ // ]
+ var preventParentArrayPartiallyBreak = false;
+
+ var preStr = '';
+ if (key != null) {
+ preStr += encodeJSObjectKey(key, quotationMark) + ': ';
+ }
var str;
var objType = getType(obj);
@@ -649,7 +2197,10 @@
switch (objType) {
case 'function':
hasLineBreak = true;
- str = preStr + quotationMark + obj + quotationMark;
+ preventParentArrayPartiallyBreak = true;
+ var fnStr = obj.toString();
+ var isMethodShorthand = key != null && isMethodShorthandNotAccurate(fnStr, obj.name, key);
+ str = (isMethodShorthand ? '' : preStr) + fnStr;
break;
case 'regexp':
case 'date':
@@ -657,63 +2208,194 @@
break;
case 'array':
case 'typedArray':
- hasLineBreak = opt.arrayLineBreak != null ? opt.arrayLineBreak : false;
+ if (forceArrayLineBreak) {
+ hasLineBreak = !!opt.arrayLineBreak;
+ }
// If no break line in array, print in single line, like [12, 23, 34].
// else, each item takes a line.
var childBuilder = [];
+ var maxColumnWithoutLineBreak = preStr.length;
+ var canPartiallyBreak = true;
for (var i = 0, len = obj.length; i < len; i++) {
var subResult = doPrint(obj[i], null, 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;
+ canPartiallyBreak = false
+ }
+ }
+ if (obj.length > 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;
+ }
+
};
/**
@@ -733,7 +2415,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;
}
@@ -823,7 +2505,7 @@
*
* @see `stringifyElements`.
*/
- testHelper.printElements = function (chart, opt) {
+ var printElements = testHelper.printElements = function (chart, opt) {
var elsStr = testHelper.stringifyElements(chart, opt);
console.log(elsStr);
};
@@ -843,7 +2525,7 @@
* param: el, return: boolean
* @return {Array.<Element>}
*/
- testHelper.retrieveElements = function (chart, opt) {
+ var retrieveElements = testHelper.retrieveElements = function (chart, opt) {
if (!chart) {
return;
}
@@ -912,6 +2594,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;
@@ -928,7 +2624,7 @@
var htmlLine = ['<tr>'];
for (var j = 0; j < line.length; j++) {
var val = i === dataTableLimit ? '...' : line[j];
- htmlLine.push('<td>' + testHelper.encodeHTML(val) + '</td>');
+ htmlLine.push('<td>' + encodeHTML(val) + '</td>');
}
htmlLine.push('</tr>');
html.push(htmlLine.join(''));
@@ -941,9 +2637,9 @@
for (var key in line) {
if (line.hasOwnProperty(key)) {
var keyText = i === dataTableLimit ? '...' : key;
- htmlLine.push('<td class="test-data-table-key">' + testHelper.encodeHTML(keyText) + '</td>');
+ htmlLine.push('<td class="test-data-table-key">' + encodeHTML(keyText) + '</td>');
var val = i === dataTableLimit ? '...' : line[key];
- htmlLine.push('<td>' + testHelper.encodeHTML(val) + '</td>');
+ htmlLine.push('<td>' + encodeHTML(val) + '</td>');
}
}
htmlLine.push('</tr>');
@@ -953,12 +2649,12 @@
else if (sourceFormat === 'keyedColumns') {
for (var key in data) {
var htmlLine = ['<tr>'];
- htmlLine.push('<td class="test-data-table-key">' + testHelper.encodeHTML(key) + '</td>');
+ htmlLine.push('<td class="test-data-table-key">' + encodeHTML(key) + '</td>');
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('<td>' + testHelper.encodeHTML(val) + '</td>');
+ htmlLine.push('<td>' + encodeHTML(val) + '</td>');
}
}
htmlLine.push('</tr>');
@@ -994,7 +2690,7 @@
function createObjectHTML(obj, key) {
var html = isObject(obj)
- ? testHelper.encodeHTML(printObject(obj, key))
+ ? encodeHTML(printObject(obj, key))
: obj
? obj.toString()
: '';
@@ -1051,6 +2747,217 @@
return type === 'function' || (!!value && type === 'object');
}
+ function arrayIndexOf(arr, value) {
+ if (arr.indexOf) {
+ return arr.indexOf(value);
+ }
+ for (var i = 0; i < arr.length; i++) {
+ if (arr[i] === value) {
+ return i;
+ }
+ }
+ 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++) {
+ var name = dashedNames[i];
+ var tmpNames = [];
+ tmpNames.push(name);
+ tmpNames.push(name.replace(/-/g, ''));
+ tmpNames.push(name.replace(/-/g, '_'));
+ tmpNames.push(name.replace(/-([a-zA-Z0-9])/g, function (_, wf) {
+ return wf.toUpperCase();
+ }));
+ for (var j = 0; j < tmpNames.length; j++) {
+ nameMap[tmpNames[j]] = 1;
+ nameMap[tmpNames[j].toUpperCase()] = 1;
+ nameMap[tmpNames[j].toLowerCase()] = 1;
+ }
+ }
+ var names = [];
+ for (var name in nameMap) {
+ if (nameMap.hasOwnProperty(name)) {
+ names.push(name);
+ }
+ }
+ 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;
+ }
+ for (var key in obj) {
+ if (obj.hasOwnProperty(key) && arrayIndexOf(exceptProps, key) < 0 && obj[key] != null) {
+ return false;
+ }
+ }
+ 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 generateNonPersistentId(prefix) {
+ return (prefix || '') + '' + (_idBase++);
+ }
+
function VideoRecorder(chart) {
this.start = startRecording;
this.stop = stopRecording;
@@ -1115,4 +3022,4 @@
context.testHelper = testHelper;
-})(window);
\ No newline at end of file
+})(window);
diff --git a/test/tmp-base.html b/test/tmp-base.html
index 8c9f5eac2..f81ce9ef5 100644
--- a/test/tmp-base.html
+++ b/test/tmp-base.html
@@ -28,34 +28,123 @@ under the License.
<script src="lib/jquery.min.js"></script>
<script src="lib/facePrint.js"></script>
<script src="lib/testHelper.js"></script>
- <!-- <script src="ut/lib/canteen.js"></script> -->
+ <!-- <script src="lib/canteen.js"></script> -->
+ <!-- <script src="lib/draggable.js"></script> -->
<link rel="stylesheet" href="lib/reset.css" />
</head>
<body>
<style>
+ html {
+ /* Fix the line-height to integer to avoid it varying across clients and
+ causing visual test failures. Some clients may not support fractional px. */
+ line-height: 18px;
+ }
</style>
- <div id="main0"></div>
+ <div id="{{TPL_DOM_ID}}"></div>
+ <!-- <div id="main_some_other_case_1"></div> -->
+ <!-- <div id="main_some_other_case_2"></div> -->
<script>
- var option;
-
require([
- 'echarts'/*, 'map/js/china' */
- ], function (echarts) {
+ 'echarts',
+ // 'data/life-expectancy-table.json'
+ // 'theme/dark.js', // auto register if load.
+ ], function (echarts /*, data */) {
- // $.getJSON('./data/nutrients.json', function (data) {
- // });
+ var option = {
+ xAxis: {},
+ yAxis: {},
+ series: {
+ type: 'scatter',
+ symbolSize: 50,
+ label: {show: true, position: 'top'},
+ data: [[1, 2], [100, 200], [500, 50]]
+ }
+ };
- var chart = testHelper.create(echarts, 'main0', {
+ var chart = testHelper.create(echarts, '{{TPL_DOM_ID}}', {
+ title: [
+ 'A sample **test case** title',
+ 'multiple lines',
+ ],
option: option,
- // recordCanvas: true
- });
- });
+ //
+ // -------------------------- Optional settings: --------------------------
+ // height: 400, // Optional. Specify a different chart height.
+ // draggable: true, // Optional. Add a draggable button to mutify the chart size.
+ // // This feature require "test/lib/draggable.js"
+ // recordCanvas: true, // Optional. Record canvas instructions. (for debug)
+ // // This feature requires "test/lib/canteen.js"
+ // boundingRect: true, // Optional. Show boundingRects of zrender elements (for debug).
+ //
+ // ------------------- Inputs (button/range/select/br/hr): ----------------
+ // inputsHeight: 30, // Optional. Fix the height of inputs area (scrollable if overflow)
+ inputsStyle: 'compact', // Optional.
+ inputs: [ // Optional. The following are sample inputs:
+ {
+ type: 'select',
+ text: '(sample) boundingRect:',
+ values: [false, true, undefined, {color: 'rgba(255,0,0,0.8)', silent: false}],
+ onchange: function () {
+ chart.__testHelper.boundingRect(this.value);
+ }
+ },
+ {
+ type: 'range',
+ text: '(sample) symbolSize:',
+ // min: -100, // Optional.
+ // max: 100, // Optional.
+ // value: 50, // Optional.
+ onchange: function () {
+ console.log('range changed:', this.value);
+ chart.setOption({series: {symbolSize: this.value}});
+ }
+ },
+ {
+ type: 'select',
+ text: '(sample range embedded select) grid.left:',
+ options: [
+ {value: undefined},
+ {value: 30},
+ {input: {type: 'range', min: -300, max: 300, value: 50}}
+ ],
+ onchange: function () {
+ var newVal = this.value;
+ console.log('select 2 changed:', newVal);
+ chart.setOption({grid: {left: newVal}});
+ }
+ },
+ {
+ type: 'br', // line break
+ },
+ {
+ text: '(sample) print failures to screen',
+ onclick: function () {
+ testHelper.printAssert('{{TPL_DOM_ID}}', function (assert) {
+ assert(true);
+ });
+ }
+ },
+ {
+ text: '(sample) copy option to clipboard',
+ onclick: function () {
+ // console.log(testHelper.printObject(option));
+ testHelper.clipboard(option);
+ }
+ }
+
+ ] // End of `inputs`
+
+ }); // End of `testHelper.create`
+
+ }); // End of `require`
+
</script>
+
</body>
</html>
\ No newline at end of file
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@echarts.apache.org
For additional commands, e-mail: commits-help@echarts.apache.org
(echarts) 02/04: test: manually sync visual test updates.
Posted by su...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
sushuang pushed a commit to branch feat-matrix-enhance
in repository https://gitbox.apache.org/repos/asf/echarts.git
commit 139206be7adab50f0d79e4d8adf3d14bac15b1bb
Author: 100pah <su...@gmail.com>
AuthorDate: Sat May 24 18:35:27 2025 +0800
test: manually sync visual test updates.
---
test/runTest/cli.js | 117 +-
test/runTest/client/client.css | 1 +
test/runTest/client/client.js | 192 ++-
test/runTest/client/index.html | 54 +-
test/runTest/compareScreenshot.js | 11 +-
test/runTest/package-lock.json | 2369 ++++++++++++++++++++++++--------
test/runTest/package.json | 12 +-
test/runTest/recorder/index.html | 2 +-
test/runTest/recorder/recorder.css | 5 +
test/runTest/recorder/recorder.js | 2 +-
test/runTest/runtime/ActionPlayback.js | 30 +-
test/runTest/runtime/main.js | 24 +-
test/runTest/server.js | 106 +-
test/runTest/store.js | 76 +-
test/runTest/util.js | 125 +-
15 files changed, 2340 insertions(+), 786 deletions(-)
diff --git a/test/runTest/cli.js b/test/runTest/cli.js
index 7b7755fda..cbc40ac5a 100644
--- a/test/runTest/cli.js
+++ b/test/runTest/cli.js
@@ -39,7 +39,9 @@ program
.option('--no-headless', 'Not headless')
.option('-s, --speed <speed>', 'Playback speed')
.option('--expected <expected>', 'Expected version')
+ .option('--expected-source <expectedSource>', 'Expected source')
.option('--actual <actual>', 'Actual version')
+ .option('--actual-source <actualSource>', 'Actual source')
.option('--renderer <renderer>', 'svg/canvas renderer')
.option('--use-coarse-pointer <useCoarsePointer>', '"auto" (by default) or "true" or "false"')
.option('--threads <threads>', 'How many threads to run concurrently')
@@ -78,12 +80,12 @@ function getClientRelativePath(absPath) {
return path.join('../', path.relative(__dirname, absPath));
}
-function replaceEChartsVersion(interceptedRequest, version) {
+function replaceEChartsVersion(interceptedRequest, source, version) {
// TODO Extensions and maps
if (interceptedRequest.url().endsWith('dist/echarts.js')) {
- console.log('Use echarts version: ' + version);
+ console.log('Use echarts version: ' + source + ' ' + version);
interceptedRequest.continue({
- url: `${origin}/test/runTest/${getVersionDir(version)}/${getEChartsTestFileName()}`
+ url: `${origin}/test/runTest/${getVersionDir(source, version)}/${getEChartsTestFileName()}`
});
}
else {
@@ -117,9 +119,9 @@ async function takeScreenshot(page, fullPage, fileUrl, desc, isExpected, minor)
if (minor) {
screenshotName += '-' + minor;
}
- let screenshotPrefix = isExpected ? 'expected' : 'actual';
+ const screenshotPrefix = isExpected ? 'expected' : 'actual';
fse.ensureDirSync(getScreenshotDir());
- let screenshotPath = path.join(getScreenshotDir(), `${screenshotName}-${screenshotPrefix}.png`);
+ const screenshotPath = path.join(getScreenshotDir(), `${screenshotName}-${screenshotPrefix}.png`);
await page.screenshot({
path: screenshotPath,
// https://github.com/puppeteer/puppeteer/issues/7043
@@ -128,11 +130,18 @@ async function takeScreenshot(page, fullPage, fileUrl, desc, isExpected, minor)
fullPage
});
- const webpScreenshotPath = await convertToWebP(screenshotPath);
+ let webpScreenshotPath;
+ try {
+ webpScreenshotPath = await convertToWebP(screenshotPath);
+ } catch (e) {
+ console.error('Failed to convert screenshot to webp', e);
+ }
+
+ console.log('Screenshot: ', webpScreenshotPath || screenshotPath);
return {
screenshotName,
- screenshotPath: webpScreenshotPath,
+ screenshotPath: webpScreenshotPath || screenshotPath,
rawScreenshotPath: screenshotPath
};
}
@@ -155,18 +164,20 @@ async function waitForNetworkIdle(page) {
page.off('requestfailed', ended);
page.off('requestfinished', ended);
};
- }
-
+}
-async function runTestPage(browser, testOpt, version, runtimeCode, isExpected) {
+/**
+ * @param {puppeteer.Browser} browser
+ */
+async function runTestPage(browser, testOpt, source, version, runtimeCode, isExpected) {
const fileUrl = testOpt.fileUrl;
const screenshots = [];
const logs = [];
- const errors = [];
+ const errors = []; // string[]
const page = await browser.newPage();
page.setRequestInterception(true);
- page.on('request', request => replaceEChartsVersion(request, version));
+ page.on('request', request => replaceEChartsVersion(request, source, version));
async function pageScreenshot() {
if (!program.save) {
@@ -196,14 +207,36 @@ async function runTestPage(browser, testOpt, version, runtimeCode, isExpected) {
await page.exposeFunction('__VRT_MOUSE_MOVE__', async (x, y) => {
await page.mouse.move(x, y);
});
- await page.exposeFunction('__VRT_MOUSE_DOWN__', async () => {
- await page.mouse.down();
+ await page.exposeFunction('__VRT_MOUSE_DOWN__', async (errMsgPart) => {
+ try {
+ await page.mouse.down();
+ }
+ catch (err) {
+ // e.g., if double mousedown without a mouseup, error "'left' is already pressed." will be thrown.
+ // Report to users to re-record the test case.
+ if (errMsgPart) {
+ if ((err.message + '').indexOf('already pressed') >= 0) {
+ errMsgPart += ' May be caused by duplicated mousedowns without a mouseup.'
+ + ' Please re-record the test case.';
+ }
+ err.message = err.message + ' ' + errMsgPart;
+ }
+ throw err;
+ }
});
- await page.exposeFunction('__VRT_MOUSE_UP__', async () => {
- await page.mouse.up();
+ await page.exposeFunction('__VRT_MOUSE_UP__', async (errMsgPart) => {
+ try {
+ await page.mouse.up();
+ }
+ catch (err) {
+ if (errMsgPart) {
+ err.message = err.message + ' ' + errMsgPart;
+ }
+ throw err;
+ }
});
- await page.exposeFunction('__VRT_LOAD_ERROR__', async (err) => {
- errors.push(err);
+ await page.exposeFunction('__VRT_LOAD_ERROR__', async (errStr) => {
+ errors.push(errStr);
});
// await page.exposeFunction('__VRT_WAIT_FOR_NETWORK_IDLE__', async () => {
// await waitForNetworkIdle();
@@ -223,8 +256,8 @@ async function runTestPage(browser, testOpt, version, runtimeCode, isExpected) {
});
});
- page.exposeFunction('__VRT_LOG_ERRORS__', (err) => {
- errors.push(err);
+ page.exposeFunction('__VRT_LOG_ERRORS__', (errStr) => {
+ errors.push(errStr);
});
let actionScreenshotCount = {};
@@ -265,11 +298,12 @@ async function runTestPage(browser, testOpt, version, runtimeCode, isExpected) {
try {
await page.setViewport({
width: 800,
- height: 600
+ height: 600,
});
await page.goto(`${origin}/test/${fileUrl}?__RENDERER__=${program.renderer}&__COARSE__POINTER__=${program.useCoarsePointer}`, {
waitUntil: 'networkidle2',
- timeout: 10000
+ timeout: 10000,
+ // timeout: 0
});
if (!vstInited) { // Not using simpleRequire in the test
@@ -316,20 +350,24 @@ async function runTestPage(browser, testOpt, version, runtimeCode, isExpected) {
};
}
-async function writePNG(diffPNG, diffPath) {
- return new Promise(resolve => {
- let writer = fs.createWriteStream(diffPath);
+function writePNG(diffPNG, diffPath) {
+ return new Promise((resolve, reject) => {
+ const writer = fs.createWriteStream(diffPath);
diffPNG.pack().pipe(writer);
- writer.on('finish', () => {resolve();});
+ writer.on('finish', resolve);
+ writer.on('error', reject);
});
};
-async function runTest(browser, testOpt, runtimeCode, expectedVersion, actualVersion) {
+/**
+ * @param {puppeteer.Browser} browser
+ */
+async function runTest(browser, testOpt, runtimeCode, expectedSource, expectedVersion, actualSource, actualVersion) {
if (program.save) {
testOpt.status === 'running';
- const expectedResult = await runTestPage(browser, testOpt, expectedVersion, runtimeCode, true);
- const actualResult = await runTestPage(browser, testOpt, actualVersion, runtimeCode, false);
+ const expectedResult = await runTestPage(browser, testOpt, expectedSource, expectedVersion, runtimeCode, true);
+ const actualResult = await runTestPage(browser, testOpt, actualSource, actualVersion, runtimeCode, false);
// sortScreenshots(expectedResult.screenshots);
// sortScreenshots(actualResult.screenshots);
@@ -353,9 +391,15 @@ async function runTest(browser, testOpt, runtimeCode, expectedVersion, actualVer
const diffPath = `${getScreenshotDir()}/${shot.screenshotName}-diff.png`;
await writePNG(diffPNG, diffPath);
- const diffWebpPath = await convertToWebP(diffPath);
- result.diff = getClientRelativePath(diffWebpPath);
+ let diffWebpPath;
+ try {
+ diffWebpPath = await convertToWebP(diffPath);
+ } catch (e) {
+ console.error('Failed to convert diff png to webp', e);
+ }
+
+ result.diff = getClientRelativePath(diffWebpPath || diffPath);
result.diffRatio = diffRatio;
// Remove png files
@@ -363,7 +407,7 @@ async function runTest(browser, testOpt, runtimeCode, expectedVersion, actualVer
await Promise.all([
fse.unlink(actual.rawScreenshotPath),
fse.unlink(expected.rawScreenshotPath),
- fse.unlink(diffPath)
+ diffWebpPath && fse.unlink(diffPath)
]);
}
catch (e) {}
@@ -391,7 +435,7 @@ async function runTest(browser, testOpt, runtimeCode, expectedVersion, actualVer
}
else {
// Only run once
- await runTestPage(browser, testOpt, 'local', runtimeCode, true);
+ await runTestPage(browser, testOpt, expectedSource, 'local', runtimeCode, true);
}
}
@@ -412,7 +456,7 @@ async function runTests(pendingTests) {
async function eachTask(testOpt) {
console.log(`Running test: ${testOpt.name}, renderer: ${program.renderer}, useCoarsePointer: ${program.useCoarsePointer}`);
try {
- await runTest(browser, testOpt, runtimeCode, program.expected, program.actual);
+ await runTest(browser, testOpt, runtimeCode, program.expectedSource, program.expected, program.actualSource, program.actual);
}
catch (e) {
// Restore status
@@ -437,7 +481,7 @@ async function runTests(pendingTests) {
}
}
catch(e) {
- console.log(e);
+ console.error(e);
}
@@ -455,4 +499,5 @@ runTests(program.tests.split(',').map(testName => {
expectedErrors: [],
status: 'pending'
};
-}));
\ No newline at end of file
+}));
+
diff --git a/test/runTest/client/client.css b/test/runTest/client/client.css
index 1cce88083..63ec88c42 100644
--- a/test/runTest/client/client.css
+++ b/test/runTest/client/client.css
@@ -277,6 +277,7 @@
.test-errors, .test-logs {
margin-top: 20px;
padding: 0 20px;
+ word-wrap: break-word;
}
.test-logs .log-item {
diff --git a/test/runTest/client/client.js b/test/runTest/client/client.js
index 0c34e0ff0..9241f39ed 100644
--- a/test/runTest/client/client.js
+++ b/test/runTest/client/client.js
@@ -21,6 +21,8 @@ const socket = io('/client');
// const LOCAL_SAVE_KEY = 'visual-regression-testing-config';
+let handlingSourceChange = false;
+
function getChangedObject(target, source) {
let changedObject = {};
Object.keys(source).forEach(key => {
@@ -83,9 +85,15 @@ function processTestsData(tests, oldTestsData) {
// Keep select status not change.
if (oldTestsData && oldTestsData[idx]) {
test.selected = oldTestsData[idx].selected;
+ // Keep source information
+ test.expectedSource = oldTestsData[idx].expectedSource;
+ test.actualSource = oldTestsData[idx].actualSource;
}
else {
test.selected = false;
+ // Initialize source information
+ test.expectedSource = app.runConfig.expectedSource;
+ test.actualSource = app.runConfig.actualSource;
}
});
return tests;
@@ -101,6 +109,22 @@ try {
}
catch (e) {}
+function getVersionFromSource(source, versions, nightlyVersions) {
+ if (source === 'PR') {
+ // Default PR version can be empty since it needs to be manually selected
+ return '#';
+ }
+ else if (source === 'nightly') {
+ return nightlyVersions.length ? nightlyVersions[0] : null;
+ }
+ else if (source === 'local') {
+ return 'local';
+ }
+ else {
+ return versions.length ? versions[0] : null;
+ }
+}
+
const app = new Vue({
el: '#app',
data: {
@@ -112,9 +136,6 @@ const app = new Vue({
allSelected: false,
lastSelectedIndex: -1,
- expectedVersionsList: [],
- actualVersionsList: [],
-
loadingVersion: false,
showIframeDialog: false,
@@ -128,21 +149,27 @@ const app = new Vue({
pageInvisible: false,
+ versions: [],
+ nightlyVersions: [],
+ prVersions: [],
+ branchVersions: [],
+
runConfig: Object.assign({
sortBy: 'name',
-
- isActualNightly: false,
- isExpectedNightly: false,
- actualVersion: 'local',
+ actualVersion: null,
expectedVersion: null,
-
+ expectedSource: 'release',
+ actualSource: 'local',
renderer: 'canvas',
useCoarsePointer: 'auto',
threads: 4
}, urlRunConfig)
},
- mounted() {
+ async mounted() {
+ // Add call to fetch branches
+ await this.fetchBranchVersions();
+
// Sync config from server when first time open
// or switching back
socket.emit('syncRunConfig', {
@@ -151,12 +178,38 @@ const app = new Vue({
forceSet: Object.keys(urlRunConfig).length > 0
});
socket.on('syncRunConfig_return', res => {
- this.expectedVersionsList = res.expectedVersionsList;
- this.actualVersionsList = res.actualVersionsList;
- // Only assign on changed object to avoid unnecessary vue change.
- Object.assign(this.runConfig, getChangedObject(this.runConfig, res.runConfig));
+ this.versions = res.versions || [];
+ this.nightlyVersions = res.nightlyVersions || [];
+ this.prVersions = res.prVersions || [];
+
+ // Only set versions if they haven't been manually set
+ handlingSourceChange = true;
+ this.$nextTick(() => {
+ if (!this.runConfig.expectedVersion) {
+ this.runConfig.expectedVersion = getVersionFromSource(
+ this.runConfig.expectedSource,
+ this.versions,
+ this.nightlyVersions
+ );
+ }
- updateUrl();
+ if (!this.runConfig.actualVersion) {
+ this.runConfig.actualVersion = getVersionFromSource(
+ this.runConfig.actualSource,
+ this.versions,
+ this.nightlyVersions
+ );
+ }
+
+ // Only apply other config changes from server
+ const configWithoutVersions = { ...res.runConfig };
+ delete configWithoutVersions.expectedVersion;
+ delete configWithoutVersions.actualVersion;
+ Object.assign(this.runConfig, getChangedObject(this.runConfig, configWithoutVersions));
+
+ handlingSourceChange = false;
+ updateUrl();
+ });
});
setTimeout(() => {
@@ -172,6 +225,16 @@ const app = new Vue({
this.pageInvisible = true;
}
});
+
+ socket.on('run_error', err => {
+ app.$notify({
+ title: 'Error',
+ message: err.message,
+ type: 'error',
+ duration: 5000
+ });
+ app.running = false;
+ });
},
computed: {
@@ -254,6 +317,36 @@ const app = new Vue({
});
},
set() {}
+ },
+
+ expectedVersionsList() {
+ switch (this.runConfig.expectedSource) {
+ case 'release':
+ return this.versions;
+ case 'nightly':
+ return this.nightlyVersions;
+ case 'PR':
+ return this.prVersions;
+ case 'local':
+ return ['local'];
+ default:
+ return [];
+ }
+ },
+
+ actualVersionsList() {
+ switch (this.runConfig.actualSource) {
+ case 'release':
+ return this.versions;
+ case 'nightly':
+ return this.nightlyVersions;
+ case 'PR':
+ return this.prVersions;
+ case 'local':
+ return ['local'];
+ default:
+ return [];
+ }
}
},
@@ -266,6 +359,44 @@ const app = new Vue({
'currentTestName'(newVal, oldVal) {
updateUrl();
+ },
+
+ 'runConfig.expectedSource': {
+ handler(newVal, oldVal) {
+ if (newVal === oldVal) {
+ return;
+ }
+
+ handlingSourceChange = true;
+ this.$nextTick(() => {
+ this.runConfig.expectedVersion = getVersionFromSource(
+ newVal,
+ this.versions,
+ this.nightlyVersions
+ );
+ handlingSourceChange = false;
+ });
+ },
+ deep: false
+ },
+
+ 'runConfig.actualSource': {
+ handler(newVal, oldVal) {
+ if (newVal === oldVal) {
+ return;
+ }
+
+ handlingSourceChange = true;
+ this.$nextTick(() => {
+ this.runConfig.actualVersion = getVersionFromSource(
+ newVal,
+ this.versions,
+ this.nightlyVersions
+ );
+ handlingSourceChange = false;
+ });
+ },
+ deep: false
}
},
@@ -339,8 +470,12 @@ const app = new Vue({
let searches = [];
let ecVersion = test[version + 'Version'];
+ let ecSource = test[version + 'Source'];
if (ecVersion !== 'local') {
- searches.push('__ECDIST__=' + ecVersion);
+ let distPath = ecSource === 'PR'
+ ? 'pr-' + ecVersion.replace(/^#/, '')
+ : ecVersion;
+ searches.push('__ECDIST__=' + distPath);
}
if (test.useSVG) {
searches.push('__RENDERER__=svg');
@@ -367,8 +502,6 @@ const app = new Vue({
this.runConfig.expectedVersion = runResult.expectedVersion;
this.runConfig.actualVersion = runResult.actualVersion;
// TODO
- this.runConfig.isExpectedNightly = runResult.expectedVersion.includes('-dev.');
- this.runConfig.isActualNightly = runResult.actualVersion.includes('-dev.');
this.runConfig.renderer = runResult.renderer;
this.runConfig.useCoarsePointer = runResult.useCoarsePointer;
@@ -397,6 +530,17 @@ const app = new Vue({
open(url, target) {
window.open(url, target);
+ },
+
+ async fetchBranchVersions() {
+ try {
+ const response = await fetch('https://api.github.com/repos/apache/echarts/branches?per_page=100');
+ const branches = await response.json();
+ this.branchVersions = branches.map(branch => branch.name);
+ } catch (error) {
+ console.error('Failed to fetch branches:', error);
+ this.branchVersions = [];
+ }
}
}
});
@@ -419,7 +563,9 @@ function runTests(tests, noHeadless) {
app.running = true;
socket.emit('run', {
tests,
+ expectedSource: app.runConfig.expectedSource,
expectedVersion: app.runConfig.expectedVersion,
+ actualSource: app.runConfig.actualSource,
actualVersion: app.runConfig.actualVersion,
threads: app.runConfig.threads,
renderer: app.runConfig.renderer,
@@ -498,11 +644,19 @@ function updateUrl() {
// Only update url when version is changed.
app.$watch('runConfig', (newVal, oldVal) => {
- if (!app.pageInvisible) {
+ if (!app.pageInvisible && !handlingSourceChange) {
socket.emit('syncRunConfig', {
runConfig: app.runConfig,
- // Override server config from URL.
forceSet: true
+ }, err => {
+ if (err) {
+ app.$notify({
+ title: 'Error',
+ message: err,
+ type: 'error',
+ duration: 5000
+ });
+ }
});
}
}, { deep: true });
\ No newline at end of file
diff --git a/test/runTest/client/index.html b/test/runTest/client/index.html
index df8cdc9a7..f5270a90a 100644
--- a/test/runTest/client/index.html
+++ b/test/runTest/client/index.html
@@ -1,4 +1,3 @@
-
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
@@ -144,27 +143,58 @@ under the License.
<div class="run-config-item">
<span class="label">
Expected
- <el-tooltip content="Use Nightly Build">
- <el-checkbox :disabled="running" v-model="runConfig.isExpectedNightly" size="mini"></el-checkbox>
- </el-tooltip>
</span>
- <el-select :disabled="running" size="mini" v-model="runConfig.expectedVersion" placeholder="Select Version"
- :style="`width: ${runConfig.isExpectedNightly ? 160 : 80}px;`"
+ <el-select :disabled="running" size="mini" v-model="runConfig.expectedSource" style="width: 90px;">
+ <el-option value="release" label="Release"></el-option>
+ <el-option value="nightly" label="Nightly"></el-option>
+ <el-option value="PR" label="PR"></el-option>
+ <el-option value="local" label="Local"></el-option>
+ </el-select>
+ <el-select
+ v-if="runConfig.expectedSource !== 'local' && runConfig.expectedSource !== 'PR'"
+ :disabled="running"
+ size="mini"
+ v-model="runConfig.expectedVersion"
+ placeholder="Select Version"
+ :style="`width: ${runConfig.expectedSource === 'nightly' ? 160 : 80}px;`"
>
<el-option v-for="version in expectedVersionsList" :key="version" :label="version" :value="version"></el-option>
</el-select>
- <!-- <i class="el-icon-link"></i> -->
+ <el-input
+ v-if="runConfig.expectedSource === 'PR'"
+ :disabled="running"
+ size="mini"
+ v-model="runConfig.expectedVersion"
+ placeholder="#123"
+ style="width: 120px;"
+ ></el-input>
<span class="label">
Actual
- <el-tooltip content="Use Nightly Build">
- <el-checkbox :disabled="running" v-model="runConfig.isActualNightly" size="mini"></el-checkbox>
- </el-tooltip>
</span>
- <el-select :disabled="running" size="mini" v-model="runConfig.actualVersion" placeholder="Select Version"
- :style="`width: ${runConfig.isActualNightly ? 160 : 80}px;`"
+ <el-select :disabled="running" size="mini" v-model="runConfig.actualSource" style="width: 90px;">
+ <el-option value="release" label="Release"></el-option>
+ <el-option value="nightly" label="Nightly"></el-option>
+ <el-option value="PR" label="PR"></el-option>
+ <el-option value="local" label="Local"></el-option>
+ </el-select>
+ <el-select
+ v-if="runConfig.actualSource !== 'local' && runConfig.actualSource !== 'PR'"
+ :disabled="running"
+ size="mini"
+ v-model="runConfig.actualVersion"
+ placeholder="Select Version"
+ :style="`width: ${runConfig.actualSource === 'nightly' ? 160 : 80}px;`"
>
<el-option v-for="version in actualVersionsList" :key="version" :label="version" :value="version"></el-option>
</el-select>
+ <el-input
+ v-if="runConfig.actualSource === 'PR'"
+ :disabled="running"
+ size="mini"
+ v-model="runConfig.actualVersion"
+ placeholder="#123"
+ style="width: 120px;"
+ ></el-input>
</div>
<div class="run-config-item">
<span class="label">Renderer</span>
diff --git a/test/runTest/compareScreenshot.js b/test/runTest/compareScreenshot.js
index f4c1627c3..769c1b739 100644
--- a/test/runTest/compareScreenshot.js
+++ b/test/runTest/compareScreenshot.js
@@ -22,7 +22,7 @@ const pixelmatch = require('pixelmatch');
const fs = require('fs');
function readPNG(path) {
- return new Promise(resolve => {
+ return new Promise((resolve, reject) => {
fs.createReadStream(path)
.pipe(new PNG())
.on('parsed', function () {
@@ -31,7 +31,8 @@ function readPNG(path) {
width: this.width,
height: this.height
});
- });
+ })
+ .on('error', (err) => reject(`failed to read PNG from '${path}'\n` + err));
});
}
@@ -46,7 +47,11 @@ module.exports = function (expectedShotPath, actualShotPath, threshold = 0.01) {
(width !== actualImg.width)
|| (height !== actualImg.height)
) {
- throw new Error('Image size not match');
+ throw new Error(
+ 'Image size not match. '
+ + ' expect: ' + width + 'x' + height
+ + ' actual: ' + actualImg.width + 'x' + actualImg.height
+ );
}
const diffPNG = new PNG({width, height});
let diffPixelsCount = pixelmatch(expectedImg.data, actualImg.data, diffPNG.data, width, height, {threshold});
diff --git a/test/runTest/package-lock.json b/test/runTest/package-lock.json
index cd221ad51..e33450bd8 100644
--- a/test/runTest/package-lock.json
+++ b/test/runTest/package-lock.json
@@ -10,12 +10,73 @@
"devDependencies": {
"cwebp-bin": "^6.1.1",
"glob": "7.0.0",
- "pixelmatch": "5.0.2",
- "pngjs": "3.4.0",
- "puppeteer": "^9.1.1",
- "serve-handler": "6.1.1",
- "slugify": "^1.3.4",
- "socket.io": "2.5.0"
+ "pixelmatch": "<6.0.0",
+ "pngjs": "7.0.0",
+ "puppeteer": "^23.10.0",
+ "serve-handler": "^6.1.6",
+ "slugify": "^1.6.6",
+ "socket.io": "^2.5.1"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.26.2",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
+ "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.25.9",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
+ "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@puppeteer/browsers": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.5.0.tgz",
+ "integrity": "sha512-6TQAc/5uRILE6deixJ1CR8rXyTbzXIXNgO1D0Woi9Bqicz2FV5iKP3BHYEg6o4UATCMcbQQ0jbmeaOkn/HQk2w==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "debug": "^4.3.7",
+ "extract-zip": "^2.0.1",
+ "progress": "^2.0.3",
+ "proxy-agent": "^6.4.0",
+ "semver": "^7.6.3",
+ "tar-fs": "^3.0.6",
+ "unbzip2-stream": "^1.4.3",
+ "yargs": "^17.7.2"
+ },
+ "bin": {
+ "browsers": "lib/cjs/main-cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@puppeteer/browsers/node_modules/semver": {
+ "version": "7.6.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
+ "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
}
},
"node_modules/@sindresorhus/is": {
@@ -27,18 +88,30 @@
"node": ">=4"
}
},
+ "node_modules/@tootallnate/quickjs-emscripten": {
+ "version": "0.23.0",
+ "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz",
+ "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/node": {
- "version": "15.0.2",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-15.0.2.tgz",
- "integrity": "sha512-p68+a+KoxpoB47015IeYZYRrdqMUcpbK8re/zpFB8Ld46LHC1lPEbp3EXgkEhAYEcPvjJF6ZO+869SQ0aH1dcA==",
+ "version": "22.10.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz",
+ "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==",
"dev": true,
- "optional": true
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "undici-types": "~6.20.0"
+ }
},
"node_modules/@types/yauzl": {
- "version": "2.9.1",
- "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.1.tgz",
- "integrity": "sha512-A1b8SU4D10uoPjwb0lnHmmu8wZhR9d+9o2PKBQT2jU5YPTKsxac6M2qGAdY7VcL+dHHhARVUDmeg0rOrcd9EjA==",
+ "version": "2.10.3",
+ "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
+ "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
"dev": true,
+ "license": "MIT",
"optional": true,
"dependencies": {
"@types/node": "*"
@@ -64,15 +137,42 @@
"dev": true
},
"node_modules/agent-base": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
- "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz",
+ "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "debug": "4"
+ "debug": "^4.3.4"
},
"engines": {
- "node": ">= 6.0.0"
+ "node": ">= 14"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/arch": {
@@ -116,12 +216,39 @@
"node": ">=4"
}
},
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
"node_modules/arraybuffer.slice": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz",
"integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==",
"dev": true
},
+ "node_modules/ast-types": {
+ "version": "0.13.4",
+ "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz",
+ "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/b4a": {
+ "version": "1.6.7",
+ "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz",
+ "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
"node_modules/backo2": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
@@ -134,6 +261,57 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
},
+ "node_modules/bare-events": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.0.tgz",
+ "integrity": "sha512-/E8dDe9dsbLyh2qrZ64PEPadOQ0F4gbl1sUJOrmph7xOiIxfY8vwab/4bFLh4Y88/Hk/ujKcrQKc+ps0mv873A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true
+ },
+ "node_modules/bare-fs": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.3.5.tgz",
+ "integrity": "sha512-SlE9eTxifPDJrT6YgemQ1WGFleevzwY+XAP1Xqgl56HtcrisC2CHCZ2tq6dBpcH2TnNxwUEUGhweo+lrQtYuiw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "dependencies": {
+ "bare-events": "^2.0.0",
+ "bare-path": "^2.0.0",
+ "bare-stream": "^2.0.0"
+ }
+ },
+ "node_modules/bare-os": {
+ "version": "2.4.4",
+ "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.4.4.tgz",
+ "integrity": "sha512-z3UiI2yi1mK0sXeRdc4O1Kk8aOa/e+FNWZcTiPB/dfTWyLypuE99LibgRaQki914Jq//yAWylcAt+mknKdixRQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true
+ },
+ "node_modules/bare-path": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.3.tgz",
+ "integrity": "sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "dependencies": {
+ "bare-os": "^2.1.0"
+ }
+ },
+ "node_modules/bare-stream": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.4.2.tgz",
+ "integrity": "sha512-XZ4ln/KV4KT+PXdIWTKjsLY+quqCaEtqqtgGJVPw9AoM73By03ij64YjepK0aQvHSWDb6AfAZwqKaFu68qkrdA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "dependencies": {
+ "streamx": "^2.20.0"
+ }
+ },
"node_modules/base64-arraybuffer": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz",
@@ -172,6 +350,16 @@
"node": "^4.5.0 || >= 5.9"
}
},
+ "node_modules/basic-ftp": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz",
+ "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
"node_modules/bin-build": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bin-build/-/bin-build-3.0.0.tgz",
@@ -229,10 +417,11 @@
}
},
"node_modules/bin-version/node_modules/cross-spawn": {
- "version": "6.0.5",
- "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
- "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
+ "version": "6.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz",
+ "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"nice-try": "^1.0.4",
"path-key": "^2.0.1",
@@ -432,17 +621,6 @@
"node": ">=4"
}
},
- "node_modules/bl": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
- "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
- "dev": true,
- "dependencies": {
- "buffer": "^5.5.0",
- "inherits": "^2.0.4",
- "readable-stream": "^3.4.0"
- }
- },
"node_modules/blob": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz",
@@ -556,6 +734,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/caw": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/caw/-/caw-2.0.1.tgz",
@@ -571,11 +759,35 @@
"node": ">=4"
}
},
- "node_modules/chownr": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
- "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
- "dev": true
+ "node_modules/chromium-bidi": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.8.0.tgz",
+ "integrity": "sha512-uJydbGdTw0DEUjhoogGveneJVWX/9YuqkWePzMmkBYwtdAqo5d3J/ovNKFr+/2hWXYmYCr6it8mSSTIj6SS6Ug==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "mitt": "3.0.1",
+ "urlpattern-polyfill": "10.0.0",
+ "zod": "3.23.8"
+ },
+ "peerDependencies": {
+ "devtools-protocol": "*"
+ }
+ },
+ "node_modules/cliui": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
},
"node_modules/clone-response": {
"version": "1.0.2",
@@ -586,6 +798,26 @@
"mimic-response": "^1.0.0"
}
},
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
@@ -653,6 +885,33 @@
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"dev": true
},
+ "node_modules/cosmiconfig": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz",
+ "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "env-paths": "^2.2.1",
+ "import-fresh": "^3.3.0",
+ "js-yaml": "^4.1.0",
+ "parse-json": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/d-fischer"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.9.5"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
"node_modules/cross-spawn": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
@@ -684,13 +943,24 @@
"url": "https://github.com/imagemin/cwebp-bin?sponsor=1"
}
},
+ "node_modules/data-uri-to-buffer": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz",
+ "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"node_modules/debug": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
- "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "ms": "2.1.2"
+ "ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
@@ -913,11 +1183,27 @@
"node": ">=0.10.0"
}
},
+ "node_modules/degenerator": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz",
+ "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ast-types": "^0.13.4",
+ "escodegen": "^2.1.0",
+ "esprima": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"node_modules/devtools-protocol": {
- "version": "0.0.869402",
- "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.869402.tgz",
- "integrity": "sha512-VvlVYY+VDJe639yHs5PHISzdWTLL3Aw8rO4cvUtwvoxFd6FHbE4OpHHcde52M6096uYYazAmd4l0o5VuFRO2WA==",
- "dev": true
+ "version": "0.0.1367902",
+ "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1367902.tgz",
+ "integrity": "sha512-XxtPuC3PGakY6PD7dG66/o8KwJ/LkH2/EKe19Dcw58w53dv4/vSQEkn/SzuyhHE2q4zPgCkxQBxus3VV4ql+Pg==",
+ "dev": true,
+ "license": "BSD-3-Clause"
},
"node_modules/download": {
"version": "6.2.5",
@@ -965,6 +1251,13 @@
"integrity": "sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==",
"dev": true
},
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/end-of-stream": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
@@ -975,27 +1268,29 @@
}
},
"node_modules/engine.io": {
- "version": "3.6.1",
- "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.6.1.tgz",
- "integrity": "sha512-dfs8EVg/i7QjFsXxn7cCRQ+Wai1G1TlEvHhdYEi80fxn5R1vZ2K661O6v/rezj1FP234SZ14r9CmJke99iYDGg==",
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.6.2.tgz",
+ "integrity": "sha512-C4JjGQZLY3kWlIDx0BQNKizbrfpb7NahxDztGdN5jrPK2ghmXiNDN+E/t0JzDeNRZxPVaszxEng42Pmj27X/0w==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"accepts": "~1.3.4",
"base64id": "2.0.0",
"cookie": "~0.4.1",
"debug": "~4.1.0",
"engine.io-parser": "~2.2.0",
- "ws": "~7.4.2"
+ "ws": "~7.5.10"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/engine.io-client": {
- "version": "3.5.3",
- "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.5.3.tgz",
- "integrity": "sha512-qsgyc/CEhJ6cgMUwxRRtOndGVhIu5hpL5tR4umSpmX/MvkFoIxUTM7oFMDQumHNzlNLwSVy6qhstFPoWTf7dOw==",
+ "version": "3.5.4",
+ "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.5.4.tgz",
+ "integrity": "sha512-ydc8uuMMDxC5KCKNJN3zZKYJk2sgyTuTZQ7Aj1DJSsLKAcizA/PzWivw8fZMIjJVBo2CJOYzntv4FSjY/Lr//g==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"component-emitter": "~1.3.0",
"component-inherit": "0.0.3",
@@ -1005,7 +1300,7 @@
"indexof": "0.0.1",
"parseqs": "0.0.6",
"parseuri": "0.0.6",
- "ws": "~7.4.2",
+ "ws": "~7.5.10",
"xmlhttprequest-ssl": "~1.6.2",
"yeast": "0.1.2"
}
@@ -1048,6 +1343,36 @@
"ms": "^2.1.1"
}
},
+ "node_modules/env-paths": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
+ "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/error-ex": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
+ "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
@@ -1057,6 +1382,62 @@
"node": ">=0.8.0"
}
},
+ "node_modules/escodegen": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz",
+ "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esprima": "^4.0.1",
+ "estraverse": "^5.2.0",
+ "esutils": "^2.0.2"
+ },
+ "bin": {
+ "escodegen": "bin/escodegen.js",
+ "esgenerate": "bin/esgenerate.js"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "optionalDependencies": {
+ "source-map": "~0.6.1"
+ }
+ },
+ "node_modules/esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "bin": {
+ "esparse": "bin/esparse.js",
+ "esvalidate": "bin/esvalidate.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/execa": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz",
@@ -1135,6 +1516,7 @@
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
"integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
"dev": true,
+ "license": "BSD-2-Clause",
"dependencies": {
"debug": "^4.1.1",
"get-stream": "^5.1.0",
@@ -1150,14 +1532,12 @@
"@types/yauzl": "^2.9.1"
}
},
- "node_modules/fast-url-parser": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/fast-url-parser/-/fast-url-parser-1.1.3.tgz",
- "integrity": "sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ==",
+ "node_modules/fast-fifo": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
+ "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
"dev": true,
- "dependencies": {
- "punycode": "^1.3.2"
- }
+ "license": "MIT"
},
"node_modules/fd-slicer": {
"version": "1.1.0",
@@ -1200,19 +1580,6 @@
"node": ">=4"
}
},
- "node_modules/find-up": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
- "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
- "dev": true,
- "dependencies": {
- "locate-path": "^5.0.0",
- "path-exists": "^4.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/find-versions": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/find-versions/-/find-versions-3.2.0.tgz",
@@ -1277,11 +1644,15 @@
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
"dev": true
},
- "node_modules/fs.realpath": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
- "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
- "dev": true
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
},
"node_modules/get-proxy": {
"version": "2.1.0",
@@ -1300,6 +1671,7 @@
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
"integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"pump": "^3.0.0"
},
@@ -1310,6 +1682,21 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/get-uri": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.4.tgz",
+ "integrity": "sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "basic-ftp": "^5.0.2",
+ "data-uri-to-buffer": "^6.0.2",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"node_modules/glob": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.0.0.tgz",
@@ -1408,17 +1795,32 @@
"integrity": "sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w==",
"dev": true
},
+ "node_modules/http-proxy-agent": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"node_modules/https-proxy-agent": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz",
- "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==",
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz",
+ "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "agent-base": "6",
+ "agent-base": "^7.0.2",
"debug": "4"
},
"engines": {
- "node": ">= 6"
+ "node": ">= 14"
}
},
"node_modules/ieee754": {
@@ -1441,6 +1843,23 @@
}
]
},
+ "node_modules/import-fresh": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
+ "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/import-lazy": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-3.1.0.tgz",
@@ -1491,6 +1910,37 @@
"node": ">=4"
}
},
+ "node_modules/ip-address": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
+ "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "jsbn": "1.1.0",
+ "sprintf-js": "^1.1.3"
+ },
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/is-arrayish": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/is-natural-number": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz",
@@ -1558,13 +2008,47 @@
"node": ">= 4"
}
},
- "node_modules/json-buffer": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz",
- "integrity": "sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==",
- "dev": true
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true,
+ "license": "MIT"
},
- "node_modules/keyv": {
+ "node_modules/js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsbn": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
+ "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz",
+ "integrity": "sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==",
+ "dev": true
+ },
+ "node_modules/json-parse-even-better-errors": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/keyv": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-3.0.0.tgz",
"integrity": "sha512-eguHnq22OE3uVoSYG0LVWNP+4ppamWr9+zWBe1bsNcovIMy6huUJFPgy4mGwCd/rnl3vOLGW1MTlu4c57CT1xA==",
@@ -1573,17 +2057,12 @@
"json-buffer": "3.0.0"
}
},
- "node_modules/locate-path": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
- "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"dev": true,
- "dependencies": {
- "p-locate": "^4.1.0"
- },
- "engines": {
- "node": ">=8"
- }
+ "license": "MIT"
},
"node_modules/lowercase-keys": {
"version": "1.0.1",
@@ -1667,17 +2146,19 @@
"node": "*"
}
},
- "node_modules/mkdirp-classic": {
- "version": "0.5.3",
- "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
- "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
- "dev": true
+ "node_modules/mitt": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
+ "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
+ "dev": true,
+ "license": "MIT"
},
"node_modules/ms": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
- "dev": true
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
},
"node_modules/negotiator": {
"version": "0.6.3",
@@ -1688,32 +2169,22 @@
"node": ">= 0.6"
}
},
+ "node_modules/netmask": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz",
+ "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
"node_modules/nice-try": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
"dev": true
},
- "node_modules/node-fetch": {
- "version": "2.6.11",
- "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.11.tgz",
- "integrity": "sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==",
- "dev": true,
- "dependencies": {
- "whatwg-url": "^5.0.0"
- },
- "engines": {
- "node": "4.x || >=6.0.0"
- },
- "peerDependencies": {
- "encoding": "^0.1.0"
- },
- "peerDependenciesMeta": {
- "encoding": {
- "optional": true
- }
- }
- },
"node_modules/normalize-url": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-2.0.1.tgz",
@@ -1852,33 +2323,6 @@
"node": ">=4"
}
},
- "node_modules/p-limit": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
- "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
- "dev": true,
- "dependencies": {
- "p-try": "^2.0.0"
- },
- "engines": {
- "node": ">=6"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/p-locate": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
- "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
- "dev": true,
- "dependencies": {
- "p-limit": "^2.2.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/p-map-series": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-map-series/-/p-map-series-1.0.0.tgz",
@@ -1912,15 +2356,72 @@
"node": ">=4"
}
},
- "node_modules/p-try": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
- "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+ "node_modules/pac-proxy-agent": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.2.tgz",
+ "integrity": "sha512-BFi3vZnO9X5Qt6NRz7ZOaPja3ic0PhlsmCRYLOpN11+mWBCR6XJDqW5RF3j8jm4WGGQZtBA+bTfxYzeKW73eHg==",
"dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@tootallnate/quickjs-emscripten": "^0.23.0",
+ "agent-base": "^7.0.2",
+ "debug": "^4.3.4",
+ "get-uri": "^6.0.1",
+ "http-proxy-agent": "^7.0.0",
+ "https-proxy-agent": "^7.0.5",
+ "pac-resolver": "^7.0.1",
+ "socks-proxy-agent": "^8.0.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/pac-resolver": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz",
+ "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "degenerator": "^5.0.0",
+ "netmask": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
"engines": {
"node": ">=6"
}
},
+ "node_modules/parse-json": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
+ "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.0.0",
+ "error-ex": "^1.3.1",
+ "json-parse-even-better-errors": "^2.3.0",
+ "lines-and-columns": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/parseqs": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz",
@@ -1933,15 +2434,6 @@
"integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==",
"dev": true
},
- "node_modules/path-exists": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
- "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
- "dev": true,
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
@@ -1967,10 +2459,11 @@
}
},
"node_modules/path-to-regexp": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-2.2.1.tgz",
- "integrity": "sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ==",
- "dev": true
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz",
+ "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==",
+ "dev": true,
+ "license": "MIT"
},
"node_modules/pend": {
"version": "1.2.0",
@@ -1978,6 +2471,13 @@
"integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=",
"dev": true
},
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/pify": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
@@ -2009,36 +2509,36 @@
}
},
"node_modules/pixelmatch": {
- "version": "5.0.2",
- "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-5.0.2.tgz",
- "integrity": "sha512-b65UpTI40rGFY8QwN6IYuCbpmwAOL6M8d6voX4F3zR99UmDqh7r2QWLxoeHOazBRgEmDUdqNVESDREqFxQS7rQ==",
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-5.3.0.tgz",
+ "integrity": "sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q==",
"dev": true,
+ "license": "ISC",
"dependencies": {
- "pngjs": "^3.4.0"
+ "pngjs": "^6.0.0"
},
"bin": {
"pixelmatch": "bin/pixelmatch"
}
},
- "node_modules/pkg-dir": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
- "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
+ "node_modules/pixelmatch/node_modules/pngjs": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz",
+ "integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==",
"dev": true,
- "dependencies": {
- "find-up": "^4.0.0"
- },
+ "license": "MIT",
"engines": {
- "node": ">=8"
+ "node": ">=12.13.0"
}
},
"node_modules/pngjs": {
- "version": "3.4.0",
- "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz",
- "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz",
+ "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==",
"dev": true,
+ "license": "MIT",
"engines": {
- "node": ">=4.0.0"
+ "node": ">=14.19.0"
}
},
"node_modules/prepend-http": {
@@ -2061,6 +2561,7 @@
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=0.4.0"
}
@@ -2071,11 +2572,42 @@
"integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==",
"dev": true
},
+ "node_modules/proxy-agent": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz",
+ "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.0.2",
+ "debug": "^4.3.4",
+ "http-proxy-agent": "^7.0.1",
+ "https-proxy-agent": "^7.0.3",
+ "lru-cache": "^7.14.1",
+ "pac-proxy-agent": "^7.0.1",
+ "proxy-from-env": "^1.1.0",
+ "socks-proxy-agent": "^8.0.2"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/proxy-agent/node_modules/lru-cache": {
+ "version": "7.18.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
+ "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/pseudomap": {
"version": "1.0.2",
@@ -2093,34 +2625,66 @@
"once": "^1.3.1"
}
},
- "node_modules/punycode": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
- "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==",
- "dev": true
- },
"node_modules/puppeteer": {
- "version": "9.1.1",
- "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-9.1.1.tgz",
- "integrity": "sha512-W+nOulP2tYd/ZG99WuZC/I5ljjQQ7EUw/jQGcIb9eu8mDlZxNY2SgcJXTLG9h5gRvqA3uJOe4hZXYsd3EqioMw==",
+ "version": "23.10.0",
+ "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-23.10.0.tgz",
+ "integrity": "sha512-vLpEvUM8POKBX4j6y/yyD4oGRhS1oBAbMn3lYpz1yeakhRsA8IhF5QmjXwAIQYGdR/INxyFiY6kQDoUtLGDP3g==",
"dev": true,
"hasInstallScript": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@puppeteer/browsers": "2.5.0",
+ "chromium-bidi": "0.8.0",
+ "cosmiconfig": "^9.0.0",
+ "devtools-protocol": "0.0.1367902",
+ "puppeteer-core": "23.10.0",
+ "typed-query-selector": "^2.12.0"
+ },
+ "bin": {
+ "puppeteer": "lib/cjs/puppeteer/node/cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/puppeteer-core": {
+ "version": "23.10.0",
+ "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-23.10.0.tgz",
+ "integrity": "sha512-7pv6kFget4Iki0RLBDowi35vaccz73XpC6/FAnfCrYa2IXVUlBHfZw+HGWzlvvTGwM9HHd32rgCrIDzil3kj+w==",
+ "dev": true,
+ "license": "Apache-2.0",
"dependencies": {
- "debug": "^4.1.0",
- "devtools-protocol": "0.0.869402",
- "extract-zip": "^2.0.0",
- "https-proxy-agent": "^5.0.0",
- "node-fetch": "^2.6.1",
- "pkg-dir": "^4.2.0",
- "progress": "^2.0.1",
- "proxy-from-env": "^1.1.0",
- "rimraf": "^3.0.2",
- "tar-fs": "^2.0.0",
- "unbzip2-stream": "^1.3.3",
- "ws": "^7.2.3"
+ "@puppeteer/browsers": "2.5.0",
+ "chromium-bidi": "0.8.0",
+ "debug": "^4.3.7",
+ "devtools-protocol": "0.0.1367902",
+ "typed-query-selector": "^2.12.0",
+ "ws": "^8.18.0"
},
"engines": {
- "node": ">=10.18.1"
+ "node": ">=18"
+ }
+ },
+ "node_modules/puppeteer-core/node_modules/ws": {
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
+ "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
}
},
"node_modules/query-string": {
@@ -2137,6 +2701,13 @@
"node": ">=0.10.0"
}
},
+ "node_modules/queue-tick": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz",
+ "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/range-parser": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
@@ -2146,18 +2717,24 @@
"node": ">= 0.6"
}
},
- "node_modules/readable-stream": {
- "version": "3.6.0",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
- "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"dev": true,
- "dependencies": {
- "inherits": "^2.0.3",
- "string_decoder": "^1.1.1",
- "util-deprecate": "^1.0.1"
- },
+ "license": "MIT",
"engines": {
- "node": ">= 6"
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
}
},
"node_modules/responselike": {
@@ -2169,41 +2746,6 @@
"lowercase-keys": "^1.0.0"
}
},
- "node_modules/rimraf": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
- "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
- "dev": true,
- "dependencies": {
- "glob": "^7.1.3"
- },
- "bin": {
- "rimraf": "bin.js"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/rimraf/node_modules/glob": {
- "version": "7.2.3",
- "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
- "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
- "dev": true,
- "dependencies": {
- "fs.realpath": "^1.0.0",
- "inflight": "^1.0.4",
- "inherits": "2",
- "minimatch": "^3.1.1",
- "once": "^1.3.0",
- "path-is-absolute": "^1.0.0"
- },
- "engines": {
- "node": "*"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -2238,10 +2780,11 @@
}
},
"node_modules/semver": {
- "version": "5.7.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
- "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+ "version": "5.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
+ "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
"dev": true,
+ "license": "ISC",
"bin": {
"semver": "bin/semver"
}
@@ -2268,18 +2811,18 @@
}
},
"node_modules/serve-handler": {
- "version": "6.1.1",
- "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.1.tgz",
- "integrity": "sha512-LQPvxGia2TYqyMkHKH4jW9jx6jlQUMcWz6gJavZ3+4vsnB+SaWbYTncb9YsK5YBR6SlvyumREZJAzLw8VaFAUQ==",
+ "version": "6.1.6",
+ "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.6.tgz",
+ "integrity": "sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"bytes": "3.0.0",
"content-disposition": "0.5.2",
- "fast-url-parser": "1.1.3",
"mime-types": "2.1.18",
- "minimatch": "3.0.4",
+ "minimatch": "3.1.2",
"path-is-inside": "1.0.2",
- "path-to-regexp": "2.2.1",
+ "path-to-regexp": "3.3.0",
"range-parser": "1.2.0"
}
},
@@ -2313,18 +2856,6 @@
"node": ">= 0.6"
}
},
- "node_modules/serve-handler/node_modules/minimatch": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
- "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
- "dev": true,
- "dependencies": {
- "brace-expansion": "^1.1.7"
- },
- "engines": {
- "node": "*"
- }
- },
"node_modules/shebang-command": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
@@ -2353,19 +2884,32 @@
"dev": true
},
"node_modules/slugify": {
- "version": "1.3.4",
- "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.3.4.tgz",
- "integrity": "sha512-KP0ZYk5hJNBS8/eIjGkFDCzGQIoZ1mnfQRYS5WM3273z+fxGWXeN0fkwf2ebEweydv9tioZIHGZKoF21U07/nw==",
+ "version": "1.6.6",
+ "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz",
+ "integrity": "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==",
"dev": true,
+ "license": "MIT",
"engines": {
- "node": ">=4.0.0"
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/smart-buffer": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
+ "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6.0.0",
+ "npm": ">= 3.0.0"
}
},
"node_modules/socket.io": {
- "version": "2.5.0",
- "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.5.0.tgz",
- "integrity": "sha512-gGunfS0od3VpwDBpGwVkzSZx6Aqo9uOcf1afJj2cKnKFAoyl16fvhpsUhmUFd4Ldbvl5JvRQed6eQw6oQp6n8w==",
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.5.1.tgz",
+ "integrity": "sha512-eaTE4tBKRD6RFoetquMbxgvcpvoDtRyIlkIMI/SMK2bsKvbENTsDeeu4GJ/z9c90yOWxB7b/eC+yKLPbHnH6bA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"debug": "~4.1.0",
"engine.io": "~3.6.0",
@@ -2416,9 +2960,9 @@
"dev": true
},
"node_modules/socket.io-client/node_modules/socket.io-parser": {
- "version": "3.3.3",
- "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.3.tgz",
- "integrity": "sha512-qOg87q1PMWWTeO01768Yh9ogn7chB9zkKtQnya41Y355S0UmpXgpcrFwAgjYJxu9BdKug5r5e9YtVSeWhKBUZg==",
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.4.tgz",
+ "integrity": "sha512-z/pFQB3x+EZldRRzORYW1vwVO8m/3ILkswtnpoeU6Ve3cbMWkmHEWDAVJn4QJtchiiFTo5j7UG2QvwxvaA9vow==",
"dev": true,
"dependencies": {
"component-emitter": "~1.3.0",
@@ -2466,6 +3010,36 @@
"ms": "^2.1.1"
}
},
+ "node_modules/socks": {
+ "version": "2.8.3",
+ "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz",
+ "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ip-address": "^9.0.5",
+ "smart-buffer": "^4.2.0"
+ },
+ "engines": {
+ "node": ">= 10.0.0",
+ "npm": ">= 3.0.0"
+ }
+ },
+ "node_modules/socks-proxy-agent": {
+ "version": "8.0.4",
+ "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz",
+ "integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.1",
+ "debug": "^4.3.4",
+ "socks": "^2.8.3"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"node_modules/sort-keys": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz",
@@ -2490,6 +3064,39 @@
"node": ">=0.10.0"
}
},
+ "node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/sprintf-js": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
+ "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/streamx": {
+ "version": "2.21.0",
+ "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.21.0.tgz",
+ "integrity": "sha512-Qz6MsDZXJ6ur9u+b+4xCG18TluU7PGlRfXVAAjNiGsFrBUt/ioyLkxbFaKJygoPs+/kW4VyBj0bSj89Qu0IGyg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-fifo": "^1.3.2",
+ "queue-tick": "^1.0.1",
+ "text-decoder": "^1.1.0"
+ },
+ "optionalDependencies": {
+ "bare-events": "^2.2.0"
+ }
+ },
"node_modules/strict-uri-encode": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz",
@@ -2499,13 +3106,32 @@
"node": ">=0.10.0"
}
},
- "node_modules/string_decoder": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
- "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "safe-buffer": "~5.2.0"
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
}
},
"node_modules/strip-dirs": {
@@ -2539,31 +3165,30 @@
}
},
"node_modules/tar-fs": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz",
- "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==",
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz",
+ "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "chownr": "^1.1.1",
- "mkdirp-classic": "^0.5.2",
"pump": "^3.0.0",
- "tar-stream": "^2.1.4"
+ "tar-stream": "^3.1.5"
+ },
+ "optionalDependencies": {
+ "bare-fs": "^2.1.1",
+ "bare-path": "^2.1.0"
}
},
"node_modules/tar-stream": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
- "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz",
+ "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "bl": "^4.0.3",
- "end-of-stream": "^1.4.1",
- "fs-constants": "^1.0.0",
- "inherits": "^2.0.3",
- "readable-stream": "^3.1.1"
- },
- "engines": {
- "node": ">=6"
+ "b4a": "^1.6.4",
+ "fast-fifo": "^1.2.0",
+ "streamx": "^2.15.0"
}
},
"node_modules/temp-dir": {
@@ -2588,6 +3213,13 @@
"node": ">=4"
}
},
+ "node_modules/text-decoder": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.1.tgz",
+ "integrity": "sha512-x9v3H/lTKIJKQQe7RPQkLfKAnc9lUTkWDypIQgTzPJAq+5/GCDHonmshfvlsNSj58yyshbIJJDLmU15qNERrXQ==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
"node_modules/through": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
@@ -2615,12 +3247,6 @@
"integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==",
"dev": true
},
- "node_modules/tr46": {
- "version": "0.0.3",
- "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
- "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
- "dev": true
- },
"node_modules/trim-repeated": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz",
@@ -2633,6 +3259,13 @@
"node": ">=0.10.0"
}
},
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
+ "license": "0BSD"
+ },
"node_modules/tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
@@ -2645,6 +3278,13 @@
"node": "*"
}
},
+ "node_modules/typed-query-selector": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz",
+ "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/unbzip2-stream": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz",
@@ -2655,6 +3295,14 @@
"through": "^2.3.8"
}
},
+ "node_modules/undici-types": {
+ "version": "6.20.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
+ "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true
+ },
"node_modules/url-parse-lax": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz",
@@ -2676,6 +3324,13 @@
"node": ">= 4"
}
},
+ "node_modules/urlpattern-polyfill": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz",
+ "integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -2692,22 +3347,6 @@
"uuid": "bin/uuid"
}
},
- "node_modules/webidl-conversions": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
- "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
- "dev": true
- },
- "node_modules/whatwg-url": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
- "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
- "dev": true,
- "dependencies": {
- "tr46": "~0.0.3",
- "webidl-conversions": "^3.0.0"
- }
- },
"node_modules/which": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
@@ -2720,6 +3359,24 @@
"which": "bin/which"
}
},
+ "node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
@@ -2727,10 +3384,11 @@
"dev": true
},
"node_modules/ws": {
- "version": "7.4.6",
- "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
- "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==",
+ "version": "7.5.10",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
+ "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=8.3.0"
},
@@ -2765,12 +3423,51 @@
"node": ">=0.4"
}
},
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/yallist": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
"integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==",
"dev": true
},
+ "node_modules/yargs": {
+ "version": "17.7.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+ "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/yauzl": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
@@ -2786,26 +3483,86 @@
"resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
"integrity": "sha512-8HFIh676uyGYP6wP13R/j6OJ/1HwJ46snpvzE7aHAN3Ryqh2yX6Xox2B4CUmTwwOIzlG3Bs7ocsP5dZH/R1Qbg==",
"dev": true
+ },
+ "node_modules/zod": {
+ "version": "3.23.8",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
+ "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
}
},
"dependencies": {
+ "@babel/code-frame": {
+ "version": "7.26.2",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
+ "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-validator-identifier": "^7.25.9",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.0.0"
+ }
+ },
+ "@babel/helper-validator-identifier": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
+ "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
+ "dev": true
+ },
+ "@puppeteer/browsers": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.5.0.tgz",
+ "integrity": "sha512-6TQAc/5uRILE6deixJ1CR8rXyTbzXIXNgO1D0Woi9Bqicz2FV5iKP3BHYEg6o4UATCMcbQQ0jbmeaOkn/HQk2w==",
+ "dev": true,
+ "requires": {
+ "debug": "^4.3.7",
+ "extract-zip": "^2.0.1",
+ "progress": "^2.0.3",
+ "proxy-agent": "^6.4.0",
+ "semver": "^7.6.3",
+ "tar-fs": "^3.0.6",
+ "unbzip2-stream": "^1.4.3",
+ "yargs": "^17.7.2"
+ },
+ "dependencies": {
+ "semver": {
+ "version": "7.6.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
+ "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
+ "dev": true
+ }
+ }
+ },
"@sindresorhus/is": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz",
"integrity": "sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==",
"dev": true
},
+ "@tootallnate/quickjs-emscripten": {
+ "version": "0.23.0",
+ "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz",
+ "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==",
+ "dev": true
+ },
"@types/node": {
- "version": "15.0.2",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-15.0.2.tgz",
- "integrity": "sha512-p68+a+KoxpoB47015IeYZYRrdqMUcpbK8re/zpFB8Ld46LHC1lPEbp3EXgkEhAYEcPvjJF6ZO+869SQ0aH1dcA==",
+ "version": "22.10.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz",
+ "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==",
"dev": true,
- "optional": true
+ "optional": true,
+ "requires": {
+ "undici-types": "~6.20.0"
+ }
},
"@types/yauzl": {
- "version": "2.9.1",
- "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.1.tgz",
- "integrity": "sha512-A1b8SU4D10uoPjwb0lnHmmu8wZhR9d+9o2PKBQT2jU5YPTKsxac6M2qGAdY7VcL+dHHhARVUDmeg0rOrcd9EjA==",
+ "version": "2.10.3",
+ "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
+ "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
"dev": true,
"optional": true,
"requires": {
@@ -2829,12 +3586,27 @@
"dev": true
},
"agent-base": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
- "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz",
+ "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==",
"dev": true,
"requires": {
- "debug": "4"
+ "debug": "^4.3.4"
+ }
+ },
+ "ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true
+ },
+ "ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^2.0.1"
}
},
"arch": {
@@ -2860,12 +3632,33 @@
}
}
},
+ "argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true
+ },
"arraybuffer.slice": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz",
"integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==",
"dev": true
},
+ "ast-types": {
+ "version": "0.13.4",
+ "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz",
+ "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==",
+ "dev": true,
+ "requires": {
+ "tslib": "^2.0.1"
+ }
+ },
+ "b4a": {
+ "version": "1.6.7",
+ "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz",
+ "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==",
+ "dev": true
+ },
"backo2": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
@@ -2878,6 +3671,52 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
},
+ "bare-events": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.0.tgz",
+ "integrity": "sha512-/E8dDe9dsbLyh2qrZ64PEPadOQ0F4gbl1sUJOrmph7xOiIxfY8vwab/4bFLh4Y88/Hk/ujKcrQKc+ps0mv873A==",
+ "dev": true,
+ "optional": true
+ },
+ "bare-fs": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.3.5.tgz",
+ "integrity": "sha512-SlE9eTxifPDJrT6YgemQ1WGFleevzwY+XAP1Xqgl56HtcrisC2CHCZ2tq6dBpcH2TnNxwUEUGhweo+lrQtYuiw==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "bare-events": "^2.0.0",
+ "bare-path": "^2.0.0",
+ "bare-stream": "^2.0.0"
+ }
+ },
+ "bare-os": {
+ "version": "2.4.4",
+ "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.4.4.tgz",
+ "integrity": "sha512-z3UiI2yi1mK0sXeRdc4O1Kk8aOa/e+FNWZcTiPB/dfTWyLypuE99LibgRaQki914Jq//yAWylcAt+mknKdixRQ==",
+ "dev": true,
+ "optional": true
+ },
+ "bare-path": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.3.tgz",
+ "integrity": "sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "bare-os": "^2.1.0"
+ }
+ },
+ "bare-stream": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.4.2.tgz",
+ "integrity": "sha512-XZ4ln/KV4KT+PXdIWTKjsLY+quqCaEtqqtgGJVPw9AoM73By03ij64YjepK0aQvHSWDb6AfAZwqKaFu68qkrdA==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "streamx": "^2.20.0"
+ }
+ },
"base64-arraybuffer": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz",
@@ -2896,6 +3735,12 @@
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
"dev": true
},
+ "basic-ftp": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz",
+ "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==",
+ "dev": true
+ },
"bin-build": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bin-build/-/bin-build-3.0.0.tgz",
@@ -2930,9 +3775,9 @@
},
"dependencies": {
"cross-spawn": {
- "version": "6.0.5",
- "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
- "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
+ "version": "6.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz",
+ "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==",
"dev": true,
"requires": {
"nice-try": "^1.0.4",
@@ -3107,17 +3952,6 @@
}
}
},
- "bl": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
- "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
- "dev": true,
- "requires": {
- "buffer": "^5.5.0",
- "inherits": "^2.0.4",
- "readable-stream": "^3.4.0"
- }
- },
"blob": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz",
@@ -3207,6 +4041,12 @@
}
}
},
+ "callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true
+ },
"caw": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/caw/-/caw-2.0.1.tgz",
@@ -3219,11 +4059,27 @@
"url-to-options": "^1.0.1"
}
},
- "chownr": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
- "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
- "dev": true
+ "chromium-bidi": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.8.0.tgz",
+ "integrity": "sha512-uJydbGdTw0DEUjhoogGveneJVWX/9YuqkWePzMmkBYwtdAqo5d3J/ovNKFr+/2hWXYmYCr6it8mSSTIj6SS6Ug==",
+ "dev": true,
+ "requires": {
+ "mitt": "3.0.1",
+ "urlpattern-polyfill": "10.0.0",
+ "zod": "3.23.8"
+ }
+ },
+ "cliui": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "dev": true,
+ "requires": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ }
},
"clone-response": {
"version": "1.0.2",
@@ -3234,6 +4090,21 @@
"mimic-response": "^1.0.0"
}
},
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
"commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
@@ -3295,6 +4166,18 @@
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"dev": true
},
+ "cosmiconfig": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz",
+ "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==",
+ "dev": true,
+ "requires": {
+ "env-paths": "^2.2.1",
+ "import-fresh": "^3.3.0",
+ "js-yaml": "^4.1.0",
+ "parse-json": "^5.2.0"
+ }
+ },
"cross-spawn": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
@@ -3316,13 +4199,19 @@
"bin-wrapper": "^4.0.1"
}
},
+ "data-uri-to-buffer": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz",
+ "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==",
+ "dev": true
+ },
"debug": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
- "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"dev": true,
"requires": {
- "ms": "2.1.2"
+ "ms": "^2.1.3"
}
},
"decode-uri-component": {
@@ -3506,10 +4395,21 @@
}
}
},
+ "degenerator": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz",
+ "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==",
+ "dev": true,
+ "requires": {
+ "ast-types": "^0.13.4",
+ "escodegen": "^2.1.0",
+ "esprima": "^4.0.1"
+ }
+ },
"devtools-protocol": {
- "version": "0.0.869402",
- "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.869402.tgz",
- "integrity": "sha512-VvlVYY+VDJe639yHs5PHISzdWTLL3Aw8rO4cvUtwvoxFd6FHbE4OpHHcde52M6096uYYazAmd4l0o5VuFRO2WA==",
+ "version": "0.0.1367902",
+ "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1367902.tgz",
+ "integrity": "sha512-XxtPuC3PGakY6PD7dG66/o8KwJ/LkH2/EKe19Dcw58w53dv4/vSQEkn/SzuyhHE2q4zPgCkxQBxus3VV4ql+Pg==",
"dev": true
},
"download": {
@@ -3551,6 +4451,12 @@
"integrity": "sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==",
"dev": true
},
+ "emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
"end-of-stream": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
@@ -3561,9 +4467,9 @@
}
},
"engine.io": {
- "version": "3.6.1",
- "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.6.1.tgz",
- "integrity": "sha512-dfs8EVg/i7QjFsXxn7cCRQ+Wai1G1TlEvHhdYEi80fxn5R1vZ2K661O6v/rezj1FP234SZ14r9CmJke99iYDGg==",
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.6.2.tgz",
+ "integrity": "sha512-C4JjGQZLY3kWlIDx0BQNKizbrfpb7NahxDztGdN5jrPK2ghmXiNDN+E/t0JzDeNRZxPVaszxEng42Pmj27X/0w==",
"dev": true,
"requires": {
"accepts": "~1.3.4",
@@ -3571,7 +4477,7 @@
"cookie": "~0.4.1",
"debug": "~4.1.0",
"engine.io-parser": "~2.2.0",
- "ws": "~7.4.2"
+ "ws": "~7.5.10"
},
"dependencies": {
"debug": {
@@ -3586,9 +4492,9 @@
}
},
"engine.io-client": {
- "version": "3.5.3",
- "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.5.3.tgz",
- "integrity": "sha512-qsgyc/CEhJ6cgMUwxRRtOndGVhIu5hpL5tR4umSpmX/MvkFoIxUTM7oFMDQumHNzlNLwSVy6qhstFPoWTf7dOw==",
+ "version": "3.5.4",
+ "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.5.4.tgz",
+ "integrity": "sha512-ydc8uuMMDxC5KCKNJN3zZKYJk2sgyTuTZQ7Aj1DJSsLKAcizA/PzWivw8fZMIjJVBo2CJOYzntv4FSjY/Lr//g==",
"dev": true,
"requires": {
"component-emitter": "~1.3.0",
@@ -3599,7 +4505,7 @@
"indexof": "0.0.1",
"parseqs": "0.0.6",
"parseuri": "0.0.6",
- "ws": "~7.4.2",
+ "ws": "~7.5.10",
"xmlhttprequest-ssl": "~1.6.2",
"yeast": "0.1.2"
},
@@ -3634,12 +4540,63 @@
"has-binary2": "~1.0.2"
}
},
+ "env-paths": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
+ "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
+ "dev": true
+ },
+ "error-ex": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
+ "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+ "dev": true,
+ "requires": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
+ "escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true
+ },
"escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
"dev": true
},
+ "escodegen": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz",
+ "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==",
+ "dev": true,
+ "requires": {
+ "esprima": "^4.0.1",
+ "estraverse": "^5.2.0",
+ "esutils": "^2.0.2",
+ "source-map": "~0.6.1"
+ }
+ },
+ "esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "dev": true
+ },
+ "estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true
+ },
+ "esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true
+ },
"execa": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz",
@@ -3711,14 +4668,11 @@
"yauzl": "^2.10.0"
}
},
- "fast-url-parser": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/fast-url-parser/-/fast-url-parser-1.1.3.tgz",
- "integrity": "sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ==",
- "dev": true,
- "requires": {
- "punycode": "^1.3.2"
- }
+ "fast-fifo": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
+ "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
+ "dev": true
},
"fd-slicer": {
"version": "1.1.0",
@@ -3752,16 +4706,6 @@
"trim-repeated": "^1.0.0"
}
},
- "find-up": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
- "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
- "dev": true,
- "requires": {
- "locate-path": "^5.0.0",
- "path-exists": "^4.0.0"
- }
- },
"find-versions": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/find-versions/-/find-versions-3.2.0.tgz",
@@ -3825,10 +4769,10 @@
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
"dev": true
},
- "fs.realpath": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
- "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+ "get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true
},
"get-proxy": {
@@ -3849,6 +4793,17 @@
"pump": "^3.0.0"
}
},
+ "get-uri": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.4.tgz",
+ "integrity": "sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ==",
+ "dev": true,
+ "requires": {
+ "basic-ftp": "^5.0.2",
+ "data-uri-to-buffer": "^6.0.2",
+ "debug": "^4.3.4"
+ }
+ },
"glob": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.0.0.tgz",
@@ -3934,13 +4889,23 @@
"integrity": "sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w==",
"dev": true
},
+ "http-proxy-agent": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+ "dev": true,
+ "requires": {
+ "agent-base": "^7.1.0",
+ "debug": "^4.3.4"
+ }
+ },
"https-proxy-agent": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz",
- "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==",
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz",
+ "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==",
"dev": true,
"requires": {
- "agent-base": "6",
+ "agent-base": "^7.0.2",
"debug": "4"
}
},
@@ -3950,6 +4915,16 @@
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"dev": true
},
+ "import-fresh": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
+ "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+ "dev": true,
+ "requires": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ }
+ },
"import-lazy": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-3.1.0.tgz",
@@ -3994,6 +4969,28 @@
"p-is-promise": "^1.1.0"
}
},
+ "ip-address": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
+ "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==",
+ "dev": true,
+ "requires": {
+ "jsbn": "1.1.0",
+ "sprintf-js": "^1.1.3"
+ }
+ },
+ "is-arrayish": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
+ "dev": true
+ },
+ "is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true
+ },
"is-natural-number": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz",
@@ -4046,12 +5043,39 @@
"is-object": "^1.0.1"
}
},
+ "js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true
+ },
+ "js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dev": true,
+ "requires": {
+ "argparse": "^2.0.1"
+ }
+ },
+ "jsbn": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
+ "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==",
+ "dev": true
+ },
"json-buffer": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz",
"integrity": "sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==",
"dev": true
},
+ "json-parse-even-better-errors": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
+ "dev": true
+ },
"keyv": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-3.0.0.tgz",
@@ -4061,14 +5085,11 @@
"json-buffer": "3.0.0"
}
},
- "locate-path": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
- "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
- "dev": true,
- "requires": {
- "p-locate": "^4.1.0"
- }
+ "lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "dev": true
},
"lowercase-keys": {
"version": "1.0.1",
@@ -4133,16 +5154,16 @@
"brace-expansion": "^1.1.7"
}
},
- "mkdirp-classic": {
- "version": "0.5.3",
- "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
- "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
+ "mitt": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
+ "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
"dev": true
},
"ms": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true
},
"negotiator": {
@@ -4151,21 +5172,18 @@
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
"dev": true
},
+ "netmask": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz",
+ "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==",
+ "dev": true
+ },
"nice-try": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
"dev": true
},
- "node-fetch": {
- "version": "2.6.11",
- "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.11.tgz",
- "integrity": "sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==",
- "dev": true,
- "requires": {
- "whatwg-url": "^5.0.0"
- }
- },
"normalize-url": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-2.0.1.tgz",
@@ -4272,24 +5290,6 @@
"integrity": "sha512-zL7VE4JVS2IFSkR2GQKDSPEVxkoH43/p7oEnwpdCndKYJO0HVeRB7fA8TJwuLOTBREtK0ea8eHaxdwcpob5dmg==",
"dev": true
},
- "p-limit": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
- "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
- "dev": true,
- "requires": {
- "p-try": "^2.0.0"
- }
- },
- "p-locate": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
- "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
- "dev": true,
- "requires": {
- "p-limit": "^2.2.0"
- }
- },
"p-map-series": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-map-series/-/p-map-series-1.0.0.tgz",
@@ -4314,11 +5314,52 @@
"p-finally": "^1.0.0"
}
},
- "p-try": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
- "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
- "dev": true
+ "pac-proxy-agent": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.2.tgz",
+ "integrity": "sha512-BFi3vZnO9X5Qt6NRz7ZOaPja3ic0PhlsmCRYLOpN11+mWBCR6XJDqW5RF3j8jm4WGGQZtBA+bTfxYzeKW73eHg==",
+ "dev": true,
+ "requires": {
+ "@tootallnate/quickjs-emscripten": "^0.23.0",
+ "agent-base": "^7.0.2",
+ "debug": "^4.3.4",
+ "get-uri": "^6.0.1",
+ "http-proxy-agent": "^7.0.0",
+ "https-proxy-agent": "^7.0.5",
+ "pac-resolver": "^7.0.1",
+ "socks-proxy-agent": "^8.0.4"
+ }
+ },
+ "pac-resolver": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz",
+ "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==",
+ "dev": true,
+ "requires": {
+ "degenerator": "^5.0.0",
+ "netmask": "^2.0.2"
+ }
+ },
+ "parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "requires": {
+ "callsites": "^3.0.0"
+ }
+ },
+ "parse-json": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
+ "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+ "dev": true,
+ "requires": {
+ "@babel/code-frame": "^7.0.0",
+ "error-ex": "^1.3.1",
+ "json-parse-even-better-errors": "^2.3.0",
+ "lines-and-columns": "^1.1.6"
+ }
},
"parseqs": {
"version": "0.0.6",
@@ -4332,12 +5373,6 @@
"integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==",
"dev": true
},
- "path-exists": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
- "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
- "dev": true
- },
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
@@ -4357,9 +5392,9 @@
"dev": true
},
"path-to-regexp": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-2.2.1.tgz",
- "integrity": "sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ==",
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz",
+ "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==",
"dev": true
},
"pend": {
@@ -4368,6 +5403,12 @@
"integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=",
"dev": true
},
+ "picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true
+ },
"pify": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
@@ -4390,27 +5431,26 @@
}
},
"pixelmatch": {
- "version": "5.0.2",
- "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-5.0.2.tgz",
- "integrity": "sha512-b65UpTI40rGFY8QwN6IYuCbpmwAOL6M8d6voX4F3zR99UmDqh7r2QWLxoeHOazBRgEmDUdqNVESDREqFxQS7rQ==",
- "dev": true,
- "requires": {
- "pngjs": "^3.4.0"
- }
- },
- "pkg-dir": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
- "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-5.3.0.tgz",
+ "integrity": "sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q==",
"dev": true,
"requires": {
- "find-up": "^4.0.0"
+ "pngjs": "^6.0.0"
+ },
+ "dependencies": {
+ "pngjs": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz",
+ "integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==",
+ "dev": true
+ }
}
},
"pngjs": {
- "version": "3.4.0",
- "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz",
- "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz",
+ "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==",
"dev": true
},
"prepend-http": {
@@ -4437,6 +5477,30 @@
"integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==",
"dev": true
},
+ "proxy-agent": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz",
+ "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==",
+ "dev": true,
+ "requires": {
+ "agent-base": "^7.0.2",
+ "debug": "^4.3.4",
+ "http-proxy-agent": "^7.0.1",
+ "https-proxy-agent": "^7.0.3",
+ "lru-cache": "^7.14.1",
+ "pac-proxy-agent": "^7.0.1",
+ "proxy-from-env": "^1.1.0",
+ "socks-proxy-agent": "^8.0.2"
+ },
+ "dependencies": {
+ "lru-cache": {
+ "version": "7.18.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
+ "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
+ "dev": true
+ }
+ }
+ },
"proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@@ -4459,30 +5523,41 @@
"once": "^1.3.1"
}
},
- "punycode": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
- "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==",
- "dev": true
- },
"puppeteer": {
- "version": "9.1.1",
- "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-9.1.1.tgz",
- "integrity": "sha512-W+nOulP2tYd/ZG99WuZC/I5ljjQQ7EUw/jQGcIb9eu8mDlZxNY2SgcJXTLG9h5gRvqA3uJOe4hZXYsd3EqioMw==",
+ "version": "23.10.0",
+ "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-23.10.0.tgz",
+ "integrity": "sha512-vLpEvUM8POKBX4j6y/yyD4oGRhS1oBAbMn3lYpz1yeakhRsA8IhF5QmjXwAIQYGdR/INxyFiY6kQDoUtLGDP3g==",
"dev": true,
"requires": {
- "debug": "^4.1.0",
- "devtools-protocol": "0.0.869402",
- "extract-zip": "^2.0.0",
- "https-proxy-agent": "^5.0.0",
- "node-fetch": "^2.6.1",
- "pkg-dir": "^4.2.0",
- "progress": "^2.0.1",
- "proxy-from-env": "^1.1.0",
- "rimraf": "^3.0.2",
- "tar-fs": "^2.0.0",
- "unbzip2-stream": "^1.3.3",
- "ws": "^7.2.3"
+ "@puppeteer/browsers": "2.5.0",
+ "chromium-bidi": "0.8.0",
+ "cosmiconfig": "^9.0.0",
+ "devtools-protocol": "0.0.1367902",
+ "puppeteer-core": "23.10.0",
+ "typed-query-selector": "^2.12.0"
+ }
+ },
+ "puppeteer-core": {
+ "version": "23.10.0",
+ "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-23.10.0.tgz",
+ "integrity": "sha512-7pv6kFget4Iki0RLBDowi35vaccz73XpC6/FAnfCrYa2IXVUlBHfZw+HGWzlvvTGwM9HHd32rgCrIDzil3kj+w==",
+ "dev": true,
+ "requires": {
+ "@puppeteer/browsers": "2.5.0",
+ "chromium-bidi": "0.8.0",
+ "debug": "^4.3.7",
+ "devtools-protocol": "0.0.1367902",
+ "typed-query-selector": "^2.12.0",
+ "ws": "^8.18.0"
+ },
+ "dependencies": {
+ "ws": {
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
+ "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
+ "dev": true,
+ "requires": {}
+ }
}
},
"query-string": {
@@ -4496,22 +5571,29 @@
"strict-uri-encode": "^1.0.0"
}
},
+ "queue-tick": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz",
+ "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==",
+ "dev": true
+ },
"range-parser": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
"integrity": "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==",
"dev": true
},
- "readable-stream": {
- "version": "3.6.0",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
- "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
- "dev": true,
- "requires": {
- "inherits": "^2.0.3",
- "string_decoder": "^1.1.1",
- "util-deprecate": "^1.0.1"
- }
+ "require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "dev": true
+ },
+ "resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true
},
"responselike": {
"version": "1.0.2",
@@ -4522,31 +5604,6 @@
"lowercase-keys": "^1.0.0"
}
},
- "rimraf": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
- "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
- "dev": true,
- "requires": {
- "glob": "^7.1.3"
- },
- "dependencies": {
- "glob": {
- "version": "7.2.3",
- "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
- "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
- "dev": true,
- "requires": {
- "fs.realpath": "^1.0.0",
- "inflight": "^1.0.4",
- "inherits": "2",
- "minimatch": "^3.1.1",
- "once": "^1.3.0",
- "path-is-absolute": "^1.0.0"
- }
- }
- }
- },
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -4563,9 +5620,9 @@
}
},
"semver": {
- "version": "5.7.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
- "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+ "version": "5.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
+ "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
"dev": true
},
"semver-regex": {
@@ -4584,18 +5641,17 @@
}
},
"serve-handler": {
- "version": "6.1.1",
- "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.1.tgz",
- "integrity": "sha512-LQPvxGia2TYqyMkHKH4jW9jx6jlQUMcWz6gJavZ3+4vsnB+SaWbYTncb9YsK5YBR6SlvyumREZJAzLw8VaFAUQ==",
+ "version": "6.1.6",
+ "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.6.tgz",
+ "integrity": "sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==",
"dev": true,
"requires": {
"bytes": "3.0.0",
"content-disposition": "0.5.2",
- "fast-url-parser": "1.1.3",
"mime-types": "2.1.18",
- "minimatch": "3.0.4",
+ "minimatch": "3.1.2",
"path-is-inside": "1.0.2",
- "path-to-regexp": "2.2.1",
+ "path-to-regexp": "3.3.0",
"range-parser": "1.2.0"
},
"dependencies": {
@@ -4619,15 +5675,6 @@
"requires": {
"mime-db": "~1.33.0"
}
- },
- "minimatch": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
- "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
- "dev": true,
- "requires": {
- "brace-expansion": "^1.1.7"
- }
}
}
},
@@ -4653,15 +5700,21 @@
"dev": true
},
"slugify": {
- "version": "1.3.4",
- "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.3.4.tgz",
- "integrity": "sha512-KP0ZYk5hJNBS8/eIjGkFDCzGQIoZ1mnfQRYS5WM3273z+fxGWXeN0fkwf2ebEweydv9tioZIHGZKoF21U07/nw==",
+ "version": "1.6.6",
+ "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz",
+ "integrity": "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==",
+ "dev": true
+ },
+ "smart-buffer": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
+ "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
"dev": true
},
"socket.io": {
- "version": "2.5.0",
- "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.5.0.tgz",
- "integrity": "sha512-gGunfS0od3VpwDBpGwVkzSZx6Aqo9uOcf1afJj2cKnKFAoyl16fvhpsUhmUFd4Ldbvl5JvRQed6eQw6oQp6n8w==",
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.5.1.tgz",
+ "integrity": "sha512-eaTE4tBKRD6RFoetquMbxgvcpvoDtRyIlkIMI/SMK2bsKvbENTsDeeu4GJ/z9c90yOWxB7b/eC+yKLPbHnH6bA==",
"dev": true,
"requires": {
"debug": "~4.1.0",
@@ -4724,9 +5777,9 @@
"dev": true
},
"socket.io-parser": {
- "version": "3.3.3",
- "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.3.tgz",
- "integrity": "sha512-qOg87q1PMWWTeO01768Yh9ogn7chB9zkKtQnya41Y355S0UmpXgpcrFwAgjYJxu9BdKug5r5e9YtVSeWhKBUZg==",
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.4.tgz",
+ "integrity": "sha512-z/pFQB3x+EZldRRzORYW1vwVO8m/3ILkswtnpoeU6Ve3cbMWkmHEWDAVJn4QJtchiiFTo5j7UG2QvwxvaA9vow==",
"dev": true,
"requires": {
"component-emitter": "~1.3.0",
@@ -4764,6 +5817,27 @@
}
}
},
+ "socks": {
+ "version": "2.8.3",
+ "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz",
+ "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==",
+ "dev": true,
+ "requires": {
+ "ip-address": "^9.0.5",
+ "smart-buffer": "^4.2.0"
+ }
+ },
+ "socks-proxy-agent": {
+ "version": "8.0.4",
+ "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz",
+ "integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==",
+ "dev": true,
+ "requires": {
+ "agent-base": "^7.1.1",
+ "debug": "^4.3.4",
+ "socks": "^2.8.3"
+ }
+ },
"sort-keys": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz",
@@ -4782,19 +5856,55 @@
"sort-keys": "^1.0.0"
}
},
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "optional": true
+ },
+ "sprintf-js": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
+ "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
+ "dev": true
+ },
+ "streamx": {
+ "version": "2.21.0",
+ "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.21.0.tgz",
+ "integrity": "sha512-Qz6MsDZXJ6ur9u+b+4xCG18TluU7PGlRfXVAAjNiGsFrBUt/ioyLkxbFaKJygoPs+/kW4VyBj0bSj89Qu0IGyg==",
+ "dev": true,
+ "requires": {
+ "bare-events": "^2.2.0",
+ "fast-fifo": "^1.3.2",
+ "queue-tick": "^1.0.1",
+ "text-decoder": "^1.1.0"
+ }
+ },
"strict-uri-encode": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz",
"integrity": "sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==",
"dev": true
},
- "string_decoder": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
- "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "requires": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ }
+ },
+ "strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"requires": {
- "safe-buffer": "~5.2.0"
+ "ansi-regex": "^5.0.1"
}
},
"strip-dirs": {
@@ -4822,28 +5932,26 @@
}
},
"tar-fs": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz",
- "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==",
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz",
+ "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==",
"dev": true,
"requires": {
- "chownr": "^1.1.1",
- "mkdirp-classic": "^0.5.2",
+ "bare-fs": "^2.1.1",
+ "bare-path": "^2.1.0",
"pump": "^3.0.0",
- "tar-stream": "^2.1.4"
+ "tar-stream": "^3.1.5"
}
},
"tar-stream": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
- "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz",
+ "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==",
"dev": true,
"requires": {
- "bl": "^4.0.3",
- "end-of-stream": "^1.4.1",
- "fs-constants": "^1.0.0",
- "inherits": "^2.0.3",
- "readable-stream": "^3.1.1"
+ "b4a": "^1.6.4",
+ "fast-fifo": "^1.2.0",
+ "streamx": "^2.15.0"
}
},
"temp-dir": {
@@ -4862,6 +5970,12 @@
"uuid": "^3.0.1"
}
},
+ "text-decoder": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.1.tgz",
+ "integrity": "sha512-x9v3H/lTKIJKQQe7RPQkLfKAnc9lUTkWDypIQgTzPJAq+5/GCDHonmshfvlsNSj58yyshbIJJDLmU15qNERrXQ==",
+ "dev": true
+ },
"through": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
@@ -4886,12 +6000,6 @@
"integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==",
"dev": true
},
- "tr46": {
- "version": "0.0.3",
- "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
- "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
- "dev": true
- },
"trim-repeated": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz",
@@ -4901,6 +6009,12 @@
"escape-string-regexp": "^1.0.2"
}
},
+ "tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true
+ },
"tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
@@ -4910,6 +6024,12 @@
"safe-buffer": "^5.0.1"
}
},
+ "typed-query-selector": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz",
+ "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==",
+ "dev": true
+ },
"unbzip2-stream": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz",
@@ -4920,6 +6040,13 @@
"through": "^2.3.8"
}
},
+ "undici-types": {
+ "version": "6.20.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
+ "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
+ "dev": true,
+ "optional": true
+ },
"url-parse-lax": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz",
@@ -4935,6 +6062,12 @@
"integrity": "sha512-0kQLIzG4fdk/G5NONku64rSH/x32NOA39LVQqlK8Le6lvTF6GGRJpqaQFGgU+CLwySIqBSMdwYM0sYcW9f6P4A==",
"dev": true
},
+ "urlpattern-polyfill": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz",
+ "integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==",
+ "dev": true
+ },
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -4947,22 +6080,6 @@
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"dev": true
},
- "webidl-conversions": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
- "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
- "dev": true
- },
- "whatwg-url": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
- "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
- "dev": true,
- "requires": {
- "tr46": "~0.0.3",
- "webidl-conversions": "^3.0.0"
- }
- },
"which": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
@@ -4972,6 +6089,17 @@
"isexe": "^2.0.0"
}
},
+ "wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ }
+ },
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
@@ -4979,9 +6107,9 @@
"dev": true
},
"ws": {
- "version": "7.4.6",
- "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
- "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==",
+ "version": "7.5.10",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
+ "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
"dev": true,
"requires": {}
},
@@ -4997,12 +6125,39 @@
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"dev": true
},
+ "y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "dev": true
+ },
"yallist": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
"integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==",
"dev": true
},
+ "yargs": {
+ "version": "17.7.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+ "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+ "dev": true,
+ "requires": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ }
+ },
+ "yargs-parser": {
+ "version": "21.1.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "dev": true
+ },
"yauzl": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
@@ -5018,6 +6173,12 @@
"resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
"integrity": "sha512-8HFIh676uyGYP6wP13R/j6OJ/1HwJ46snpvzE7aHAN3Ryqh2yX6Xox2B4CUmTwwOIzlG3Bs7ocsP5dZH/R1Qbg==",
"dev": true
+ },
+ "zod": {
+ "version": "3.23.8",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
+ "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
+ "dev": true
}
}
}
diff --git a/test/runTest/package.json b/test/runTest/package.json
index 078ca8291..43095a6fe 100644
--- a/test/runTest/package.json
+++ b/test/runTest/package.json
@@ -5,12 +5,12 @@
"devDependencies": {
"cwebp-bin": "^6.1.1",
"glob": "7.0.0",
- "pixelmatch": "5.0.2",
- "pngjs": "3.4.0",
- "puppeteer": "^9.1.1",
- "serve-handler": "6.1.1",
- "slugify": "^1.3.4",
- "socket.io": "2.5.0"
+ "pixelmatch": "<6.0.0",
+ "pngjs": "7.0.0",
+ "puppeteer": "^23.10.0",
+ "serve-handler": "^6.1.6",
+ "slugify": "^1.6.6",
+ "socket.io": "^2.5.1"
},
"scripts": {
"start": "npm run test:visual",
diff --git a/test/runTest/recorder/index.html b/test/runTest/recorder/index.html
index d344b1018..40adf2793 100644
--- a/test/runTest/recorder/index.html
+++ b/test/runTest/recorder/index.html
@@ -61,7 +61,7 @@ under the License.
<div v-for="action in actions" :class="{'action-item': true, 'active': action.name === currentAction.name}" @click.self="select(action.name)">
{{action.name}}
<div style="float:right" class="operations">
- <el-popover>
+ <el-popover popper-class="action-popover">
<div style="text-align: center">{ scrollX: {{action.scrollX}}, scrollY: {{action.scrollY}} }</div>
<div v-for="op in action.ops" style="text-align: center">{{op}}</div>
<div slot="reference">
diff --git a/test/runTest/recorder/recorder.css b/test/runTest/recorder/recorder.css
index 9e6c8cce7..3e2e81f4a 100644
--- a/test/runTest/recorder/recorder.css
+++ b/test/runTest/recorder/recorder.css
@@ -189,6 +189,11 @@ iframe {
margin-left: 10px;
}
+.action-popover {
+ max-height: 90%;
+ overflow: auto;
+}
+
::-webkit-scrollbar {
height: 8px;
width: 8px;
diff --git a/test/runTest/recorder/recorder.js b/test/runTest/recorder/recorder.js
index 66f18dee7..e4289563e 100644
--- a/test/runTest/recorder/recorder.js
+++ b/test/runTest/recorder/recorder.js
@@ -40,7 +40,7 @@ function getUniqueSelector(el) {
let selector = '';
if (el.id) {
// id has highest priority.
- return el.id;
+ return '#' + el.id;
}
else {
selector = el.tagName.toLowerCase();
diff --git a/test/runTest/runtime/ActionPlayback.js b/test/runTest/runtime/ActionPlayback.js
index aa50caa67..cad29497c 100644
--- a/test/runTest/runtime/ActionPlayback.js
+++ b/test/runTest/runtime/ActionPlayback.js
@@ -37,6 +37,8 @@ export class ActionPlayback {
this._currentOpIndex = 0;
this._isLastOpMousewheel = false;
+
+ this._isMouseDown = false;
}
getContext() {
@@ -52,6 +54,7 @@ export class ActionPlayback {
this._current = Date.now();
this._elapsedTime = 0;
this._isLastOpMousewheel = false;
+ this._isMouseDown = false;
}
_restoreContext(ctx) {
@@ -103,7 +106,7 @@ export class ActionPlayback {
try {
// Execute all if there are multiple ops in one frame.
do {
- const executed = await self._update(takeScreenshot);
+ const executed = await self._update(takeScreenshot, action.name);
if (!executed) {
break;
}
@@ -117,6 +120,14 @@ export class ActionPlayback {
if (self._currentOpIndex >= self._ops.length) {
// Finished
+ if (self._isMouseDown) {
+ reject(new Error(
+ action.name + ' has finished, but the page remains in mousedown state.'
+ + ' A mouseup is needed; otherwise the subsequent test case may be affected.'
+ + ' Please re-record this test case.'
+ ));
+ return;
+ }
resolve();
}
else {
@@ -135,7 +146,7 @@ export class ActionPlayback {
}
}
- async _update(takeScreenshot) {
+ async _update(takeScreenshot, actionName) {
let op = this._ops[this._currentOpIndex];
if (!op || (op.time > this._elapsedTime)) {
@@ -144,18 +155,23 @@ export class ActionPlayback {
}
let screenshotTaken = false;
+ let errMsgPart;
switch (op.type) {
case 'mousedown':
+ errMsgPart = actionName + ' ' + op.type + ' (' + op.x + ',' + op.y + ')';
// Pause timeline to avoid frame not sync.
timeline.pause();
await __VRT_MOUSE_MOVE__(op.x, op.y);
- await __VRT_MOUSE_DOWN__();
+ await __VRT_MOUSE_DOWN__(errMsgPart);
+ this._isMouseDown = true;
timeline.resume();
break;
case 'mouseup':
+ errMsgPart = actionName + ' ' + op.type + ' (' + op.x + ',' + op.y + ')';
timeline.pause();
await __VRT_MOUSE_MOVE__(op.x, op.y);
- await __VRT_MOUSE_UP__();
+ await __VRT_MOUSE_UP__(errMsgPart);
+ this._isMouseDown = false;
if (window.__VRT_RELOAD_TRIGGERED__) {
return;
}
@@ -187,6 +203,12 @@ export class ActionPlayback {
break;
case 'valuechange':
const selector = document.querySelector(op.selector);
+ if (!selector) {
+ throw new Error(
+ '[Test Case Error] (' + actionName + ') Selector not found: ' + op.selector
+ + '. It may be caused by test case changes.'
+ );
+ }
selector.value = op.value;
// changing value via js won't trigger `change` event, so trigger it manually
selector.dispatchEvent(new Event('change'));
diff --git a/test/runTest/runtime/main.js b/test/runTest/runtime/main.js
index 633911d32..c83bd6640 100644
--- a/test/runTest/runtime/main.js
+++ b/test/runTest/runtime/main.js
@@ -103,7 +103,16 @@ window.__VRT_RUN_ACTIONS__ = async function (actions, restoredActionIndex, resto
continue;
}
window.scrollTo(action.scrollX, action.scrollY);
- await actionPlayback.runAction(action, index === restoredActionIndex ? restoredActionContext : null);
+ try {
+ await actionPlayback.runAction(action, index === restoredActionIndex ? restoredActionContext : null);
+ }
+ catch (err) {
+ // Any error in this JS task must be handled; otherwise __VRT_FINISH_ACTIONS__
+ // can not be called and the entire test execution will be blocked.
+ const errStr = errToStr(err);
+ // console.error(errStr);
+ __VRT_LOG_ERRORS__(errStr);
+ }
}
actionPlayback.stop();
@@ -111,6 +120,19 @@ window.__VRT_RUN_ACTIONS__ = async function (actions, restoredActionIndex, resto
__VRT_FINISH_ACTIONS__();
}
+function errToStr(err) {
+ if (typeof err === 'string') {
+ return err;
+ }
+ if (err && err.message != null) {
+ return err.message + (err.stack ? ' ' + err.stack : '');
+ }
+ if (err && err.toString) {
+ return err.toString();
+ }
+ return '[error] ' + err;
+}
+
window.addEventListener('DOMContentLoaded', () => {
let style = document.createElement('style');
diff --git a/test/runTest/server.js b/test/runTest/server.js
index ce701f785..a6f7e2933 100644
--- a/test/runTest/server.js
+++ b/test/runTest/server.js
@@ -25,6 +25,8 @@ process.on('uncaughtException', (err) => {
console.log();
console.error(`Please run \`${chalk.yellow('npm install')}\` in \`${chalk.green('test/runTest')}\` folder first!`);
console.log();
+ } else {
+ console.error('Uncaught err:', err);
}
process.exit(1);
});
@@ -46,17 +48,23 @@ const {
getAllTestsRuns,
delTestsRun,
RESULTS_ROOT_DIR,
- checkStoreVersion
+ checkStoreVersion,
+ clearStaledResults
} = require('./store');
-const {prepareEChartsLib, getActionsFullPath, fetchVersions} = require('./util');
+const {prepareEChartsLib, getActionsFullPath, fetchVersions, cleanBranchDirectory, fetchRecentPRs} = require('./util');
const fse = require('fs-extra');
const fs = require('fs');
const open = require('open');
const genReport = require('./genReport');
+const useCNMirror = process.argv.includes('--useCNMirror') || !!process.env.USE_CN_MIRROR;
+
+console.info(chalk.green('useCNMirror:'), useCNMirror);
const CLI_FIXED_THREADS_COUNT = 1;
function serve() {
+ clearStaledResults();
+
const server = http.createServer((request, response) => {
return handler(request, response, {
cleanUrls: false,
@@ -139,7 +147,9 @@ function startTests(testsNameList, socket, {
noHeadless,
threadsCount,
replaySpeed,
+ actualSource,
actualVersion,
+ expectedSource,
expectedVersion,
renderer,
useCoarsePointer,
@@ -201,6 +211,8 @@ function startTests(testsNameList, socket, {
'--speed', replaySpeed || 5,
'--actual', actualVersion,
'--expected', expectedVersion,
+ '--actual-source', actualSource,
+ '--expected-source', expectedSource,
'--renderer', renderer || '',
'--use-coarse-pointer', useCoarsePointer,
'--threads', Math.min(threadsCount, CLI_FIXED_THREADS_COUNT),
@@ -225,12 +237,25 @@ function checkPuppeteer() {
async function start() {
+ // Clean PR directories before starting
+ const {cleanPRDirectories} = require('./util');
+ cleanPRDirectories();
+
if (!checkPuppeteer()) {
- // TODO Check version.
console.error(`Can't find puppeteer >= 9.0.0, run 'npm install' to update in the 'test/runTest' folder`);
return;
}
+ let stableVersions;
+ let nightlyVersions;
+ try {
+ stableVersions = await fetchVersions(false, useCNMirror);
+ nightlyVersions = (await fetchVersions(true, useCNMirror)).slice(0, 100);
+ } catch (e) {
+ console.error('Failed to fetch version list:', e);
+ console.log(`Try again later or try the CN mirror with: ${chalk.yellow('npm run test:visual -- -- --useCNMirror')}`)
+ return;
+ }
let _currentTestHash;
let _currentRunConfig;
@@ -241,11 +266,6 @@ async function start() {
// Start a static server for puppeteer open the html test cases.
let {io} = serve();
- const stableVersions = await fetchVersions(false);
- const nightlyVersions = (await fetchVersions(true)).slice(0, 100);
- stableVersions.unshift('local');
- nightlyVersions.unshift('local');
-
io.of('/client').on('connect', async socket => {
let isAborted = false;
@@ -271,20 +291,10 @@ async function start() {
return;
}
- const expectedVersionsList = _currentRunConfig.isExpectedNightly ? nightlyVersions : stableVersions;
- const actualVersionsList = _currentRunConfig.isActualNightly ? nightlyVersions : stableVersions;
- if (!expectedVersionsList.includes(_currentRunConfig.expectedVersion)) {
- // Pick first version not local
- _currentRunConfig.expectedVersion = expectedVersionsList[1];
- }
- if (!actualVersionsList.includes(_currentRunConfig.actualVersion)) {
- _currentRunConfig.actualVersion = 'local';
- }
-
socket.emit('syncRunConfig_return', {
runConfig: _currentRunConfig,
- expectedVersionsList,
- actualVersionsList
+ versions: stableVersions,
+ nightlyVersions: nightlyVersions
});
if (_currentTestHash !== getRunHash(_currentRunConfig)) {
@@ -308,6 +318,7 @@ async function start() {
});
socket.on('genTestsRunReport', async (params) => {
+ console.log('genTestsRunReport', params);
const absPath = await genReport(
path.join(RESULTS_ROOT_DIR, getRunHash(params))
);
@@ -328,8 +339,18 @@ async function start() {
let startTime = Date.now();
- await prepareEChartsLib(data.expectedVersion); // Expected version.
- await prepareEChartsLib(data.actualVersion); // Version to test
+ try {
+ await prepareEChartsLib(data.expectedSource, data.expectedVersion, useCNMirror);
+ await prepareEChartsLib(data.actualSource, data.actualVersion, useCNMirror);
+ }
+ catch (e) {
+ console.error(e);
+ // Send error to client
+ socket.emit('run_error', {
+ message: e.toString()
+ });
+ return;
+ }
// If aborted in the time downloading lib.
if (isAborted) {
@@ -337,30 +358,27 @@ async function start() {
}
// TODO Should broadcast to all sockets.
- try {
- if (!checkStoreVersion(data)) {
- throw new Error('Unmatched store version and run version.');
- }
-
- await startTests(
- data.tests,
- io.of('/client'),
- {
- noHeadless: data.noHeadless,
- threadsCount: data.threads,
- replaySpeed: data.replaySpeed,
- actualVersion: data.actualVersion,
- expectedVersion: data.expectedVersion,
- renderer: data.renderer,
- useCoarsePointer: data.useCoarsePointer,
- noSave: false
- }
- );
- }
- catch (e) {
- console.error(e);
+ if (!checkStoreVersion(data)) {
+ throw new Error('Unmatched store version and run version.');
}
+ await startTests(
+ data.tests,
+ io.of('/client'),
+ {
+ noHeadless: data.noHeadless,
+ threadsCount: data.threads,
+ replaySpeed: data.replaySpeed,
+ actualSource: data.actualSource,
+ actualVersion: data.actualVersion,
+ expectedSource: data.expectedSource,
+ expectedVersion: data.expectedVersion,
+ renderer: data.renderer,
+ useCoarsePointer: data.useCoarsePointer,
+ noSave: false
+ }
+ );
+
if (!isAborted) {
const deltaTime = Date.now() - startTime;
console.log('Finished in ', Math.round(deltaTime / 1000) + ' second');
diff --git a/test/runTest/store.js b/test/runTest/store.js
index 3430bd47e..21d1d2d49 100644
--- a/test/runTest/store.js
+++ b/test/runTest/store.js
@@ -77,9 +77,19 @@ class Test {
* It depends on two versions and rendering mode.
*/
function getRunHash(params) {
+ // Replace # with PR- in the hash to avoid URL issues
+ const expectedVersion = params.expectedSource === 'PR'
+ ? params.expectedVersion.replace('#', 'PR-')
+ : params.expectedVersion;
+ const actualVersion = params.actualSource === 'PR'
+ ? params.actualVersion.replace('#', 'PR-')
+ : params.actualVersion;
+
return [
- params.expectedVersion,
- params.actualVersion,
+ params.expectedSource,
+ expectedVersion,
+ params.actualSource,
+ actualVersion,
params.renderer,
params.useCoarsePointer
].join(TEST_HASH_SPLITTER);
@@ -90,11 +100,21 @@ function getRunHash(params) {
*/
function parseRunHash(str) {
const parts = str.split(TEST_HASH_SPLITTER);
+ // Convert back PR-123 to #123 for PR versions
+ const expectedVersion = parts[0] === 'PR'
+ ? parts[1].replace('PR-', '#')
+ : parts[1];
+ const actualVersion = parts[2] === 'PR'
+ ? parts[3].replace('PR-', '#')
+ : parts[3];
+
return {
- expectedVersion: parts[0],
- actualVersion: parts[1],
- renderer: parts[2],
- useCoarsePointer: parts[3]
+ expectedSource: parts[0],
+ expectedVersion: expectedVersion,
+ actualSource: parts[2],
+ actualVersion: actualVersion,
+ renderer: parts[4],
+ useCoarsePointer: parts[5]
};
}
@@ -102,6 +122,22 @@ function getResultBaseDir() {
return path.join(RESULTS_ROOT_DIR, _runHash);
}
+module.exports.clearStaledResults = async function () {
+ // If split by __ and there is no 6 parts, it is staled.
+ try {
+ const dirs = await globby('*', { cwd: RESULTS_ROOT_DIR, onlyDirectories: true });
+ for (let dir of dirs) {
+ const parts = dir.split(TEST_HASH_SPLITTER);
+ if (parts.length !== 6) {
+ await module.exports.delTestsRun(dir);
+ }
+ }
+ }
+ catch(e) {
+ console.error('Failed to clear staled results', e);
+ }
+}
+
module.exports.getResultBaseDir = getResultBaseDir;
module.exports.getRunHash = getRunHash;
@@ -111,7 +147,9 @@ module.exports.getRunHash = getRunHash;
module.exports.checkStoreVersion = function (runParams) {
const storeParams = parseRunHash(_runHash);
console.log('Store ', _runHash);
- return storeParams.expectedVersion === runParams.expectedVersion
+ return storeParams.expectedSource === runParams.expectedSource
+ && storeParams.expectedVersion === runParams.expectedVersion
+ && storeParams.actualSource === runParams.actualSource
&& storeParams.actualVersion === runParams.actualVersion
&& storeParams.renderer === runParams.renderer
&& storeParams.useCoarsePointer === runParams.useCoarsePointer;
@@ -301,14 +339,22 @@ module.exports.getAllTestsRuns = async function () {
continue;
}
- params.lastRunTime = lastRunTime > 0 ? formatDate(lastRunTime) : 'N/A';
- params.total = total;
- params.passed = passedCount;
- params.finished = finishedCount;
- params.id = dir;
- params.diskSize = convertBytes(await getFolderSize(path.join(RESULTS_ROOT_DIR, dir)));
-
- results.push(params);
+ const runData = {
+ expectedSource: params.expectedSource,
+ expectedVersion: params.expectedVersion,
+ actualSource: params.actualSource,
+ actualVersion: params.actualVersion,
+ renderer: params.renderer,
+ useCoarsePointer: params.useCoarsePointer,
+ lastRunTime: lastRunTime > 0 ? formatDate(lastRunTime) : 'N/A',
+ total: total,
+ passed: passedCount,
+ finished: finishedCount,
+ id: dir,
+ diskSize: convertBytes(await getFolderSize(path.join(RESULTS_ROOT_DIR, dir)))
+ };
+
+ results.push(runData);
};
return results;
}
diff --git a/test/runTest/util.js b/test/runTest/util.js
index 4d892b344..751b6a1bb 100644
--- a/test/runTest/util.js
+++ b/test/runTest/util.js
@@ -42,10 +42,15 @@ module.exports.fileNameFromTest = function (testName) {
return testName + '.html';
};
-function getVersionDir(version) {
+function getVersionDir(source, version) {
version = version || 'local';
+ if (source === 'PR') {
+ // For PR preview artifacts
+ const prNumber = version.replace(/^#/, '');
+ return `tmp/__version__/pr-${prNumber}`;
+ }
return `tmp/__version__/${version}`;
-};
+}
module.exports.getVersionDir = getVersionDir;
module.exports.getActionsFullPath = function (testName) {
@@ -56,58 +61,97 @@ module.exports.getEChartsTestFileName = function () {
return `echarts.test-${config.testVersion}.js`;
};
-module.exports.prepareEChartsLib = function (version) {
+// Clean PR directories at the start of initing because PR code may change
+module.exports.cleanPRDirectories = function () {
+ const baseDir = path.join(__dirname, 'tmp/__version__');
+ if (fs.existsSync(baseDir)) {
+ const dirs = fs.readdirSync(baseDir);
+ dirs.forEach(dir => {
+ if (dir.startsWith('pr-')) {
+ fse.removeSync(path.join(baseDir, dir));
+ }
+ });
+ }
+}
+
+module.exports.prepareEChartsLib = function (source, version, useCNMirror) {
+ console.log(`Preparing ECharts lib: ${source} ${version}`);
- const versionFolder = path.join(__dirname, getVersionDir(version));
+ const versionFolder = path.join(__dirname, getVersionDir(source, version));
const ecDownloadPath = `${versionFolder}/echarts.js`;
+ const testLibPath = `${versionFolder}/${module.exports.getEChartsTestFileName()}`;
+
fse.ensureDirSync(versionFolder);
+
if (!version || version === 'local') {
// Developing version, make sure it's new build
- fse.copySync(path.join(__dirname, '../../dist/echarts.js'), `${versionFolder}/echarts.js`);
+ fse.copySync(path.join(__dirname, '../../dist/echarts.js'), ecDownloadPath);
let code = modifyEChartsCode(fs.readFileSync(ecDownloadPath, 'utf-8'));
- fs.writeFileSync(`${versionFolder}/${module.exports.getEChartsTestFileName()}`, code, 'utf-8');
-
+ fs.writeFileSync(testLibPath, code, 'utf-8');
return Promise.resolve();
}
- return new Promise(resolve => {
- const testLibPath = `${versionFolder}/${module.exports.getEChartsTestFileName()}`;
- if (!fs.existsSync(ecDownloadPath)) {
- const file = fs.createWriteStream(ecDownloadPath);
- const isNightly = version.includes('-dev');
- const packageName = isNightly ? 'echarts-nightly' : 'echarts'
-
- const url = `https://unpkg.com/${packageName}@${version}/dist/echarts.js`;
- console.log(`Downloading ${packageName}@${version} from ${url}`, );
- https.get(url, response => {
- response.pipe(file);
-
- file.on('finish', () => {
- let code = modifyEChartsCode(fs.readFileSync(ecDownloadPath, 'utf-8'));
- fs.writeFileSync(testLibPath, code, 'utf-8');
- resolve();
- });
- });
+ else if (fs.existsSync(ecDownloadPath) && fs.existsSync(testLibPath) &&
+ fs.statSync(ecDownloadPath).size > 0 && fs.statSync(testLibPath).size > 0) {
+ return Promise.resolve();
+ }
+
+ return new Promise((resolve, reject) => {
+ let url;
+
+ if (source === 'PR') {
+ const prNumber = version.replace(/^#/, '');
+ if (!/^\d+$/.test(prNumber)) {
+ reject('Invalid PR number format. Should be #123');
+ return;
+ }
+ url = `https://echarts-pr-${prNumber}.surge.sh/dist/echarts.js`;
}
else {
- // Always do code modifaction.
- // In case we need to do replacement on old downloads.
- let code = modifyEChartsCode(fs.readFileSync(ecDownloadPath, 'utf-8'));
- fs.writeFileSync(testLibPath, code, 'utf-8');
- resolve();
+ const isNightly = source === 'nightly';
+ const packageName = isNightly ? 'echarts-nightly' : 'echarts';
+ url = useCNMirror
+ ? `https://registry.npmmirror.com/${packageName}/${version}/files/dist/echarts.js`
+ : `https://unpkg.com/${packageName}@${version}/dist/echarts.js`;
}
+
+ console.log(`Downloading ECharts from ${url}`);
+ https.get(url, response => {
+ if (response.statusCode === 404) {
+ reject(`PR artifact doesn't exist at ${url}. Make sure the PR build is complete.`);
+ return;
+ }
+
+ let data = '';
+ response.on('data', chunk => {
+ data += chunk;
+ });
+
+ response.on('end', () => {
+ if (!data) {
+ reject(`Downloaded file is empty from ${url}`);
+ return;
+ }
+ fs.writeFileSync(ecDownloadPath, data, 'utf-8');
+ const code = modifyEChartsCode(data);
+ fs.writeFileSync(testLibPath, code, 'utf-8');
+ resolve();
+ });
+ }).on('error', (e) => {
+ reject(`Failed to download from ${url}: ${e}`);
+ });
});
};
-module.exports.fetchVersions = function (isNighlty) {
+module.exports.fetchVersions = function (isNightly, useCNMirror) {
return new Promise((resolve, reject) => {
- https.get(
- isNighlty
- ? `https://registry.npmjs.org/echarts-nightly`
- : `https://registry.npmjs.org/echarts`
- , res => {
+ const npmRegistry = useCNMirror
+ ? 'https://registry.npmmirror.com'
+ : 'https://registry.npmjs.org';
+ const endpoint = `${npmRegistry}/echarts${isNightly ? '-nightly' : ''}`;
+ https.get(endpoint, res => {
if (res.statusCode !== 200) {
res.destroy();
- reject('Failed fetch versions from https://registry.npmjs.org/echarts');
+ reject('status code: ' + res.statusCode);
return;
}
var buffers = [];
@@ -118,10 +162,11 @@ module.exports.fetchVersions = function (isNighlty) {
resolve(Object.keys(JSON.parse(data).versions).reverse());
}
catch (e) {
- reject(e.toString());
+ reject(e);
}
});
- });
+ })
+ .on('error', reject);
});
};
@@ -156,4 +201,4 @@ module.exports.waitTime = function (time) {
resolve();
}, time);
});
-};
\ No newline at end of file
+};
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@echarts.apache.org
For additional commands, e-mail: commits-help@echarts.apache.org