/
githubmirror
/
echarts
Обзор
Документация
Войти
/
githubmirror
/
echarts
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/coord/matrix/Matrix.ts
631 строка
24 KB
100pah
feature&fix(axis):
27 фев 2026, 10:19
27 фев 2026, 10:19
8de2b64
Код
Авторство
О чём код?
/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { RectLike } from 'zrender/src/core/BoundingRect'; import type { CoordinateSystemDataLayout, NullUndefined, OrdinalNumber } from '../../util/types'; import { CoordinateSystem, CoordinateSystemMaster } from '../CoordinateSystem'; import GlobalModel from '../../model/Global'; import ExtensionAPI from '../../core/ExtensionAPI'; import MatrixModel, { MatrixCoordRangeOption, MatrixDimensionCellOption, MatrixDimensionLevelOption, MatrixDimensionModel } from './MatrixModel'; import { LayoutRect, getLayoutRect } from '../../util/layout'; import { ListIterator, ParsedModelFinder, ParsedModelFinderKnown } from '../../util/model'; import { eqNaN, isArray, retrieve2 } from 'zrender/src/core/util'; import Point from 'zrender/src/core/Point'; import { WH, XY } from '../../util/graphic'; import Model from '../../model/Model'; import type { MatrixCellLayoutInfo, MatrixDimensionCell, MatrixDimPair, MatrixXYLocator } from './MatrixDim'; import { mathMax, mathMin, parsePositionSizeOption } from '../../util/number'; import { createNaNRectLike, MatrixClampOption, MatrixCellLayoutInfoType, parseCoordRangeOption, resetXYLocatorRange, xyLocatorRangeToRectOneDim } from './matrixCoordHelper'; import type { MatrixBodyCorner, MatrixBodyOrCornerKind } from './MatrixBodyCorner'; import { error } from '../../util/log'; import { injectCoordSysByOption, simpleCoordSysInjectionProvider } from '../../core/CoordinateSystem'; class Matrix implements CoordinateSystem, CoordinateSystemMaster { static readonly dimensions = ['x', 'y', 'value']; /** * @see fetchers in `model/referHelper.ts`, * which is used to parse data in ordinal way. * In most series only 'x' and 'y' is required, * but some series, such as heatmap, can specify value. */ static getDimensionsInfo() { return [ {name: 'x', type: 'ordinal' as const}, {name: 'y', type: 'ordinal' as const}, {name: 'value'}, ]; } readonly dimensions = Matrix.dimensions; readonly type = 'matrix'; private _model: MatrixModel; private _dimModels: { x: MatrixDimensionModel; y: MatrixDimensionModel; }; private _dims: MatrixDimPair; private _rect: LayoutRect; static create(ecModel: GlobalModel, api: ExtensionAPI) { const matrixList: Matrix[] = []; ecModel.eachComponent('matrix', function (matrixModel: MatrixModel) { const matrix = new Matrix(matrixModel, ecModel, api); matrixList.push(matrix); matrixModel.coordinateSystem = matrix; }); // Inject coordinate system // PENDING: optimize to not to travel all components? // (collect relevant components in ecModel only when model update?) ecModel.eachComponent((mainType, componentModel) => { injectCoordSysByOption({ targetModel: componentModel, coordSysType: 'matrix', coordSysProvider: simpleCoordSysInjectionProvider, }); }); return matrixList; } constructor(matrixModel: MatrixModel, ecModel: GlobalModel, api: ExtensionAPI) { this._model = matrixModel; const models = this._dimModels = { x: matrixModel.getDimensionModel('x'), y: matrixModel.getDimensionModel('y'), }; this._dims = { x: models.x.dim, y: models.y.dim, }; this._resize(matrixModel, api); } getRect(): LayoutRect { return this._rect; } private _resize(matrixModel: MatrixModel, api: ExtensionAPI) { const dims = this._dims; const dimModels = this._dimModels; const rect = this._rect = getLayoutRect(matrixModel.getBoxLayoutParams(), { width: api.getWidth(), height: api.getHeight(), }); layOutUnitsOnDimension(dimModels, dims, rect, 0); layOutUnitsOnDimension(dimModels, dims, rect, 1); layOutDimCellsRestInfoByUnit(0, dims); layOutDimCellsRestInfoByUnit(1, dims); layOutBodyCornerCellMerge(this._model.getBody(), dims); layOutBodyCornerCellMerge(this._model.getCorner(), dims); } /** * @implement * - The input is allowed to be `[NaN/null/undefined, xxx]`/`[xxx, NaN/null/undefined]`; * the return is `[NaN, xxxresult]`/`[xxxresult, NaN]` or clamped boundary value if * `clamp` passed. This is for the usage that only get coord on single x or y. * - Alwasy return an numeric array, but never be null/undefined. * If it can not be located or invalid, return `[NaN, NaN]`. */ dataToPoint( data: MatrixCoordRangeOption[], opt?: Parameters<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; } /** * @implement * - The input is allowed to be `[NaN/null/undefined, xxx]`/`[xxx, NaN/null/undefined]`; * the return is `{x: NaN, width: NaN, y: xxxresulty, height: xxxresulth}`/ * `{y: NaN, height: NaN, x: xxxresultx, width: xxxresultw}` or clamped boundary value * if `clamp` passed. This is for the usage that only get coord on single x or y. * - The returned `out.rect` and `out.matrixXYLocatorRange` is always an object or an 2d-array, * but never be null/undefined. If it cannot be located or invalid, `NaN` is in their * corresponding number props. * - Do not provide `out.contentRect`, because it's allowed to input non-leaf dimension x/y or * a range of x/y, which determines a rect covering multiple cells (even not merged), in which * case the padding and borderWidth can not be determined to make a contentRect. Therefore only * return `out.rect` in any case for consistency. The caller is responsible for adding space to * avoid covering cell borders, if necessary. */ dataToLayout( data: MatrixCoordRangeOption[], opt?: { // No clamp by default, considering the possibility of supporting dataZoom (overflow/scroll). clamp?: MatrixClampOption | NullUndefined; // Expand if cell merging is encountered. // - `false`: If intersecting with a rect of merged cells, expand the result to cover it. // This is the default option, becuase `series.data` do not support the format // `MatrixCoordRangeOption` (e.g., `[[3,5], [5,8]]`), thus merged cells can only // be located by single cell locators (e.g., `[3, 5]`). // - `true`: regardless of cell merging, even if the resulting rect spans accorss the merged cells. ignoreMergeCells?: boolean; }, out?: CoordinateSystemDataLayout ): CoordinateSystemDataLayout { const dims = this._dims; out = out || {} as CoordinateSystemDataLayout; const outRect = out.rect = out.rect || {} as RectLike; outRect.x = outRect.y = outRect.width = outRect.height = NaN; const outLocRange = out.matrixXYLocatorRange = resetXYLocatorRange(out.matrixXYLocatorRange); if (!isArray(data)) { if (__DEV__) { error('Input data must be an array in `convertToLayout`, `convertToPixel`'); } return out; } parseCoordRangeOption( outLocRange, null, data, dims, retrieve2(opt && opt.clamp, MatrixClampOption.none) ); if (!opt || !opt.ignoreMergeCells) { if (!opt || opt.clamp !== MatrixClampOption.corner) { this._model.getBody().expandRangeByCellMerge(outLocRange); } if (!opt || opt.clamp !== MatrixClampOption.body) { this._model.getCorner().expandRangeByCellMerge(outLocRange); } } xyLocatorRangeToRectOneDim(outRect, outLocRange, dims, 0); xyLocatorRangeToRectOneDim(outRect, outLocRange, dims, 1); return out; } /** * The returned locator pair can be the input of `dataToPoint` or `dataToLayout`. * * If point[0] is out of the matrix rect, * the out[0] is NaN; * else if it is on the right of top-left corner of body, * the out[0] is the oridinal number (>= 0). * else * out[0] is the locator for corner or header (<= 0). * * The same rule goes for point[1] and out[1]. * * But point[0] and point[1] are calculated separately, i.e., * the reuslt can be `[1, NaN]` or `[NaN, 1]` if only one dimension is out of boundary. * * @implement */ pointToData( point: number[], opt?: { clamp?: MatrixClampOption | NullUndefined }, out?: MatrixXYLocator[] ): MatrixXYLocator[] { const dims = this._dims; pointToDataOneDimPrepareCtx(_tmpCtxPointToData, 0, dims, point, opt && opt.clamp); pointToDataOneDimPrepareCtx(_tmpCtxPointToData, 1, dims, point, opt && opt.clamp); out = out || []; out[0] = out[1] = NaN; if (_tmpCtxPointToData.y === CtxPointToDataAreaType.inCorner && _tmpCtxPointToData.x === CtxPointToDataAreaType.inBody ) { pointToDataOnlyHeaderFillOut(_tmpCtxPointToData, out, 0, dims); } else if (_tmpCtxPointToData.x === CtxPointToDataAreaType.inCorner && _tmpCtxPointToData.y === CtxPointToDataAreaType.inBody ) { pointToDataOnlyHeaderFillOut(_tmpCtxPointToData, out, 1, dims); } else { pointToDataBodyCornerFillOut(_tmpCtxPointToData, out, 0, dims); pointToDataBodyCornerFillOut(_tmpCtxPointToData, out, 1, dims); } return out; } convertToPixel( ecModel: GlobalModel, finder: ParsedModelFinder, value: Parameters<Matrix['dataToPoint']>[0], opt?: Parameters<Matrix['dataToPoint']>[1], ): ReturnType<Matrix['dataToPoint']> | NullUndefined { const coordSys = getCoordSys(finder); return coordSys === this ? coordSys.dataToPoint(value, opt) : undefined; } 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.dataToLayout(value, opt) : undefined; } 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, 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, 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); 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; if (span[XY[dimIdx]] > 1) { const layoutUnit2 = dim.getUnitLayoutInfo(dimIdx, id[XY[dimIdx]] + span[XY[dimIdx]] - 1); // Be careful the cumulative error - cell must be aligned. outRect[WH[dimIdx]] = layoutUnit2.xy + layoutUnit2.wh - layoutUnit.xy; } } /** * Return NaN if not defined or invalid. */ function parseSizeOption( sizeOption: unknown, dimIdx: number, matrixRect: RectLike, ): number { const sizeNum = parsePositionSizeOption(sizeOption, matrixRect[WH[dimIdx]]); return confineSize(sizeNum, matrixRect[WH[dimIdx]]); } function confineSize( sizeNum: number, sizeLimit?: number, ): number { return Math.max(Math.min(sizeNum, retrieve2(sizeLimit, Infinity)), 0); } function getCoordSys(finder: ParsedModelFinderKnown): Matrix { const matrixModel = finder.matrixModel as MatrixModel; const seriesModel = finder.seriesModel; const coordSys = matrixModel ? matrixModel.coordinateSystem : seriesModel ? seriesModel.coordinateSystem : null; return coordSys as Matrix; } const CtxPointToDataAreaType = {inBody: 1, inCorner: 2, outside: 3}; type CtxPointToDataAreaType = (typeof CtxPointToDataAreaType)[keyof typeof CtxPointToDataAreaType]; type CtxPointToData = { x: CtxPointToDataAreaType | NullUndefined; y: CtxPointToDataAreaType | NullUndefined; point: number[]; // If clamp required, this point is clamped after prepared. }; // For quick 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;