blob: a9609fb7c7148ee4d18277f12d40521717b2d68b [file] [log] [blame]
import {
createHashMap,
defaults,
each, eqNaN, isArray, isObject, isString
} from 'zrender/src/core/util';
import Point from 'zrender/src/core/Point';
import OrdinalMeta from '../../data/OrdinalMeta';
import { NullUndefined, OrdinalNumber } from '../../util/types';
import Ordinal from '../../scale/Ordinal';
import {
MatrixDimensionModel, MatrixDimensionCellOption, MatrixDimensionCellLooseOption,
MatrixDimensionLevelOption,
MatrixCoordValueOption,
} from './MatrixModel';
import { WH, XY } from '../../util/graphic';
import { ListIterator } from '../../util/model';
import { RectLike } from 'zrender/src/core/BoundingRect';
import {
createNaNRectLike, setDimXYValue, MatrixCellLayoutInfoType
} from './matrixCoordHelper';
import { error } from '../../util/log';
import { mathMax } from '../../util/number';
export interface MatrixCellLayoutInfo {
type: MatrixCellLayoutInfoType;
// Represents col/row, serves as both id and locator.
// For a `MatrixDimensionCell`:
// it is `setDimXYValue(new Point(), dimIdx, firstLeafLocator, level - _levels.length)`.
// e.g., `id[dimIdx]` is `0` or `1` or `2` or `3` if there are at most `4` leaves in the tree.
// For a `MatrixDimensionLevelInfo`:
// it is `setDimXYValue(new Point(), dimIdx, 0, level - _levels.length)`
// e.g., `id[1 - dimIdx]` is `-3` or `-2` or `-1` if the tree has `3` levels.
// If negative, locate to corner cells; otherwise, locate to body cells.
id: Point;
// By pixel. Computed left-top x (for x dimension) or y (for y dimension).
// Used to locate.
xy: number;
// By pixel. Computed height (for x dimension) or width (for y dimension).
// Used to locate.
wh: number;
dim: MatrixDim;
}
export type MatrixXYLocator = MatrixCellLayoutInfo['id']['x'] | MatrixCellLayoutInfo['id']['y'];
/**
* [[xmin, xmax], [ymin, ymax]]
* For each internal value, be NaN if invalid or out of boundary (never be null/undefined),
* otherwise must be valid locators.
* @see parseCoordRangeOption
* @see resetXYLocatorRange
*/
export type MatrixXYLocatorRange = MatrixXYLocator[][] & {__brand: 'MatrixXYLocatorRange'};
/**
* [[xmin, xmax], [ymin, ymax]], be `NullUndefined` if illegal.
*/
export type MatrixXYCellLayoutInfoRange = (MatrixCellLayoutInfo | NullUndefined)[][];
export interface MatrixDimensionCell extends MatrixCellLayoutInfo {
// Computed col/row span. Always exists and >= 1.
// `span[XY[dimIdx]]` is actually the leaves count of this subtree.
span: Point;
// Start from 0, tree depth.
level: number;
// It is both `MatrixXYLocator` and `OrdinalNumber` and _cell[index].
firstLeafLocator: MatrixXYLocator;
// Used to fatch its raw value by `matrixDim.getOrdinalMeta().category[ordinal]`,
// or feach cell by `_cells[ordinal]`.
// The ordinal of the leaf nodes is the same as `id.getOnDim(0)` for quick query,
// but not the same for non-leaf nodes.
ordinal: OrdinalNumber;
// Normalized raw option of `matrix.x/y.data[i]`, not include any parents option.
// Never be NullUndefined
option: MatrixDimensionCellOption;
// The layout rect for rendering. Available after matrix coordinate system resizing.
rect: RectLike;
}
/**
* Computed properties of a certain tree level.
* In most cases this is used to describe level size or locate corner cells.
*/
export interface MatrixDimensionLevelInfo extends MatrixCellLayoutInfo {
// The raw option of `matrix.levels[i]`
option: MatrixDimensionLevelOption | NullUndefined;
}
export type MatrixDimPair = {
x: MatrixDim;
y: MatrixDim;
};
/**
* Lifetime: the same with `MatrixModel`, but different from `coord/Matrix`.
*/
export class MatrixDim {
// Use it to visit `cell.id` and `cell.span`
readonly dim: 'x' | 'y';
// Must be `0 | 1`, corresponding to 'x' | 'y'
readonly dimIdx: number;
// Under the current definition, every leave corresponds a unit cell,
// and leaves can serve as the locator of cells.
// Therefore make sure:
// - The first `_leavesCount` elements in `_cells` are leaves.
// - `_cells[leaf.id[XY[this.dimIdx]]]` is the leaf itself.
// - Leaves of each subtree are placed together, that is, the leaves of a dimCell are:
// `this._cells.slice(dimCell.firstLeafLocator, dimCell.span[XY[this.dimIdx]])`
private _cells: MatrixDimensionCell[] = [];
// Can be visited by `_levels[cell.level]` or `_levels[cell.id[1 - dimIdx] + _levels.length]`.
// Items are never be null/undefined after initialized.
private _levels: MatrixDimensionLevelInfo[] = [];
private _leavesCount: number;
private _model: MatrixDimensionModel;
private _ordinalMeta: OrdinalMeta;
// Only for uniformly parsing.
private _scale: Ordinal;
private _uniqueValueGen: ReturnType<typeof createUniqueValueGenerator>;
constructor(dim: 'x' | 'y', dimModel: MatrixDimensionModel) {
this.dim = dim;
this.dimIdx = dim === 'x' ? 0 : 1;
this._model = dimModel;
this._uniqueValueGen = createUniqueValueGenerator(dim);
let dimModelData = dimModel.get('data', true);
if (dimModelData != null && !isArray(dimModelData)) {
if (__DEV__) {
error(`Illegal echarts option - matrix.${this.dim}.data must be an array if specified.`);
}
dimModelData = [];
}
if (dimModelData) {
this._initByDimModelData(dimModelData);
}
else {
this._initBySeriesData();
}
}
private _initByDimModelData(dimModelData: MatrixDimensionCellLooseOption[]) {
const self = this;
const _cells = self._cells;
const _levels = self._levels;
const sameLocatorCellsLists: MatrixDimensionCell[][] = []; // Save for sorting.
let _cellCount = 0;
self._leavesCount = traverseInitCells(dimModelData, 0, 0);
postInitCells();
return;
function traverseInitCells(
dimModelData: MatrixDimensionCellLooseOption[] | NullUndefined,
firstLeafLocator: number,
level: number
): number {
let totalSpan = 0;
if (!dimModelData) {
return totalSpan;
}
each(dimModelData, (option, optionIdx) => {
let invalidOption = false;
let cellOption: MatrixDimensionCell['option'];
if (isString(option)) {
cellOption = {value: option};
}
else if (isObject(option)) {
cellOption = option;
if (option.value != null && !isString(option.value)) {
invalidOption = true;
cellOption = {value: null};
}
}
else {
cellOption = {value: null};
if (option != null) {
invalidOption = true;
}
}
if (invalidOption) {
if (__DEV__) {
error(`Illegal echarts option - matrix.${self.dim}.data[${optionIdx}]`
+ ' must be `string | {value: string}`.'
);
}
}
const cell: MatrixDimensionCell = {
type: MatrixCellLayoutInfoType.nonLeaf, // Update to leaf later if it's a leaf.
ordinal: NaN, // Set it later.
level,
firstLeafLocator,
id: new Point(), // Set it in `_initCellsId`.
span: setDimXYValue(new Point(), self.dimIdx, 1, 1),
option: cellOption,
xy: NaN,
wh: NaN,
dim: self,
rect: createNaNRectLike(),
};
_cellCount++;
(sameLocatorCellsLists[firstLeafLocator]
|| (sameLocatorCellsLists[firstLeafLocator] = [])
).push(cell);
if (!_levels[level]) {
// Create a level only if at least one cell exists.
_levels[level] = {
type: MatrixCellLayoutInfoType.level,
xy: NaN, wh: NaN, option: null, id: new Point(), dim: self
};
}
const childrenSpan = traverseInitCells(
cellOption.children, firstLeafLocator, level + 1
);
const subSpan = Math.max(1, childrenSpan);
cell.span[XY[self.dimIdx]] = subSpan;
totalSpan += subSpan;
firstLeafLocator += subSpan;
});
return totalSpan;
}
function postInitCells() {
// Sort to make sure the leaves are at the beginning, so that
// they can be used as the locator of body cells.
const categories: (string | NullUndefined)[] = [];
while (_cells.length < _cellCount) {
for (let locator = 0; locator < sameLocatorCellsLists.length; locator++) {
const cell = sameLocatorCellsLists[locator].pop();
if (cell) {
cell.ordinal = categories.length;
const val = cell.option.value;
categories.push(val);
_cells.push(cell);
self._uniqueValueGen.calcDupBase(val);
}
}
}
self._uniqueValueGen.ensureValueUnique(categories, _cells);
const ordinalMeta = self._ordinalMeta = new OrdinalMeta({
categories: categories,
needCollect: false,
deduplication: false,
});
self._scale = new Ordinal({ordinalMeta});
for (let idx = 0; idx < self._leavesCount; idx++) {
const leaf = self._cells[idx];
leaf.type = MatrixCellLayoutInfoType.leaf;
// Handle the tree level variation: enlarge the span of the leaves to reach the body cells.
leaf.span[XY[1 - self.dimIdx]] = self._levels.length - leaf.level;
}
self._initCellsId();
self._initLevelIdOptions();
}
}
private _initBySeriesData() {
const self = this;
self._leavesCount = 0;
self._levels = [{
type: MatrixCellLayoutInfoType.level,
xy: NaN, wh: NaN, option: null, id: new Point(), dim: self
}];
self._initLevelIdOptions();
const ordinalMeta = self._ordinalMeta = new OrdinalMeta({
needCollect: true,
deduplication: true,
onCollect: (value: unknown, ordinalNumber: number): void => {
const cell = self._cells[ordinalNumber] = {
type: MatrixCellLayoutInfoType.leaf,
ordinal: ordinalNumber,
level: 0,
firstLeafLocator: ordinalNumber,
id: new Point(), // Set it in `_initCellsId`.
span: setDimXYValue(new Point(), self.dimIdx, 1, 1),
// Theoretically `value` is from `dataset` or `series.data`, so it may be any type.
// Do not restrict this case for user's convenience, and here simply convert it to
// string for display.
option: {value: value + ''},
xy: NaN,
wh: NaN,
dim: self,
rect: createNaNRectLike(),
};
self._leavesCount++;
self._setCellId(cell);
},
});
self._scale = new Ordinal({ordinalMeta});
}
private _setCellId(cell: MatrixDimensionCell) {
const levelsLen = this._levels.length;
const dimIdx = this.dimIdx;
setDimXYValue(cell.id, dimIdx, cell.firstLeafLocator, cell.level - levelsLen);
}
private _initCellsId() {
const levelsLen = this._levels.length;
const dimIdx = this.dimIdx;
each(this._cells, cell => {
setDimXYValue(cell.id, dimIdx, cell.firstLeafLocator, cell.level - levelsLen);
});
}
private _initLevelIdOptions() {
const levelsLen = this._levels.length;
const dimIdx = this.dimIdx;
let levelOptionList = this._model.get('levels', true);
levelOptionList = isArray(levelOptionList) ? levelOptionList : [];
each(this._levels, (levelCfg, level) => {
setDimXYValue(levelCfg.id, dimIdx, 0, level - levelsLen);
levelCfg.option = levelOptionList[level];
});
}
shouldShow(): boolean {
return !!this._model.getShallow('show', true);
}
/**
* Iterate leaves (they are layout units) if dimIdx === this.dimIdx.
* Iterate levels if dimIdx !== this.dimIdx.
*/
resetLayoutIterator(
it: ListIterator<MatrixCellLayoutInfo> | NullUndefined,
dimIdx: number,
startLocator?: MatrixXYLocator | NullUndefined,
count?: number | NullUndefined,
): ListIterator<MatrixCellLayoutInfo> {
it = it || new ListIterator<MatrixCellLayoutInfo>();
if (dimIdx === this.dimIdx) {
const len = this._leavesCount;
const startIdx = startLocator != null ? Math.max(0, startLocator) : 0;
count = count != null ? Math.min(count, len) : len;
it.reset(this._cells, startIdx, startIdx + count);
}
else {
const len = this._levels.length;
// Corner locator is from `-this._levels.length` to `-1`.
const startIdx = startLocator != null ? Math.max(0, startLocator + len) : 0;
count = count != null ? Math.min(count, len) : len;
it.reset(this._levels, startIdx, startIdx + count);
}
return it;
}
resetCellIterator(
it?: ListIterator<MatrixDimensionCell>
): ListIterator<MatrixDimensionCell> {
return (it || new ListIterator<MatrixDimensionCell>()).reset(this._cells, 0);
}
resetLevelIterator(
it?: ListIterator<MatrixDimensionLevelInfo>
): ListIterator<MatrixDimensionLevelInfo> {
return (it || new ListIterator<MatrixDimensionLevelInfo>()).reset(this._levels, 0);
}
getLayout(outRect: RectLike, dimIdx: number, locator: MatrixXYLocator): void {
const layout = this.getUnitLayoutInfo(dimIdx, locator);
outRect[XY[dimIdx]] = layout ? layout.xy : NaN;
outRect[WH[dimIdx]] = layout ? layout.wh : NaN;
}
/**
* Get leaf cell or get level info.
* Should be able to return null/undefined if not found on x or y, thus input `dimIdx` is needed.
*/
getUnitLayoutInfo(dimIdx: number, locator: MatrixXYLocator): MatrixCellLayoutInfo | NullUndefined {
return dimIdx === this.dimIdx
? (locator < this._leavesCount ? this._cells[locator] : undefined)
: this._levels[locator + this._levels.length];
}
/**
* Get dimension cell by data, including leaves and non-leaves.
*/
getCell(value: MatrixCoordValueOption): MatrixDimensionCell | NullUndefined {
const ordinal = this._scale.parse(value);
return eqNaN(ordinal) ? undefined : this._cells[ordinal];
}
/**
* Get leaf count or get level count.
*/
getLocatorCount(dimIdx: number): number {
return dimIdx === this.dimIdx ? this._leavesCount : this._levels.length;
}
getOrdinalMeta(): OrdinalMeta {
return this._ordinalMeta;
}
}
function createUniqueValueGenerator(dim: 'x' | 'y') {
const dimUpper = dim.toUpperCase();
const defaultValReg = new RegExp(`^${dimUpper}([0-9]+)$`);
let dupBase = 0;
function calcDupBase(val: string | NullUndefined): void {
let matchResult;
if (val != null && (matchResult = val.match(defaultValReg))) {
dupBase = mathMax(dupBase, +matchResult[1] + 1);
}
}
function makeUniqueValue(): string {
return `${dimUpper}${dupBase++}`;
}
// Duplicated value is allowed, because the `matrix.x/y.data` can be a tree and it's reasonable
// that leaves in different subtrees has the same text. But only the first one is allowed to be
// queried by the text, and the other ones can only be queried by index.
// Additionally, `matrix.x/y.data: [null, null, ...]` is allowed.
function ensureValueUnique(categories: (string | NullUndefined)[], cells: MatrixDimensionCell[]): void {
// A simple way to deduplicate or handle illegal or not specified values to avoid unexpected behaviors.
// The tree structure should not be broken even if duplicated.
const cateMap = createHashMap<true, string>();
for (let idx = 0; idx < categories.length; idx++) {
let value = categories[idx];
// value may be set to NullUndefined by users or if illegal.
if (value == null || cateMap.get(value) != null) {
// Still display the original option.value if duplicated, but loose the ability to query by text.
categories[idx] = value = makeUniqueValue();
cells[idx].option = defaults({value}, cells[idx].option);
}
cateMap.set(value, true);
}
}
return {calcDupBase, ensureValueUnique};
}