100pah commented on code in PR #19807: URL: https://github.com/apache/echarts/pull/19807#discussion_r2071735731
########## src/coord/matrix/MatrixDim.ts: ########## @@ -0,0 +1,206 @@ +/* +* 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[]; +}; + +export type MatrixNodeRawOption = string | MatrixNodeOption; + +export interface MatrixDimOption { + show?: boolean; + data?: MatrixNodeOption[]; +} +export interface MatrixDimRawOption { + data?: MatrixNodeRawOption[]; +} + +export interface MatrixCell { + value: string; + rowId: number; + rowSpan: number; + colId: number; + colSpan: number; +} + +export class MatrixDim { + + 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 = []; Review Comment: Conventionally, model and its internal data structure `option` can only be modified by itself, rather than be modified outside. Modifying an internal data structure belonging to another data structure is error-prone. The `setOption` mechanism do not promise the option instance are never replaced. (I know that problem will not be encountered in this case, it's still not a good practice.) ########## src/coord/matrix/MatrixDim.ts: ########## @@ -0,0 +1,206 @@ +/* +* 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[]; +}; + +export type MatrixNodeRawOption = string | MatrixNodeOption; + +export interface MatrixDimOption { + show?: boolean; + data?: MatrixNodeOption[]; +} +export interface MatrixDimRawOption { Review Comment: Both `MatrixDimRawOption` and `MatrixNodeRawOption ` are not used anywhere. ########## src/coord/matrix/MatrixDim.ts: ########## @@ -0,0 +1,206 @@ +/* +* 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[]; +}; + +export type MatrixNodeRawOption = string | MatrixNodeOption; + +export interface MatrixDimOption { + show?: boolean; + data?: MatrixNodeOption[]; +} +export interface MatrixDimRawOption { + data?: MatrixNodeRawOption[]; +} + +export interface MatrixCell { + value: string; + rowId: number; + rowSpan: number; + colId: number; + colSpan: number; +} + +export class MatrixDim { + + 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(); + } + + 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; + } + + getHeight() { Review Comment: Regarding the naming, should it be `depth` (`depthCount`) rather than `height`? In most cases the term `height` implies vertical. For y axis, the word height is probably confusing. ########## src/coord/matrix/MatrixDim.ts: ########## @@ -0,0 +1,206 @@ +/* +* 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[]; +}; + +export type MatrixNodeRawOption = string | MatrixNodeOption; + +export interface MatrixDimOption { + show?: boolean; + data?: MatrixNodeOption[]; +} +export interface MatrixDimRawOption { + data?: MatrixNodeRawOption[]; +} + +export interface MatrixCell { + value: string; + rowId: number; + rowSpan: number; + colId: number; + colSpan: number; +} + +export class MatrixDim { + + 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(); + } + + 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; + } + + getHeight() { + if (!this._option.show) { + return 0; + } + if (this._height != null) { + return this._height; + } + const data = this._option.data; + if (!data) { + return 0; + } + if (isString(data)) { + return 1; + } + let height = 0; + for (let i = 0; i < data.length; i++) { + height = Math.max(height, this._countHeight(data[i])); + } + this._height = height; + return height; + } + + getCells() { + return this._cells; + } + + 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 Review Comment: I believe that `eslint-disable-next-line eqeqeq` should not be introduced, and `==` should not be used except `== null` or have no other choice. `==` allows `0 == ' '`, `0 == ''`, `['a'] == 'a'`, ..., they might be unexpected and introduce burden to maintenance. In this case, I think we should 1. This input type should be modified to `MatrixNodeOption['value']` rather than `ParsedValue` (actually no parse performed) 2. Change the compare code: + either restrict it to be string and forbid number. + or convert the input to string and compare them by `===`: ```js if (this._cells[i].value === ( isNumber(value) ? value + '' : isString(value) ? value : {} )) { // ... } ``` + And if we allow the input value be a number, we should also allow the `matrix.x.data: [121, 2132]` (be number). Therefore `MatrixNodeOption['value']` should also be modified to `string | number`. ########## src/coord/matrix/MatrixDim.ts: ########## @@ -0,0 +1,206 @@ +/* +* 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[]; +}; + +export type MatrixNodeRawOption = string | MatrixNodeOption; + +export interface MatrixDimOption { + show?: boolean; + data?: MatrixNodeOption[]; +} +export interface MatrixDimRawOption { + data?: MatrixNodeRawOption[]; +} + +export interface MatrixCell { + value: string; + rowId: number; + rowSpan: number; + colId: number; + colSpan: number; +} + +export class MatrixDim { + + 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(); + } + + 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; + } + + getHeight() { + if (!this._option.show) { + return 0; + } + if (this._height != null) { + return this._height; + } + const data = this._option.data; + if (!data) { + return 0; + } + if (isString(data)) { + return 1; + } + let height = 0; + for (let i = 0; i < data.length; i++) { + height = Math.max(height, this._countHeight(data[i])); + } + this._height = height; + return height; + } + + getCells() { + return this._cells; + } + + getCell(value: ParsedValue) { Review Comment: `MatrixNodeOption['value']` is used to identify a node/cell. Actually it should be identical. I think we should forbid the duplicate 'value' (such as, console error) to avoid snowrongly usage and reduce the burden of maintenance (to accept duplicated values). And as a coordinate system, I think a hash map or linear index might need to be provided to find/locate cell quickly in `dataToPoint`, rather than traveling all cells. Suppose we have a series data with length 10000, and layout them in this matrix coordinate system, current impl make the complexity 10000 * matrix_size^2. Unless that scenario is not likely to occur. ########## src/coord/matrix/Matrix.ts: ########## @@ -0,0 +1,182 @@ +/* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ + +import { RectLike } from 'zrender/src/core/BoundingRect'; +import type SeriesModel from '../../model/Series'; +import type { SeriesOnMatrixOptionMixin, SeriesOption } from '../../util/types'; +import { CoordinateSystem, CoordinateSystemMaster } from '../CoordinateSystem'; +import GlobalModel from '../../model/Global'; +import ExtensionAPI from '../../core/ExtensionAPI'; +import MatrixModel from './MatrixModel'; +import { LayoutRect, getLayoutRect } from '../../util/layout'; +import { MatrixDim } from './MatrixDim'; +import { ParsedModelFinder, ParsedModelFinderKnown } from '../../util/model'; + +class Matrix implements CoordinateSystem, CoordinateSystemMaster { + + static readonly dimensions = ['x', 'y', 'value']; + static getDimensionsInfo() { + return ['x', 'y', 'value']; + } + + readonly dimensions = Matrix.dimensions; + readonly type = 'matrix'; + + private _model: MatrixModel; + private _rect: LayoutRect; + private _xDim: MatrixDim; + private _yDim: MatrixDim; + private _lineWidth: number; + + static create(ecModel: GlobalModel, api: ExtensionAPI) { + const matrixList: Matrix[] = []; + + ecModel.eachComponent('matrix', function (matrixModel: MatrixModel) { + const matrix = new Matrix(matrixModel, ecModel, api); + matrixList.push(matrix); + matrixModel.coordinateSystem = matrix; + }); + + ecModel.eachSeries(function (matrixSeries: SeriesModel<SeriesOption & SeriesOnMatrixOptionMixin>) { + if (matrixSeries.get('coordinateSystem') === 'matrix') { + // Inject coordinate system + matrixSeries.coordinateSystem = matrixList[matrixSeries.get('matrixIndex') || 0]; + } + }); + 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')); + } + + getRect(): LayoutRect { + return this._rect; + } + + getDim(dim: 'x' | 'y'): MatrixDim { + return dim === 'x' ? this._xDim : this._yDim; + } + + update(ecModel: GlobalModel, api: ExtensionAPI) { + this.resize(this._model, api); + } + + 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; + } + + 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]; + } + + 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 + ]; + } + + 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 + }; + } + + 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); + + const xCell = this._xDim.getCellByColId(xIdx - yHeight); + const yCell = this._yDim.getCellByColId(yIdx - xHeight); + + return [xCell.colId, yCell.rowId, xCell.colSpan, yCell.rowSpan]; + } + + convertToPixel(ecModel: GlobalModel, finder: ParsedModelFinder, value: [string, string]) { + const coordSys = getCoordSys(finder); + return coordSys === this ? coordSys.dataToPoint(value) : null; + } + + convertFromPixel(ecModel: GlobalModel, finder: ParsedModelFinder, pixel: number[]) { + const coordSys = getCoordSys(finder); + return coordSys === this ? coordSys.pointToData(pixel) : null; + } + + containPoint(point: number[]): boolean { + console.warn('Not implemented.'); Review Comment: It can be implemented easily by rect.contain. ########## src/coord/matrix/MatrixModel.ts: ########## @@ -0,0 +1,88 @@ +/* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ + +import ComponentModel from '../../model/Component'; +import { BoxLayoutOptionMixin, ComponentOption, ItemStyleOption, LabelOption } from '../../util/types'; +import Matrix from './Matrix'; +import { MatrixNodeOption } from './MatrixDim'; + +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; + } + backgroundStyle?: ItemStyleOption; + innerBackgroundStyle?: ItemStyleOption; Review Comment: Naming: in the context of 2d-table, the term "inner", "innerCell" seem not common. Could be "body cell"(my recommendation), "data cell". In html, th td represent "header" and "data"; in other context, "header"-"body" is commonly used. ########## src/coord/matrix/prepareCustom.ts: ########## @@ -0,0 +1,43 @@ +/* +* 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 Matrix from './Matrix'; + +export default function matrixPrepareCustom(coordSys: Matrix) { + const rect = coordSys.getRect(); + + return { + coordSys: { + type: 'matrix', + x: rect.x, + y: rect.y, + width: rect.width, + height: rect.height + }, + api: { + coord: function (data: [string, string]) { + return coordSys.dataToPoint(data); + }, + size: function (data: [string, string]) { Review Comment: This size impl is inconsistent with other coordinate system in definition. The size signature in other coord system is: ```ts 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[]; ``` I think under this definition, `size` is not applicable. But we can add another api, `layout` for coord sys matrix and calender. See `convertToLayout` and `dataToLayout` ########## src/coord/matrix/MatrixDim.ts: ########## @@ -0,0 +1,206 @@ +/* +* 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[]; +}; + +export type MatrixNodeRawOption = string | MatrixNodeOption; + +export interface MatrixDimOption { + show?: boolean; + data?: MatrixNodeOption[]; +} +export interface MatrixDimRawOption { + data?: MatrixNodeRawOption[]; +} + +export interface MatrixCell { + value: string; + rowId: number; + rowSpan: number; + colId: number; + colSpan: number; +} + +export class MatrixDim { + + 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(); + } + + 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; + } + + getHeight() { + if (!this._option.show) { + return 0; + } + if (this._height != null) { + return this._height; + } + const data = this._option.data; + if (!data) { + return 0; + } + if (isString(data)) { + return 1; + } + let height = 0; + for (let i = 0; i < data.length; i++) { + height = Math.max(height, this._countHeight(data[i])); + } + this._height = height; + return height; + } + + getCells() { + return this._cells; + } + + 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]; + } + } + } + + getCellByColId(id: number) { + for (let i = 0; i < this._cells.length; i++) { + if (this._cells[i].colId === id) { + return this._cells[i]; + } + } + } + + 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 _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 }; + } + + let currentColId = colId; + let totalColSpan = 0; + const childrenColSpans = []; + + if (node.children && node.children.length) { + for (const child of node.children) { Review Comment: I'm not sure whether currently the bible used in this version of TS will transpile `for of` to compatible format. But using `each` is clearly more concise and avoid this concern. ########## src/chart/heatmap/HeatmapView.ts: ########## @@ -34,6 +34,7 @@ import type Cartesian2D from '../../coord/cartesian/Cartesian2D'; import type Calendar from '../../coord/calendar/Calendar'; Review Comment: HeatMapSeries.ts need to be modified: ```ts export interface HeatmapSeriesOption { // ... coordinateSystem?: 'cartesian2d' | 'geo' | 'calendar' | 'matrix' } class HeatmapSeriesModel extends SeriesModel<HeatmapSeriesOption> { // ... static readonly dependencies = ['grid', 'geo', 'calendar', 'matrix']; coordinateSystem: Cartesian2D | Geo | Calendar | Matrix; } ``` The same goes for scatter series and custom series - especially `dependencies` ########## src/coord/matrix/Matrix.ts: ########## @@ -0,0 +1,182 @@ +/* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ + +import { RectLike } from 'zrender/src/core/BoundingRect'; +import type SeriesModel from '../../model/Series'; +import type { SeriesOnMatrixOptionMixin, SeriesOption } from '../../util/types'; +import { CoordinateSystem, CoordinateSystemMaster } from '../CoordinateSystem'; +import GlobalModel from '../../model/Global'; +import ExtensionAPI from '../../core/ExtensionAPI'; +import MatrixModel from './MatrixModel'; +import { LayoutRect, getLayoutRect } from '../../util/layout'; +import { MatrixDim } from './MatrixDim'; +import { ParsedModelFinder, ParsedModelFinderKnown } from '../../util/model'; + +class Matrix implements CoordinateSystem, CoordinateSystemMaster { + + static readonly dimensions = ['x', 'y', 'value']; + static getDimensionsInfo() { + return ['x', 'y', 'value']; + } + + readonly dimensions = Matrix.dimensions; + readonly type = 'matrix'; + + private _model: MatrixModel; + private _rect: LayoutRect; + private _xDim: MatrixDim; + private _yDim: MatrixDim; + private _lineWidth: number; + + static create(ecModel: GlobalModel, api: ExtensionAPI) { + const matrixList: Matrix[] = []; + + ecModel.eachComponent('matrix', function (matrixModel: MatrixModel) { + const matrix = new Matrix(matrixModel, ecModel, api); + matrixList.push(matrix); + matrixModel.coordinateSystem = matrix; + }); + + ecModel.eachSeries(function (matrixSeries: SeriesModel<SeriesOption & SeriesOnMatrixOptionMixin>) { + if (matrixSeries.get('coordinateSystem') === 'matrix') { + // Inject coordinate system + matrixSeries.coordinateSystem = matrixList[matrixSeries.get('matrixIndex') || 0]; + } + }); + 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')); Review Comment: Recommend use `matrixModel.get('x', true)`, because `matrixModel.get('x')` will retrieve globalModel 'x' if there is no 'x' in matrixModel, and we should not allow this usage. ########## src/coord/matrix/MatrixDim.ts: ########## @@ -0,0 +1,206 @@ +/* +* 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[]; +}; + +export type MatrixNodeRawOption = string | MatrixNodeOption; + +export interface MatrixDimOption { + show?: boolean; + data?: MatrixNodeOption[]; +} +export interface MatrixDimRawOption { + data?: MatrixNodeRawOption[]; +} + +export interface MatrixCell { + value: string; + rowId: number; + rowSpan: number; + colId: number; + colSpan: number; +} + +export class MatrixDim { + + 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(); + } + + getLeavesCount() { + if (this._leavesCount != null) { + return this._leavesCount; + } + const data = this._option.data; + if (!data) { + this._leavesCount = 0; + return 0; + } + if (isString(data)) { Review Comment: The type declaration does not mention it can be a string, and both `MatrixDimRawOption` and `MatrixNodeRawOption ` are not used anywhere. The type declaration needs to be clarified. The same goes for all of the methods that handle the tree nodes, such as `getHeight`, `_traverseInitCells`. The handling should be consistent. ########## src/coord/matrix/MatrixDim.ts: ########## @@ -0,0 +1,206 @@ +/* +* 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[]; +}; + +export type MatrixNodeRawOption = string | MatrixNodeOption; + +export interface MatrixDimOption { + show?: boolean; + data?: MatrixNodeOption[]; +} +export interface MatrixDimRawOption { + data?: MatrixNodeRawOption[]; +} + +export interface MatrixCell { + value: string; + rowId: number; + rowSpan: number; + colId: number; + colSpan: number; +} + +export class MatrixDim { + + 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(); + } + + 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; + } + + getHeight() { + if (!this._option.show) { + return 0; + } + if (this._height != null) { + return this._height; + } + const data = this._option.data; + if (!data) { + return 0; + } + if (isString(data)) { + return 1; + } + let height = 0; + for (let i = 0; i < data.length; i++) { + height = Math.max(height, this._countHeight(data[i])); + } + this._height = height; + return height; + } + + getCells() { + return this._cells; + } + + 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]; + } + } + } + + getCellByColId(id: number) { + for (let i = 0; i < this._cells.length; i++) { + if (this._cells[i].colId === id) { + return this._cells[i]; + } + } + } + + 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 _traverseInitCells( + node: MatrixNodeOption, + rowId: number, + colId: number = 0 + ): { rowId: number, colId: number } { + if (typeof node === 'string') { Review Comment: Make it consistent: replace all of the `typeof xxx === 'string'` to `isString(xxx)`. This is also good for code compression. ########## src/coord/matrix/MatrixDim.ts: ########## @@ -0,0 +1,206 @@ +/* +* 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[]; Review Comment: Some effects are not good enough. ## Tree depth varies This is commonly used header: <img width="479" alt="image" src="https://github.com/user-attachments/assets/abafdc44-4cbd-40ca-aca7-8229a4706275" /> Currenty this effect is not reasonable enough: <img width="712" alt="image" src="https://github.com/user-attachments/assets/5d2a4f37-c380-48b2-9da4-7439d600c8c1" /> It's preferable to be: <img width="556" alt="image" src="https://github.com/user-attachments/assets/24b50c0f-8328-4e4d-b142-ead43ab70b52" /> ## "Inner cell" span/merge support The case in test/matrix.html is not reasonable enough: why A3 can be cell-merging but the cell (A3, V) can not? <img width="693" alt="image" src="https://github.com/user-attachments/assets/e93ec968-93f3-41fc-885c-fb1ccd5aa81a" /> I think that would be better: <img width="602" alt="image" src="https://github.com/user-attachments/assets/4a3bd62c-0e6c-400c-94b2-7056b2532b08" /> This is also an usage that needs cell merge: <img width="472" alt="image" src="https://github.com/user-attachments/assets/adc1bfce-bcac-4497-a8d3-1bca865fe345" /> ########## src/coord/matrix/MatrixDim.ts: ########## @@ -0,0 +1,206 @@ +/* +* 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[]; +}; + +export type MatrixNodeRawOption = string | MatrixNodeOption; + +export interface MatrixDimOption { + show?: boolean; + data?: MatrixNodeOption[]; +} +export interface MatrixDimRawOption { + data?: MatrixNodeRawOption[]; +} + +export interface MatrixCell { + value: string; + rowId: number; + rowSpan: number; + colId: number; + colSpan: number; +} + +export class MatrixDim { + + 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(); + } + + 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; + } + + getHeight() { + if (!this._option.show) { + return 0; + } + if (this._height != null) { + return this._height; + } + const data = this._option.data; + if (!data) { + return 0; + } + if (isString(data)) { + return 1; + } + let height = 0; + for (let i = 0; i < data.length; i++) { + height = Math.max(height, this._countHeight(data[i])); + } + this._height = height; + return height; + } + + getCells() { + return this._cells; + } + + 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]; + } + } + } + + getCellByColId(id: number) { + for (let i = 0; i < this._cells.length; i++) { + if (this._cells[i].colId === id) { + return this._cells[i]; + } + } + } + + 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 _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 }; + } + + let currentColId = colId; + let totalColSpan = 0; + const childrenColSpans = []; + + 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; + } + + // Create cell for the current node + this._cells.push({ + value: node.value, + rowId, + colId, + rowSpan: 1, + colSpan: totalColSpan + }); + + return { rowId, colId: colId + totalColSpan }; + } + + private _countHeight(node: MatrixNodeOption): number { + if (typeof node === 'string' || !node.children) { Review Comment: Both `_countHeight` and `_countLeaves` are not necessary; they requires extra traveling of the tree. And `getHeight` and `getLeavesCount` can be simplified to one line code. But in fact the depth and the leaves count can be obtained by `_traverseInitCells` ########## src/coord/matrix/MatrixDim.ts: ########## @@ -0,0 +1,206 @@ +/* +* 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[]; +}; + +export type MatrixNodeRawOption = string | MatrixNodeOption; + +export interface MatrixDimOption { + show?: boolean; + data?: MatrixNodeOption[]; +} +export interface MatrixDimRawOption { + data?: MatrixNodeRawOption[]; +} + +export interface MatrixCell { + value: string; + rowId: number; + rowSpan: number; + colId: number; + colSpan: number; +} + +export class MatrixDim { + + 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(); + } + + 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; + } + + getHeight() { + if (!this._option.show) { + return 0; + } + if (this._height != null) { + return this._height; + } + const data = this._option.data; + if (!data) { + return 0; + } + if (isString(data)) { + return 1; + } + let height = 0; + for (let i = 0; i < data.length; i++) { + height = Math.max(height, this._countHeight(data[i])); + } + this._height = height; + return height; + } + + getCells() { + return this._cells; + } + + 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]; + } + } + } + + getCellByColId(id: number) { + for (let i = 0; i < this._cells.length; i++) { + if (this._cells[i].colId === id) { + return this._cells[i]; + } + } + } + + 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 _traverseInitCells( + node: MatrixNodeOption, + rowId: number, + colId: number = 0 Review Comment: This ES default value (`=0`) is never used, and personally not recommended, as it causes different behaviors between `undefined` and `null`, which might bring vulnerability. ########## src/coord/matrix/MatrixDim.ts: ########## @@ -0,0 +1,206 @@ +/* +* 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[]; +}; + +export type MatrixNodeRawOption = string | MatrixNodeOption; + +export interface MatrixDimOption { + show?: boolean; + data?: MatrixNodeOption[]; +} +export interface MatrixDimRawOption { + data?: MatrixNodeRawOption[]; +} + +export interface MatrixCell { + value: string; + rowId: number; + rowSpan: number; + colId: number; + colSpan: number; +} + +export class MatrixDim { + + 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(); + } + + 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; + } + + getHeight() { + if (!this._option.show) { + return 0; + } + if (this._height != null) { + return this._height; + } + const data = this._option.data; + if (!data) { + return 0; + } + if (isString(data)) { + return 1; + } + let height = 0; + for (let i = 0; i < data.length; i++) { + height = Math.max(height, this._countHeight(data[i])); + } + this._height = height; + return height; + } + + getCells() { + return this._cells; + } + + 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]; + } + } + } + + getCellByColId(id: number) { Review Comment: The name `col` is confusing. What does a `col` mean on the y dimension? In `dataToPoint`, getCellByColId is called for y dimension and return the `rowId`. That might be a mistake. It's a common sense that `col` implies horizontal and `row` implies vertical, the same goes for 'x' and 'y' (in the context of echarts). If we intend to treat `matrix.x` and `matrix.y` with the same code in this `MatrixDim.ts`, we can use `col`/`row` or `x`/`y` or `[number, number]`, but do not visit them literally. For example, use this `class XYValue` to map "this dimension"/"cross dimension" to "x"/"y" (or "col"/"row"). The entire `MatrixDim.ts` can be simplified to: ```ts import { each, isString } from 'zrender/src/core/util'; import Point from 'zrender/src/core/Point'; import Model from '../../model/Model'; import { error } from '../../util/log'; export type MatrixNodeOption = { value?: string; children?: MatrixNodeOption[]; }; export interface MatrixDimOption { show?: boolean; data?: MatrixNodeOption[]; } interface MatrixDimModel extends Model<MatrixDimOption> { } export interface MatrixCell { value: string; // col/row id, {x: number, y: number}. id: XYValue; // col/row span, {x: number, y: number} span: XYValue; } const XY = ['x', 'y'] as const; class XYValue extends Point { constructor(dimIdx: MatrixDim['dimIdx'], valueOnThisDim: number, valueOnOtherDim: number) { super(); this.setOnDim(dimIdx, valueOnThisDim); this.setOnDim(1 - dimIdx, valueOnOtherDim); } setOnDim(dimIdx: MatrixDim['dimIdx'], value: number): number { return (this[XY[dimIdx]] = value); } getOnDim(dimIdx: MatrixDim['dimIdx']): number { return this[XY[dimIdx]]; } getOnOtherDim(dimIdx: MatrixDim['dimIdx']): number { return this[XY[1 - dimIdx]]; } } 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; private _model: MatrixDimModel; private _cells: MatrixCell[]; /** * Under the current definition, every leave cell is a unit cell. * `_unitLeaveCells` index equals to cell.id.x/y. */ private _unitLeaveCells: MatrixCell[]; private _cellMap: Record<MatrixNodeOption['value'], MatrixCell>; private _depthCount: number; private _leavesCount: number; constructor(dim: 'x' | 'y', model: MatrixDimModel) { this.dim = dim; this.dimIdx = dim === 'x' ? 0 : 1; this._model = model; this._initCells(); } /** * Return the total span. */ private _initCells(): void { const dim = this.dim; const dimIdx = this.dimIdx; const cells: MatrixCell[] = this._cells = []; const unitLeaveCells: MatrixCell[] = this._unitLeaveCells = []; const cellMap: MatrixDim['_cellMap'] = this._cellMap = {}; let depthCount = 0; this._leavesCount = traverseInitCells(this._model.get('data', true), 0, 0); this._depthCount = depthCount; function traverseInitCells(nodeList: MatrixNodeOption[], branchStartId: number, depth: number): number { if (!nodeList) { return; } depthCount = Math.max(depthCount, depth); let totalSpan = 0; each(nodeList, node => { const singleSpan = 1; const value = isString(node) ? node : node.value; if (cellMap[value]) { if (__DEV__) { error(`Duplicated value "${value}" in matrix.${dim}.data; omit it.`); } return; } const cell = { value, id: new XYValue(dimIdx, branchStartId, depth), span: new XYValue(dimIdx, singleSpan, 1), }; cells.push(cell); unitLeaveCells[cell.id.getOnDim(dimIdx)] = cell; cellMap[value] = cell; const childrenSpan = traverseInitCells(node.children, branchStartId, depth + 1); const subSpan = Math.max(singleSpan, childrenSpan); totalSpan += subSpan; branchStartId += subSpan; }); return totalSpan; } } getLeavesCount(): number { return this._leavesCount; } getDepthCount(): number { return this._depthCount; } getCells(): MatrixCell[] { return this._cells; } getCell(value: MatrixNodeOption['value']): MatrixCell { return this._cellMap[value]; } getCellById(id: number): MatrixCell { return this._unitLeaveCells[id]; } } ``` ########## src/coord/matrix/Matrix.ts: ########## @@ -0,0 +1,182 @@ +/* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ + +import { RectLike } from 'zrender/src/core/BoundingRect'; +import type SeriesModel from '../../model/Series'; +import type { SeriesOnMatrixOptionMixin, SeriesOption } from '../../util/types'; +import { CoordinateSystem, CoordinateSystemMaster } from '../CoordinateSystem'; +import GlobalModel from '../../model/Global'; +import ExtensionAPI from '../../core/ExtensionAPI'; +import MatrixModel from './MatrixModel'; +import { LayoutRect, getLayoutRect } from '../../util/layout'; +import { MatrixDim } from './MatrixDim'; +import { ParsedModelFinder, ParsedModelFinderKnown } from '../../util/model'; + +class Matrix implements CoordinateSystem, CoordinateSystemMaster { + + static readonly dimensions = ['x', 'y', 'value']; + static getDimensionsInfo() { + return ['x', 'y', 'value']; + } + + readonly dimensions = Matrix.dimensions; + readonly type = 'matrix'; + + private _model: MatrixModel; + private _rect: LayoutRect; + private _xDim: MatrixDim; + private _yDim: MatrixDim; + private _lineWidth: number; + + static create(ecModel: GlobalModel, api: ExtensionAPI) { + const matrixList: Matrix[] = []; + + ecModel.eachComponent('matrix', function (matrixModel: MatrixModel) { + const matrix = new Matrix(matrixModel, ecModel, api); + matrixList.push(matrix); + matrixModel.coordinateSystem = matrix; + }); + + ecModel.eachSeries(function (matrixSeries: SeriesModel<SeriesOption & SeriesOnMatrixOptionMixin>) { + if (matrixSeries.get('coordinateSystem') === 'matrix') { + // Inject coordinate system + matrixSeries.coordinateSystem = matrixList[matrixSeries.get('matrixIndex') || 0]; + } + }); + 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')); + } + + getRect(): LayoutRect { + return this._rect; + } + + getDim(dim: 'x' | 'y'): MatrixDim { + return dim === 'x' ? this._xDim : this._yDim; + } + + update(ecModel: GlobalModel, api: ExtensionAPI) { + this.resize(this._model, api); + } + + 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; + } + + 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]; + } + + 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 + ]; + } + + 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 + }; + } + + 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); + + const xCell = this._xDim.getCellByColId(xIdx - yHeight); + const yCell = this._yDim.getCellByColId(yIdx - xHeight); + + return [xCell.colId, yCell.rowId, xCell.colSpan, yCell.rowSpan]; + } + + convertToPixel(ecModel: GlobalModel, finder: ParsedModelFinder, value: [string, string]) { + const coordSys = getCoordSys(finder); + return coordSys === this ? coordSys.dataToPoint(value) : null; + } + + convertFromPixel(ecModel: GlobalModel, finder: ParsedModelFinder, pixel: number[]) { + const coordSys = getCoordSys(finder); + return coordSys === this ? coordSys.pointToData(pixel) : null; Review Comment: In the current impl, the return value `convertFromPixel` and `pointToData` is `[xCell.colId, yCell.rowId, xCell.colSpan, yCell.rowSpan]`, but it's difficult to use it for users. Users can not use the as the input of `convertToPixel`. Currently the return `convertFromPixel` and `pointToData` is restricted to be `number[]`. This restriction is OK in category axis, since it support both raw `string` and a number `OrdinalNumber`(i.e., colId, rowId in this scenario) as the input. Additionally, no test case seems to cover that in `test/matrix.html` and `test/matrix_application.html`. The new PR #21005 support that both accept the original string and colId/rowId number (`OrdinalNumber`) TL;DR, Some memo: If we choose to accept both of them (like cartesian category axis did), both `convertTo/FromPixel` and `sries.data` should accept them. That is, `series.data` should accept `[[1,2,7654], [1,3,9876], [2,1,1823], ...]`, where the `[1,2], [1,3], [2,1]` are ordinal numbers corresponding to the string category value declared in `matrix.x.data`/`matrix.y.data`. `echarts/src/scale/Ordinal.ts` should be involved, since it has provided the infrastructures to handle that and keep consistent with other existing logic. And support these two scenarios below: [scenario 1]: ```js // Only providing `dataset` but no need to provide `matrix.x.data` and `matrix.y.data`, such as: const option = { matrix: {}, series: { type: 'scatter', coordinateSystem: 'matrix', data: [ ['fruit', 'good', 1223], ['bread', 'good', 323], ['milk', 'good', 142], ['fruit', 'medium', 63], ['bread', 'medium', 91], ['milk', 'medium', 45], ['fruit', 'bad', 55], ['bread', 'bad', 15], ['milk', 'bad', 53], ] } }; ``` [scenario 2]: ```js const option = { matrix: { x: {data: ['fruit', 'bread', 'milk']}, y: {data: ['good', 'medium', 'bad']}, }, series: { type: 'scatter', coordinateSystem: 'matrix', // Notice: the enumerable values from database are probably like: 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], ] // where 0-fruit, 1-'bread', 2-'milk'; 0-'good', 1-'medium',2-'bad', rather than save text directly. // `choice_both_idx_text` support to input that format directly. } }; ``` ########## src/coord/matrix/Matrix.ts: ########## @@ -0,0 +1,182 @@ +/* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ + +import { RectLike } from 'zrender/src/core/BoundingRect'; +import type SeriesModel from '../../model/Series'; +import type { SeriesOnMatrixOptionMixin, SeriesOption } from '../../util/types'; +import { CoordinateSystem, CoordinateSystemMaster } from '../CoordinateSystem'; +import GlobalModel from '../../model/Global'; +import ExtensionAPI from '../../core/ExtensionAPI'; +import MatrixModel from './MatrixModel'; +import { LayoutRect, getLayoutRect } from '../../util/layout'; +import { MatrixDim } from './MatrixDim'; +import { ParsedModelFinder, ParsedModelFinderKnown } from '../../util/model'; + +class Matrix implements CoordinateSystem, CoordinateSystemMaster { + + static readonly dimensions = ['x', 'y', 'value']; + static getDimensionsInfo() { + return ['x', 'y', 'value']; + } + + readonly dimensions = Matrix.dimensions; + readonly type = 'matrix'; + + private _model: MatrixModel; + private _rect: LayoutRect; + private _xDim: MatrixDim; + private _yDim: MatrixDim; + private _lineWidth: number; + + static create(ecModel: GlobalModel, api: ExtensionAPI) { + const matrixList: Matrix[] = []; + + ecModel.eachComponent('matrix', function (matrixModel: MatrixModel) { + const matrix = new Matrix(matrixModel, ecModel, api); + matrixList.push(matrix); + matrixModel.coordinateSystem = matrix; + }); + + ecModel.eachSeries(function (matrixSeries: SeriesModel<SeriesOption & SeriesOnMatrixOptionMixin>) { + if (matrixSeries.get('coordinateSystem') === 'matrix') { + // Inject coordinate system + matrixSeries.coordinateSystem = matrixList[matrixSeries.get('matrixIndex') || 0]; + } + }); + 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')); + } + + getRect(): LayoutRect { + return this._rect; + } + + getDim(dim: 'x' | 'y'): MatrixDim { + return dim === 'x' ? this._xDim : this._yDim; + } + + update(ecModel: GlobalModel, api: ExtensionAPI) { + this.resize(this._model, api); + } + + 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; + } + + 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]; + } + + 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 + ]; + } + + dataToRect(data: [string, string]): RectLike { + const xCell = this._xDim.getCell(data[0]); + const yCell = this._yDim.getCell(data[1]); Review Comment: If cell can not be found, error will be thrown. It is not a good design. it makes users to write `try` `catch` to handle the out of range cases. The new PR #21005 uses the `{x: NaN, y: NaN, width: NaN, height: NaN}` to represents illegal or out of bound. That is a consistent way to the existing `convertToPixel` and `convertFromPixel`, and be proper to handle x and y separately. That is, if input `[100, null]` we can get `{x: 123, width: 60, y: NaN, height: NaN}`, where only x dimension value is returned, and y is not relevant. NOTE: Commonly used bounding rect APIs behave differently in this part, such as, HTML `getBoundingClientRect` rect never return null/undefined, and Android `getGlobalVisibleRect` both return a never-null rect and a "valid" info. ########## src/component/matrix/MatrixView.ts: ########## @@ -0,0 +1,180 @@ +/* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ + +import MatrixModel from '../../coord/matrix/MatrixModel'; +import ComponentView from '../../view/Component'; +import { createTextStyle } from '../../label/labelStyle'; +import * as graphic from '../../util/graphic'; + +class MatrixView extends ComponentView { + + static type = 'matrix'; + type = MatrixView.type; + + render(matrixModel: MatrixModel) { + + const group = this.group; + + group.removeAll(); + + this._renderTable(matrixModel); + } + + 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 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 + }, + 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') + }) + ); + } + } + } + + 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' + }) + })); + } + } + } + + // 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 Review Comment: silent: true The same goes for other rect that has no interaction. Otherwise mouse: cursor will be displayed when hovering. ########## src/component/matrix/MatrixView.ts: ########## @@ -0,0 +1,180 @@ +/* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ + +import MatrixModel from '../../coord/matrix/MatrixModel'; +import ComponentView from '../../view/Component'; +import { createTextStyle } from '../../label/labelStyle'; +import * as graphic from '../../util/graphic'; + +class MatrixView extends ComponentView { + + static type = 'matrix'; + type = MatrixView.type; + + render(matrixModel: MatrixModel) { + + const group = this.group; + + group.removeAll(); + + this._renderTable(matrixModel); + } + + 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 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 + }, + 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') + }) + ); + } + } + } + + 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' + }) + })); + } + } + } + + // 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 + })); + } + } + + // Outer border + const backgroundStyle = matrixModel + .getModel('backgroundStyle') + .getItemStyle(); + this.group.add(new graphic.Rect({ + shape: rect, Review Comment: 1. it'd better to clone the rect, otherwise unexpected behavior might happen. zrender does not guarantee no modification on it. 2. `subPixelOptimize` need to performed, otherwise the rect and line would be thicker than expected. 3. Border of the outmost rect should on top (in z-index) to cover the regular cells border, but the background should be lower than the regular cells to avoid cover them. Therefore I think it should be two rects. And the entire z-order handle is a bit complicated, therefore I think user should be allowed two set `z2` option to customize their special cases. ########## src/coord/matrix/MatrixModel.ts: ########## @@ -0,0 +1,88 @@ +/* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ + +import ComponentModel from '../../model/Component'; +import { BoxLayoutOptionMixin, ComponentOption, ItemStyleOption, LabelOption } from '../../util/types'; +import Matrix from './Matrix'; +import { MatrixNodeOption } from './MatrixDim'; + +export interface MatrixOption extends ComponentOption, BoxLayoutOptionMixin { + mainType?: 'matrix'; + x?: { Review Comment: 1. Preferable to not to repeat the code for x and y; use one TS declaration for x, y dimension. 2. These declarations for x/y dim are duplicated with `MatrixDimOption` in `MatrixDim.ts`, need to unify them, only declared in one place. 3. In matrix header cells, it names a prop `itemStyle`, but in "inner cell", that prop is named as 'backgroundStyle' and 'innerBackGroundStyle', but they serves the same feature. Could we unify the name? such as, modify here to also be `backgroundStyle` ########## src/coord/matrix/Matrix.ts: ########## @@ -0,0 +1,182 @@ +/* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ + +import { RectLike } from 'zrender/src/core/BoundingRect'; +import type SeriesModel from '../../model/Series'; +import type { SeriesOnMatrixOptionMixin, SeriesOption } from '../../util/types'; +import { CoordinateSystem, CoordinateSystemMaster } from '../CoordinateSystem'; +import GlobalModel from '../../model/Global'; +import ExtensionAPI from '../../core/ExtensionAPI'; +import MatrixModel from './MatrixModel'; +import { LayoutRect, getLayoutRect } from '../../util/layout'; +import { MatrixDim } from './MatrixDim'; +import { ParsedModelFinder, ParsedModelFinderKnown } from '../../util/model'; + +class Matrix implements CoordinateSystem, CoordinateSystemMaster { + + static readonly dimensions = ['x', 'y', 'value']; + static getDimensionsInfo() { + return ['x', 'y', 'value']; Review Comment: Should be ```js static getDimensionsInfo() { return [ {name: 'x', type: 'ordinal'}, {name: 'y', type: 'ordinal'}, ]; } ``` Then series can read this info and parse series data to ordinal (or say, category), instead of guess type (guesswork may fail) ########## src/coord/matrix/MatrixModel.ts: ########## @@ -0,0 +1,88 @@ +/* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ + +import ComponentModel from '../../model/Component'; +import { BoxLayoutOptionMixin, ComponentOption, ItemStyleOption, LabelOption } from '../../util/types'; +import Matrix from './Matrix'; +import { MatrixNodeOption } from './MatrixDim'; + +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; + } + backgroundStyle?: ItemStyleOption; + innerBackgroundStyle?: ItemStyleOption; +} + +const defaultDimOption = { + show: true, + data: [] as MatrixNodeOption[], + label: { + show: true, + color: '#333' + }, + itemStyle: { + color: 'none', + borderWidth: 1, + borderColor: '#ccc' + } +}; + +class MatrixModel extends ComponentModel<MatrixOption> { + static type = 'matrix'; + type = MatrixModel.type; + + coordinateSystem: Matrix; Review Comment: ```ts static layoutMode = 'box' as const; ``` is needed, otherwise ```js option = { matrix: { bottom: 10, height: 100, // will not work due to the default top } } ``` ########## src/chart/pie/PieSeries.ts: ########## @@ -168,8 +168,9 @@ class PieSeriesModel extends SeriesModel<PieSeriesOption> { * @overwrite */ getInitialData(this: PieSeriesModel): SeriesData { + const isMatrix = this.option.coordinateSystem === 'matrix'; return createSeriesDataSimply(this, { - coordDimensions: ['value'], + coordDimensions: isMatrix ? ['x', 'y', 'value'] : ['value'], Review Comment: not correct. only pie.center is coorsys relevant, rather than series.data ########## src/coord/matrix/Matrix.ts: ########## @@ -0,0 +1,182 @@ +/* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ + +import { RectLike } from 'zrender/src/core/BoundingRect'; +import type SeriesModel from '../../model/Series'; +import type { SeriesOnMatrixOptionMixin, SeriesOption } from '../../util/types'; +import { CoordinateSystem, CoordinateSystemMaster } from '../CoordinateSystem'; +import GlobalModel from '../../model/Global'; +import ExtensionAPI from '../../core/ExtensionAPI'; +import MatrixModel from './MatrixModel'; +import { LayoutRect, getLayoutRect } from '../../util/layout'; +import { MatrixDim } from './MatrixDim'; +import { ParsedModelFinder, ParsedModelFinderKnown } from '../../util/model'; + +class Matrix implements CoordinateSystem, CoordinateSystemMaster { + + static readonly dimensions = ['x', 'y', 'value']; + static getDimensionsInfo() { + return ['x', 'y', 'value']; + } + + readonly dimensions = Matrix.dimensions; + readonly type = 'matrix'; + + private _model: MatrixModel; + private _rect: LayoutRect; + private _xDim: MatrixDim; + private _yDim: MatrixDim; + private _lineWidth: number; + + static create(ecModel: GlobalModel, api: ExtensionAPI) { + const matrixList: Matrix[] = []; + + ecModel.eachComponent('matrix', function (matrixModel: MatrixModel) { + const matrix = new Matrix(matrixModel, ecModel, api); + matrixList.push(matrix); + matrixModel.coordinateSystem = matrix; + }); + + ecModel.eachSeries(function (matrixSeries: SeriesModel<SeriesOption & SeriesOnMatrixOptionMixin>) { + if (matrixSeries.get('coordinateSystem') === 'matrix') { + // Inject coordinate system + matrixSeries.coordinateSystem = matrixList[matrixSeries.get('matrixIndex') || 0]; Review Comment: Follow the convention in cartesian and polar, `matrixId` should also be supported (tho that is missing in calendar) -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected] --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
