blob: 022ba938cd0e253a431ffa1f6375aefd4d97ddb2 [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 MatrixModel, { MatrixBaseCellOption, MatrixCellStyleOption, MatrixOption } from '../../coord/matrix/MatrixModel';
import ComponentView from '../../view/Component';
import { MatrixCellLayoutInfo, MatrixDim, MatrixXYLocator } from '../../coord/matrix/MatrixDim';
import Model from '../../model/Model';
import { NullUndefined } from '../../util/types';
import BoundingRect, { RectLike } from 'zrender/src/core/BoundingRect';
import * as vectorUtil from 'zrender/src/core/vector';
import { RectShape } from 'zrender/src/graphic/shape/Rect';
import { ItemStyleProps } from '../../model/mixin/itemStyle';
import { LineStyleProps } from '../../model/mixin/lineStyle';
import { LineShape } from 'zrender/src/graphic/shape/Line';
import { subPixelOptimize } from 'zrender/src/graphic/helper/subPixelOptimize';
import { Group, Text, Rect, Line, XY, setTooltipConfig, expandOrShrinkRect } from '../../util/graphic';
import { clearTmpModel, ListIterator } from '../../util/model';
import { clone, retrieve2, isFunction, isString } from 'zrender/src/core/util';
import { formatTplSimple } from '../../util/format';
import { invert } from 'zrender/src/core/matrix';
import { MatrixBodyCorner, MatrixBodyOrCornerKind } from '../../coord/matrix/MatrixBodyCorner';
import { setLabelStyle } from '../../label/labelStyle';
import GlobalModel from '../../model/Global';
const round = Math.round;
// When special border style is defined on cell, it
// should be over all of the other borders.
type Z2CellDefault = {normal: number, special: number};
const Z2_BACKGROUND = 0;
const Z2_OUTER_BORDER = 99;
const Z2_BODY_CORNER_CELL_DEFAULT: Z2CellDefault = {normal: 25, special: 100};
const Z2_DIMENSION_CELL_DEFAULT: Z2CellDefault = {normal: 50, special: 125};
class MatrixView extends ComponentView {
static type = 'matrix';
type = MatrixView.type;
render(matrixModel: MatrixModel, ecModel: GlobalModel) {
this.group.removeAll();
const group = this.group;
const coordSys = matrixModel.coordinateSystem;
const rect = coordSys.getRect();
const xDimModel = matrixModel.getDimensionModel('x');
const yDimModel = matrixModel.getDimensionModel('y');
const xDim = xDimModel.dim;
const yDim = yDimModel.dim;
// PENDING:
// reuse the existing text and rect elements for performance?
renderDimensionCells(
group,
matrixModel,
ecModel
);
createBodyAndCorner(
group,
matrixModel,
xDim,
yDim,
ecModel
);
const borderZ2Option = matrixModel.getShallow('borderZ2', true);
const outerBorderZ2 = retrieve2(borderZ2Option, Z2_OUTER_BORDER);
const dividerLineZ2 = outerBorderZ2 - 1;
// Outer border and overall background. Use separate elements because of z-order:
// The overall background should appear below any other elements.
// But in most cases, the outer border and the divider line should be above the normal cell borders -
// especially when cell borders have different colors. But users may highlight some specific cells by
// overstirking their border, in which case it should be above the outer border.
const bgStyle = matrixModel.getModel('backgroundStyle').getItemStyle(
['borderWidth']
);
bgStyle.lineWidth = 0;
const borderStyle = matrixModel.getModel('backgroundStyle').getItemStyle(
['color', 'decal', 'shadowColor', 'shadowBlur', 'shadowOffsetX', 'shadowOffsetY']
);
borderStyle.fill = 'none';
const bgRect = createMatrixRect(rect.clone(), bgStyle, Z2_BACKGROUND);
const borderRect = createMatrixRect(rect.clone(), borderStyle, outerBorderZ2);
bgRect.silent = true;
borderRect.silent = true;
group.add(bgRect);
group.add(borderRect);
// Header split line.
const xDimCell0 = xDim.getUnitLayoutInfo(0, 0);
const yDimCell0 = yDim.getUnitLayoutInfo(1, 0);
if (xDimCell0 && yDimCell0) {
if (xDim.shouldShow()) {
group.add(createMatrixLine(
{
x1: rect.x,
y1: yDimCell0.xy,
x2: rect.x + rect.width,
y2: yDimCell0.xy,
},
xDimModel.getModel('dividerLineStyle').getLineStyle(),
dividerLineZ2,
));
}
if (yDim.shouldShow()) {
group.add(createMatrixLine(
{
x1: xDimCell0.xy,
y1: rect.y,
x2: xDimCell0.xy,
y2: rect.y + rect.height,
},
yDimModel.getModel('dividerLineStyle').getLineStyle(),
dividerLineZ2,
));
}
}
}
}
function renderDimensionCells(group: Group, matrixModel: MatrixModel, ecModel: GlobalModel): void {
renderOnDimension(0);
renderOnDimension(1);
function renderOnDimension(dimIdx: 0 | 1) {
const thisDimModel = matrixModel.getDimensionModel(XY[dimIdx]);
const thisDim = thisDimModel.dim;
if (!thisDim.shouldShow()) {
return;
}
const thisDimBgStyleModel = thisDimModel.getModel('itemStyle');
const thisDimLabelModel = thisDimModel.getModel('label');
const tooltipOption = matrixModel.getShallow('tooltip', true);
const xyLocator: MatrixXYLocator[] = [];
for (const it = thisDim.resetCellIterator(); it.next();) {
const dimCell = it.item;
const shape = {} as RectLike;
BoundingRect.copy(shape, dimCell.rect);
vectorUtil.set(xyLocator, dimCell.id.x, dimCell.id.y);
createMatrixCell(
xyLocator,
matrixModel,
group,
ecModel,
dimCell.option,
thisDimBgStyleModel,
thisDimLabelModel,
thisDimModel,
shape,
dimCell.option.value,
Z2_DIMENSION_CELL_DEFAULT,
tooltipOption
);
}
}
}
function createBodyAndCorner(
group: Group,
matrixModel: MatrixModel,
xDim: MatrixDim,
yDim: MatrixDim,
ecModel: GlobalModel
): void {
createBodyOrCornerCells('body', matrixModel.getBody(), xDim, yDim);
if (xDim.shouldShow() && yDim.shouldShow()) {
createBodyOrCornerCells('corner', matrixModel.getCorner(), yDim, xDim);
}
function createBodyOrCornerCells<TBodyOrCornerKind extends MatrixBodyOrCornerKind>(
bodyCornerOptionRoot: TBodyOrCornerKind,
bodyOrCorner: MatrixBodyCorner<TBodyOrCornerKind>,
dimForCoordX: MatrixDim, // Can be `matrix.y` (transposed) for corners.
dimForCoordY: MatrixDim, // Can be `matrix.x` (trnasposed) for corners.
): void {
// Prevent inheriting from ancestor.
const parentCellModel = new Model(matrixModel.getShallow(bodyCornerOptionRoot, true));
const parentItemStyleModel = parentCellModel.getModel('itemStyle');
const parentLabelModel = parentCellModel.getModel('label');
const itx = new ListIterator<MatrixCellLayoutInfo>();
const ity = new ListIterator<MatrixCellLayoutInfo>();
const xyLocator: number[] = [];
const tooltipOption = matrixModel.getShallow('tooltip', true);
for (dimForCoordY.resetLayoutIterator(ity, 1); ity.next();) {
for (dimForCoordX.resetLayoutIterator(itx, 0); itx.next();) {
const xLayout = itx.item;
const yLayout = ity.item;
vectorUtil.set(xyLocator, xLayout.id.x, yLayout.id.y);
const bodyCornerCell = bodyOrCorner.getCell(xyLocator);
// If in span of an other body or corner cell, never render it.
if (bodyCornerCell && bodyCornerCell.inSpanOf && bodyCornerCell.inSpanOf !== bodyCornerCell) {
continue;
}
const shape = {} as RectLike;
if (bodyCornerCell && bodyCornerCell.span) {
BoundingRect.copy(shape, bodyCornerCell.spanRect);
}
else {
xLayout.dim.getLayout(shape, 0, xyLocator[0]);
yLayout.dim.getLayout(shape, 1, xyLocator[1]);
}
const bodyCornerCellOption = bodyCornerCell ? bodyCornerCell.option : null;
createMatrixCell(
xyLocator,
matrixModel,
group,
ecModel,
bodyCornerCellOption,
parentItemStyleModel,
parentLabelModel,
parentCellModel,
shape,
bodyCornerCellOption ? bodyCornerCellOption.value : null,
Z2_BODY_CORNER_CELL_DEFAULT,
tooltipOption
);
}
}
} // End of createBodyOrCornerCells
}
function createMatrixCell(
xyLocator: MatrixXYLocator[],
matrixModel: MatrixModel,
group: Group,
ecModel: GlobalModel,
cellOption: MatrixBaseCellOption | NullUndefined,
parentItemStyleModel: Model<MatrixCellStyleOption['itemStyle']>,
parentLabelModel: Model<MatrixCellStyleOption['label']>,
parentCellModel: Model<MatrixCellStyleOption>,
shape: RectLike,
textValue: unknown,
zrCellDefault: Z2CellDefault,
tooltipOption: MatrixOption['tooltip'],
): void {
// Do not use getModel for handy performance optimization.
_tmpCellItemStyleModel.option = cellOption ? cellOption.itemStyle : null;
_tmpCellItemStyleModel.parentModel = parentItemStyleModel;
_tmpCellModel.option = cellOption;
_tmpCellModel.parentModel = parentCellModel;
// Use different z2 because special border may be defined in itemStyle.
const z2 = retrieve2(
_tmpCellModel.getShallow('z2'),
(cellOption && cellOption.itemStyle) ? zrCellDefault.special : zrCellDefault.normal
);
const tooltipOptionShow = tooltipOption && tooltipOption.show;
const cellRect = createMatrixRect(shape, _tmpCellItemStyleModel.getItemStyle(), z2);
group.add(cellRect);
const cursorOption = _tmpCellModel.get('cursor');
if (cursorOption != null) {
cellRect.attr('cursor', cursorOption);
}
let cellText: Text | NullUndefined;
if (textValue != null) {
let text = textValue + '';
_tmpCellLabelModel.option = cellOption ? cellOption.label : null;
_tmpCellLabelModel.parentModel = parentLabelModel;
// This is to accept `option.textStyle` as the default.
_tmpCellLabelModel.ecModel = ecModel;
const formatter = _tmpCellLabelModel.getShallow('formatter');
if (formatter) {
const params = {
componentType: 'matrix' as const,
componentIndex: matrixModel.componentIndex,
name: text,
value: textValue as unknown,
coord: xyLocator.slice() as MatrixXYLocator[],
$vars: ['name', 'value', 'coord'] as const
};
if (isString(formatter)) {
text = formatTplSimple(formatter, params);
}
else if (isFunction(formatter)) {
const formattedText = formatter(params);
if (formattedText != null) {
text = formattedText + '';
}
}
}
setLabelStyle(
cellRect,
// Currently do not support other states (`emphasis`, `select`, `blur`)
{normal: _tmpCellLabelModel},
{
defaultText: text,
autoOverflowArea: true,
// By default based on boundingRect. But boundingRect contains borderWidth,
// and borderWidth is half outside the cell. Thus specific `layoutRect` explicitly.
layoutRect: clone(cellRect.shape)
},
);
cellText = cellRect.getTextContent();
if (cellText) {
cellText.z2 = z2 + 1;
const style = cellText.style;
if (style && (style.overflow && style.overflow !== 'none' && style.lineOverflow)) {
// `overflow: 'break'/'breakAll'/'truncate'` does not guarantee prevention of overflow
// when space is insufficient. Use a `clipPath` in such case.
const clipShape = {} as RectLike;
BoundingRect.copy(clipShape, shape);
// `lineWidth` is half outside half inside the bounding rect.
expandOrShrinkRect(clipShape, (cellRect.style?.lineWidth || 0) / 2, true, true);
cellRect.updateInnerText();
cellText.getLocalTransform(_tmpInnerTextTrans);
invert(_tmpInnerTextTrans, _tmpInnerTextTrans);
BoundingRect.applyTransform(clipShape, clipShape, _tmpInnerTextTrans);
cellText.setClipPath(new Rect({shape: clipShape}));
}
}
setTooltipConfig({ // At least for text overflow.
el: cellRect,
componentModel: matrixModel,
itemName: text,
itemTooltipOption: tooltipOption,
formatterParamsExtra: {
xyLocator: xyLocator.slice()
}
});
}
// Set silent
if (cellText) {
let labelSilent = _tmpCellLabelModel.get('silent');
// auto, tooltip of text cells need silient: false, but non-text cells
// do not need a special cursor in most cases.
if (labelSilent == null) {
labelSilent = !tooltipOptionShow;
}
cellText.silent = labelSilent;
cellText.ignoreHostSilent = true;
}
let rectSilent = _tmpCellModel.get('silent');
if (rectSilent == null) {
rectSilent = (
// If no background color in cell, set `rect.silent: false` will cause that only
// the border response to mouse hovering, which is probably weird.
!cellRect.style || cellRect.style.fill === 'none' || !cellRect.style.fill
);
}
cellRect.silent = rectSilent;
clearTmpModel(_tmpCellModel);
clearTmpModel(_tmpCellItemStyleModel);
clearTmpModel(_tmpCellLabelModel);
}
const _tmpCellModel = new Model<MatrixCellStyleOption>();
const _tmpCellItemStyleModel = new Model<MatrixCellStyleOption['itemStyle']>();
const _tmpCellLabelModel = new Model<MatrixCellStyleOption['label']>();
const _tmpInnerTextTrans: number[] = [];
// FIXME: move all of the subpixel process to Matrix.ts resize, otherwise the result of
// `dataToLayout` is not consistent with this rendering, and the caller (like heatmap) can
// not precisely align with the matrix border.
function createMatrixRect(
shape: RectShape, style: ItemStyleProps, z2: number
): Rect {
// Currently `subPixelOptimizeRect` can not be used here because it will break rect alignment.
// Optimize line and rect with the same direction.
const lineWidth = style.lineWidth;
if (lineWidth) {
const x2Original = shape.x + shape.width;
const y2Original = shape.y + shape.height;
shape.x = subPixelOptimize(shape.x, lineWidth, true);
shape.y = subPixelOptimize(shape.y, lineWidth, true);
shape.width = subPixelOptimize(x2Original, lineWidth, true) - shape.x;
shape.height = subPixelOptimize(y2Original, lineWidth, true) - shape.y;
}
return new Rect({
shape,
style: style,
z2,
});
}
function createMatrixLine(shape: Omit<LineShape, 'percent'>, style: LineStyleProps, z2: number): Line {
const lineWidth = style.lineWidth;
if (lineWidth) {
if (round(shape.x1 * 2) === round(shape.x2 * 2)) {
shape.x1 = shape.x2 = subPixelOptimize(shape.x1, lineWidth, true);
}
if (round(shape.y1 * 2) === round(shape.y2 * 2)) {
shape.y1 = shape.y2 = subPixelOptimize(shape.y1, lineWidth, true);
}
}
return new Line({
shape,
style,
silent: true,
z2,
});
}
export default MatrixView;