blob: ddaad0d47766a152bd58293c48402713668df118 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { HashMap, createHashMap, each, extend, isArray, isObject } from 'zrender/src/core/util';
import type { NullUndefined } from '../../util/types';
import type { MatrixXYLocator, MatrixDimPair, MatrixXYLocatorRange } from './MatrixDim';
import { error } from '../../util/log';
import Point from 'zrender/src/core/Point';
import { RectLike } from 'zrender/src/core/BoundingRect';
import type { MatrixBodyCornerCellOption, MatrixBodyOption, MatrixCornerOption } from './MatrixModel';
import {
resolveXYLocatorRangeByCellMerge,
MatrixClampOption,
parseCoordRangeOption,
fillIdSpanFromLocatorRange,
createNaNRectLike,
isXYLocatorRangeInvalidOnDim,
resetXYLocatorRange,
cloneXYLocatorRange,
} from './matrixCoordHelper';
import type Model from '../../model/Model';
/**
* Key: @see `makeCellMapKey`
*/
type MatrixModelBodyCornerCellMap = HashMap<MatrixBodyCornerCell, string>;
export type MatrixBodyOrCornerKind = 'body' | 'corner';
type MatrixBodyOrCornerOption<TKind extends MatrixBodyOrCornerKind> =
('body' extends TKind ? MatrixBodyOption : MatrixCornerOption);
export interface MatrixBodyCornerCell {
// Represents col/row, serves as both id and locator.
// Actually its `x` is `xDimCell.id.x`; its `y` is `yDimCell.id.y`
id: Point;
// raw option in `matrix.body/corner.data[i]`.
option: MatrixBodyCornerCellOption | NullUndefined;
// `matrix.body/corner.data[i].coord` can locate a rect of cells (say, area).
// `inSpanOf` refers to the top-left cell, which represents that area.
// The top-left cell has `inSpanOf` refering to itself.
inSpanOf: MatrixBodyCornerCell | NullUndefined;
// If existing, it indicates cell merging, and this cell is the top-left cell
// of the merging area.
cellMergeOwner: boolean;
// Exist only if `cellMergeOwner: true`.
// In this case, it enusres that x > 1 and y > 1 and never out of boundary;
// othewise it is null/undefined.
span: Point | NullUndefined;
// Exist only if `cellMergeOwner: true`.
// Convey the same info with `id`+`span`, but be used in different calculation.
locatorRange: MatrixXYLocatorRange | NullUndefined;
// Exist only if `cellMergeOwner: true`.
spanRect: RectLike | NullUndefined;
}
/**
* Lifetime: the same with `MatrixModel`, but different from `coord/Matrix`.
*/
export class MatrixBodyCorner<TKind extends MatrixBodyOrCornerKind> {
/**
* Be sparse, item exists only if needed.
*/
private _cellMap: MatrixModelBodyCornerCellMap | NullUndefined;
private _cellMergeOwnerList: MatrixBodyCornerCell[];
private _model: Model<MatrixBodyOrCornerOption<TKind>>;
private _dims: MatrixDimPair;
private _kind: TKind;
constructor(
kind: TKind,
bodyOrCornerModel: Model<MatrixBodyOrCornerOption<TKind>>,
dims: MatrixDimPair
) {
this._model = bodyOrCornerModel;
this._dims = dims;
this._kind = kind;
this._cellMergeOwnerList = [];
}
/**
* Can not be called before series models initialization finished, since the ordinalMeta may
* use collect the values from `series.data` in series initialization.
*/
private _ensureCellMap(): MatrixModelBodyCornerCellMap {
const self = this;
let _cellMap = self._cellMap;
if (!_cellMap) {
_cellMap = self._cellMap = createHashMap();
fillCellMap();
}
return _cellMap;
function fillCellMap(): void {
type TmpParsed = {
id: Point;
span: Point;
locatorRange: MatrixXYLocatorRange;
option: MatrixBodyCornerCellOption;
cellMergeOwner: boolean;
};
const parsedList: TmpParsed[] = [];
let cellOptionList = self._model.getShallow('data');
if (cellOptionList && !isArray(cellOptionList)) {
if (__DEV__) {
error(`matrix.${cellOptionList}.data must be an array if specified.`);
}
cellOptionList = null;
}
each(cellOptionList, (option, idx) => {
if (!isObject(option) || !isArray(option.coord)) {
if (__DEV__) {
error(`Illegal matrix.${self._kind}.data[${idx}], must be a {coord: [...], ...}`);
}
return;
}
const locatorRange = resetXYLocatorRange([]);
let reasonArr: string[] | NullUndefined = null;
if (__DEV__) {
reasonArr = [];
}
parseCoordRangeOption(
locatorRange, reasonArr, option.coord, self._dims,
option.coordClamp ? MatrixClampOption[self._kind] : MatrixClampOption.none
);
if (isXYLocatorRangeInvalidOnDim(locatorRange, 0) || isXYLocatorRangeInvalidOnDim(locatorRange, 1)) {
if (__DEV__) {
error(`Can not determine cells by option matrix.${self._kind}.data[${idx}]: `
+ `${reasonArr.join(' ')}`
);
}
return;
}
const cellMergeOwner = option && option.mergeCells;
const parsed: TmpParsed = {id: new Point(), span: new Point(), locatorRange, option, cellMergeOwner};
fillIdSpanFromLocatorRange(parsed, locatorRange);
// The order of the `parsedList` determines the precedence of the styles, if there
// are overlaps between ranges specified in different items. Preserve the original
// order of `matrix.body/corner/data` to make it predictable for users.
parsedList.push(parsed);
});
// Resolve cell merging intersection - union to a larger rect.
const mergedMarkList: boolean[] = [];
for (let parsedIdx = 0; parsedIdx < parsedList.length; parsedIdx++) {
const parsed = parsedList[parsedIdx];
if (!parsed.cellMergeOwner) {
continue;
}
const locatorRange = parsed.locatorRange;
resolveXYLocatorRangeByCellMerge(locatorRange, mergedMarkList, parsedList, parsedIdx);
for (let idx = 0; idx < parsedIdx; idx++) {
if (mergedMarkList[idx]) {
parsedList[idx].cellMergeOwner = false;
}
}
if (locatorRange[0][0] !== parsed.id.x || locatorRange[1][0] !== parsed.id.y) {
// The top-left cell of the unioned locatorRange is not this cell any more.
parsed.cellMergeOwner = false;
// Reconcile: simply use the last style and value option if multiple styles involved
// in a merged area, since there might be no commonly used merge strategy.
const newOption = extend({} as MatrixBodyCornerCellOption, parsed.option);
newOption.coord = null;
const newParsed: TmpParsed = {
id: new Point(),
span: new Point(),
locatorRange,
option: newOption,
cellMergeOwner: true
};
fillIdSpanFromLocatorRange(newParsed, locatorRange);
parsedList.push(newParsed);
}
}
// Assign options to cells.
each(parsedList, parsed => {
const topLeftCell = ensureBodyOrCornerCell(parsed.id.x, parsed.id.y);
if (parsed.cellMergeOwner) {
topLeftCell.cellMergeOwner = true;
topLeftCell.span = parsed.span;
topLeftCell.locatorRange = parsed.locatorRange;
topLeftCell.spanRect = createNaNRectLike();
self._cellMergeOwnerList.push(topLeftCell);
}
if (!parsed.cellMergeOwner && !parsed.option) {
return;
}
for (let yidx = 0; yidx < parsed.span.y; yidx++) {
for (let xidx = 0; xidx < parsed.span.x; xidx++) {
const cell = ensureBodyOrCornerCell(parsed.id.x + xidx, parsed.id.y + yidx);
// If multiple style options are defined on a cell, the later ones takes precedence.
cell.option = parsed.option;
if (parsed.cellMergeOwner) {
cell.inSpanOf = topLeftCell;
}
}
}
});
} // End of fillCellMap
function ensureBodyOrCornerCell(x: MatrixXYLocator, y: MatrixXYLocator): MatrixBodyCornerCell {
const key = makeCellMapKey(x, y);
let cell = _cellMap.get(key);
if (!cell) {
cell = _cellMap.set(key, {
id: new Point(x, y),
option: null,
inSpanOf: null,
span: null,
spanRect: null,
locatorRange: null,
cellMergeOwner: false,
});
}
return cell;
}
}
/**
* Body cells or corner cell are not commonly defined specifically, especially in a large
* table, thus his is a sparse data structure - bodys or corner cells exist only if there
* are options specified to it (in `matrix.body.data` or `matrix.corner.data`);
* otherwise, return `NullUndefined`.
*/
getCell(xy: MatrixXYLocator[]): MatrixBodyCornerCell | NullUndefined {
// Assert xy do not contain NaN
return this._ensureCellMap().get(makeCellMapKey(xy[0], xy[1]));
}
/**
* Only cell existing (has specific definition or props) will be travelled.
*/
travelExistingCells(cb: (cell: MatrixBodyCornerCell) => void): void {
this._ensureCellMap().each(cb);
}
/**
* @param locatorRange Must be the return of `parseCoordRangeOption`.
*/
expandRangeByCellMerge(locatorRange: MatrixXYLocatorRange): void {
if (
!isXYLocatorRangeInvalidOnDim(locatorRange, 0)
&& !isXYLocatorRangeInvalidOnDim(locatorRange, 1)
&& locatorRange[0][0] === locatorRange[0][1]
&& locatorRange[1][0] === locatorRange[1][1]
) {
// If it locates to a single cell, use this quick path to avoid travelling.
// It is based on the fact that any cell is not contained by more than one cell merging rect.
_tmpERBCMLocator[0] = locatorRange[0][0];
_tmpERBCMLocator[1] = locatorRange[1][0];
const cell = this.getCell(_tmpERBCMLocator);
const inSpanOf = cell && cell.inSpanOf;
if (inSpanOf) {
cloneXYLocatorRange(locatorRange, inSpanOf.locatorRange);
return;
}
}
const list = this._cellMergeOwnerList;
resolveXYLocatorRangeByCellMerge(locatorRange, null, list, list.length);
}
}
const _tmpERBCMLocator: MatrixXYLocator[] = [];
function makeCellMapKey(x: MatrixXYLocator, y: MatrixXYLocator): string {
return `${x}|${y}`;
}