| /* | |
| * 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 { getECData } from '../../util/innerStore'; | |
| 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, | |
| dimIdx === 0 ? 'x' : 'y' | |
| ); | |
| } | |
| } | |
| } | |
| 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, | |
| bodyCornerOptionRoot | |
| ); | |
| } | |
| } | |
| } // End of createBodyOrCornerCells | |
| } | |
| type MatrixTargetType = 'x' | 'y' | 'body' | 'corner'; | |
| 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'], | |
| targetType: MatrixTargetType | |
| ): void { | |
| // Do not use getModel - a quick 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 | |
| const triggerEvent = matrixModel.get('triggerEvent', true); | |
| if (cellText) { | |
| let labelSilent = _tmpCellLabelModel.get('silent'); | |
| // By default, silent: false is needed for triggerEvent or tooltip interaction. | |
| if (labelSilent == null) { | |
| labelSilent = !(triggerEvent || 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. | |
| // So we deliberately make rect non-interactive if `silent` is not explicitly specified, | |
| // even if `triggerEvent` is set as `true`. | |
| !cellRect.style || cellRect.style.fill === 'none' || !cellRect.style.fill | |
| ); | |
| } | |
| cellRect.silent = rectSilent; | |
| // Both `cellRect` (typically non-transparent) and `cellText` may trigger events, depending on both | |
| // `matrix.triggerEvent`, `matrix.xxx.silent` and `matrix.xxx.label.silent` settings. | |
| if (triggerEvent && cellRect) { | |
| const eventData = { | |
| componentType: 'matrix' as const, | |
| componentIndex: matrixModel.componentIndex, | |
| matrixIndex: matrixModel.componentIndex, | |
| targetType: targetType, | |
| name: (cellText && cellText.style) ? cellText.style.text : undefined, | |
| value: textValue, | |
| coord: xyLocator.slice() | |
| }; | |
| getECData(cellRect).eventData = eventData; | |
| } | |
| 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; |