diff --git a/packages/charts/src/chart_types/heatmap/layout/types/viewmodel_types.ts b/packages/charts/src/chart_types/heatmap/layout/types/viewmodel_types.ts index 0d198c4322..805b657fb8 100644 --- a/packages/charts/src/chart_types/heatmap/layout/types/viewmodel_types.ts +++ b/packages/charts/src/chart_types/heatmap/layout/types/viewmodel_types.ts @@ -9,6 +9,7 @@ import { ChartType } from '../../..'; import { Color, Colors } from '../../../../common/colors'; import { Pixels } from '../../../../common/geometry'; +import { PerPanelMap } from '../../../../common/panel_utils'; import { Box, Font, TextAlign } from '../../../../common/text_utils'; import { Fill, Line, Rect, Stroke } from '../../../../geoms/types'; import { HeatmapBrushEvent } from '../../../../specs/settings'; @@ -47,7 +48,7 @@ export interface TextBox extends Box { } /** @internal */ -export interface HeatmapViewModel { +export interface HeatmapViewModel extends PerPanelMap { gridOrigin: { x: number; y: number; @@ -62,6 +63,8 @@ export interface HeatmapViewModel { xValues: Array; yValues: Array; pageSize: number; + primaryRow: boolean; + primaryColumn: boolean; titles: Array< Font & Visible & { @@ -111,7 +114,7 @@ export type DragShape = ReturnType; /** @internal */ export type ShapeViewModel = { theme: HeatmapStyle; - heatmapViewModel: HeatmapViewModel; + heatmapViewModels: HeatmapViewModel[]; pickQuads: PickFunction; pickDragArea: PickDragFunction; pickDragShape: PickDragShapeFunction; @@ -120,29 +123,10 @@ export type ShapeViewModel = { pickCursorBand: PickCursorBand; }; -/** @internal */ -export const nullHeatmapViewModel: HeatmapViewModel = { - gridOrigin: { - x: 0, - y: 0, - }, - gridLines: { - x: [], - y: [], - stroke: { width: 0, color: Colors.Transparent.rgba }, - }, - cells: [], - xValues: [], - yValues: [], - pageSize: 0, - cellFontSize: () => 0, - titles: [], -}; - /** @internal */ export const nullShapeViewModel = (): ShapeViewModel => ({ theme: LIGHT_THEME.heatmap, - heatmapViewModel: nullHeatmapViewModel, + heatmapViewModels: [], pickQuads: () => [], pickDragArea: () => ({ cells: [], x: [], y: [], chartType: ChartType.Heatmap }), pickDragShape: () => ({ x: 0, y: 0, width: 0, height: 0 }), diff --git a/packages/charts/src/chart_types/heatmap/layout/viewmodel/scenegraph.ts b/packages/charts/src/chart_types/heatmap/layout/viewmodel/scenegraph.ts index 2b4a31c2d5..d58936ae5b 100644 --- a/packages/charts/src/chart_types/heatmap/layout/viewmodel/scenegraph.ts +++ b/packages/charts/src/chart_types/heatmap/layout/viewmodel/scenegraph.ts @@ -6,24 +6,39 @@ * Side Public License, v 1. */ +import { SmallMultipleScales } from '../../../../common/panel_utils'; import { withTextMeasure } from '../../../../utils/bbox/canvas_text_bbox_calculator'; import { Theme } from '../../../../utils/themes/theme'; +import { ChartDimensions } from '../../../xy_chart/utils/dimensions'; import { ShapeViewModel } from '../../layout/types/viewmodel_types'; import { shapeViewModel } from '../../layout/viewmodel/viewmodel'; import { HeatmapSpec } from '../../specs'; -import { ChartElementSizes, HeatmapTable } from '../../state/selectors/compute_chart_dimensions'; +import { ChartElementSizes } from '../../state/selectors/compute_chart_element_sizes'; import { ColorScale } from '../../state/selectors/get_color_scale'; +import { HeatmapTable } from '../../state/selectors/get_heatmap_table'; /** @internal */ -export function render( +export function computeScenegraph( spec: HeatmapSpec, + chartDimensions: ChartDimensions, elementSizes: ChartElementSizes, + smScales: SmallMultipleScales, heatmapTable: HeatmapTable, colorScale: ColorScale, bandsToHide: Array<[number, number]>, theme: Theme, ): ShapeViewModel { return withTextMeasure((measureText) => { - return shapeViewModel(measureText, spec, theme, elementSizes, heatmapTable, colorScale, bandsToHide); + return shapeViewModel( + measureText, + spec, + theme, + chartDimensions, + elementSizes, + heatmapTable, + colorScale, + smScales, + bandsToHide, + ); }); } diff --git a/packages/charts/src/chart_types/heatmap/layout/viewmodel/viewmodel.ts b/packages/charts/src/chart_types/heatmap/layout/viewmodel/viewmodel.ts index 9d387a5750..fb97cb5fe9 100644 --- a/packages/charts/src/chart_types/heatmap/layout/viewmodel/viewmodel.ts +++ b/packages/charts/src/chart_types/heatmap/layout/viewmodel/viewmodel.ts @@ -12,6 +12,7 @@ import { ScaleBand, scaleBand, scaleQuantize } from 'd3-scale'; import { colorToRgba } from '../../../../common/color_library_wrappers'; import { fillTextColor } from '../../../../common/fill_text_color'; import { Pixels } from '../../../../common/geometry'; +import { getPanelSize, getPerPanelMap, SmallMultipleScales, SmallMultiplesDatum } from '../../../../common/panel_utils'; import { Box, Font, maximiseFontSize } from '../../../../common/text_utils'; import { ScaleType } from '../../../../scales/constants'; import { LinearScale, OrdinalScale, RasterTimeScale } from '../../../../specs'; @@ -22,9 +23,11 @@ import { innerPad, pad } from '../../../../utils/dimensions'; import { Logger } from '../../../../utils/logger'; import { HeatmapStyle, Theme, Visible } from '../../../../utils/themes/theme'; import { PrimitiveValue } from '../../../partition_chart/layout/utils/group_by_rollup'; +import { ChartDimensions } from '../../../xy_chart/utils/dimensions'; import { HeatmapSpec } from '../../specs'; -import { ChartElementSizes, HeatmapTable } from '../../state/selectors/compute_chart_dimensions'; +import { ChartElementSizes } from '../../state/selectors/compute_chart_element_sizes'; import { ColorScale } from '../../state/selectors/get_color_scale'; +import { HeatmapTable } from '../../state/selectors/get_heatmap_table'; import { Cell, GridCell, @@ -38,7 +41,7 @@ import { import { BaseDatum } from './../../../xy_chart/utils/specs'; /** @public */ -export interface HeatmapCellDatum { +export interface HeatmapCellDatum extends SmallMultiplesDatum { x: NonNullable; y: NonNullable; value: number; @@ -60,9 +63,11 @@ export function shapeViewModel( textMeasure: TextMeasure, spec: HeatmapSpec, { heatmap: heatmapTheme, axes: { axisTitle }, background }: Theme, + { chartDimensions }: ChartDimensions, elementSizes: ChartElementSizes, heatmapTable: HeatmapTable, colorScale: ColorScale, + smScales: SmallMultipleScales, bandsToHide: Array<[number, number]>, ): ShapeViewModel { const gridStrokeWidth = heatmapTheme.grid.stroke.width ?? 1; @@ -77,31 +82,32 @@ export function shapeViewModel( ...heatmapTheme.yAxisLabel, })); + const panelSize = getPanelSize(smScales); // compute the scale for the rows positions - const yScale = scaleBand>().domain(yValues).range([0, elementSizes.fullHeatmapHeight]); + // const yScale = scaleBand>().domain(yValues).range([0, elementSizes.fullHeatmapHeight]); + const yScale = scaleBand>().domain(yValues).range([0, panelSize.height]); const yInvertedScale = scaleQuantize>() - .domain([0, elementSizes.fullHeatmapHeight]) + // .domain([0, elementSizes.fullHeatmapHeight]) + .domain([0, panelSize.height]) .range(yValues); // compute the scale for the columns positions - const xScale = scaleBand>().domain(xValues).range([0, elementSizes.grid.width]); + // const xScale = scaleBand>().domain(xValues).range([0, chartDimensions.width]); + const xScale = scaleBand>().domain(xValues).range([0, panelSize.width]); - const xInvertedScale = scaleQuantize>() - .domain([0, elementSizes.grid.width]) - .range(xValues); + // const xInvertedScale = scaleQuantize>().domain([0, chartDimensions.width]).range(xValues); + const xInvertedScale = scaleQuantize>().domain([0, panelSize.width]).range(xValues); - // compute the cell width (can be smaller then the available size depending on config + // compute the cell width, can be smaller then the available size depending on config const cellWidth = heatmapTheme.cell.maxWidth !== 'fill' && xScale.bandwidth() > heatmapTheme.cell.maxWidth ? heatmapTheme.cell.maxWidth : xScale.bandwidth(); - // compute the cell height (we already computed the max size for that) + // compute the cell height, we already computed the max size for that const cellHeight = yScale.bandwidth(); - const currentGridHeight = elementSizes.grid.height; - // compute the position of each column label const textXValues = getXTicks(spec, heatmapTheme.xAxisLabel, xScale, heatmapTable.xValues); @@ -185,11 +191,11 @@ export function shapeViewModel( * @param y */ const pickGridCell = (x: Pixels, y: Pixels): GridCell | undefined => { - if (x < elementSizes.grid.left || y < elementSizes.grid.top) return undefined; - if (x > elementSizes.grid.width + elementSizes.grid.left || y > elementSizes.grid.top + elementSizes.grid.height) + if (x < chartDimensions.left || y < chartDimensions.top) return undefined; + if (x > chartDimensions.width + chartDimensions.left || y > chartDimensions.top + chartDimensions.height) return undefined; - const xValue = xInvertedScale(x - elementSizes.grid.left); + const xValue = xInvertedScale(x - chartDimensions.left); const yValue = yInvertedScale(y); if (xValue === undefined || yValue === undefined) return undefined; @@ -205,9 +211,9 @@ export function shapeViewModel( const pickQuads = (x: Pixels, y: Pixels): Array | TextBox => { if ( x > 0 && - x < elementSizes.grid.left && - y > elementSizes.grid.top && - y < elementSizes.grid.top + elementSizes.grid.height + x < chartDimensions.left && + y > chartDimensions.top && + y < chartDimensions.top + chartDimensions.height ) { // look up for a Y axis elements const yLabelKey = yInvertedScale(y); @@ -217,13 +223,13 @@ export function shapeViewModel( } } - if (x < elementSizes.grid.left || y < elementSizes.grid.top) { + if (x < chartDimensions.left || y < chartDimensions.top) { return []; } - if (x > elementSizes.grid.width + elementSizes.grid.left || y > elementSizes.grid.top + elementSizes.grid.height) { + if (x > chartDimensions.width + chartDimensions.left || y > chartDimensions.top + chartDimensions.height) { return []; } - const xValue = xInvertedScale(x - elementSizes.grid.left); + const xValue = xInvertedScale(x - chartDimensions.left); const yValue = yInvertedScale(y); if (xValue === undefined || yValue === undefined) { return []; @@ -242,14 +248,14 @@ export function shapeViewModel( const pickDragArea: PickDragFunction = (bound) => { const [start, end] = bound; - const { left, top, width } = elementSizes.grid; + const { left, top, width } = chartDimensions; const topLeft = [Math.min(start.x, end.x) - left, Math.min(start.y, end.y) - top]; const bottomRight = [Math.max(start.x, end.x) - left, Math.max(start.y, end.y) - top]; const startX = xInvertedScale(clamp(topLeft[0], 0, width)); const endX = xInvertedScale(clamp(bottomRight[0], 0, width)); - const startY = yInvertedScale(clamp(topLeft[1], 0, currentGridHeight - 1)); - const endY = yInvertedScale(clamp(bottomRight[1], 0, currentGridHeight - 1)); + const startY = yInvertedScale(clamp(topLeft[1], 0, panelSize.height - 1)); + const endY = yInvertedScale(clamp(bottomRight[1], 0, panelSize.height - 1)); const allXValuesInRange: Array> = getValuesInRange(xValues, startX, endX); const allYValuesInRange: Array> = getValuesInRange(yValues, startY, endY); @@ -300,7 +306,7 @@ export function shapeViewModel( return null; } - const xStart = elementSizes.grid.left + startFromScale; + const xStart = chartDimensions.left + startFromScale; // extend the range in case the right boundary has been selected const width = endFromScale - startFromScale + (isRightOutOfRange || isLeftOutOfRange ? cellWidth : 0); @@ -346,9 +352,9 @@ export function shapeViewModel( ? undefined : { width: cellWidth, - x: elementSizes.grid.left + (xScale(xValues[index]) ?? NaN), - y: elementSizes.grid.top, - height: elementSizes.grid.height, + x: chartDimensions.left + (xScale(xValues[index]) ?? NaN), + y: chartDimensions.top, + height: chartDimensions.height, }; }; @@ -356,19 +362,19 @@ export function shapeViewModel( const xLines = Array.from({ length: xValues.length + 1 }, (d, i) => { const xAxisExtension = i % elementSizes.xAxisTickCadence === 0 ? 5 : 0; return { - x1: elementSizes.grid.left + i * cellWidth, - x2: elementSizes.grid.left + i * cellWidth, - y1: elementSizes.grid.top, - y2: currentGridHeight + xAxisExtension, + x1: i * cellWidth, + x2: i * cellWidth, + y1: 0, + y2: panelSize.height + xAxisExtension, }; }); // horizontal lines const yLines = Array.from({ length: elementSizes.visibleNumberOfRows + 1 }, (d, i) => ({ - x1: elementSizes.grid.left, - x2: elementSizes.grid.left + elementSizes.grid.width, - y1: elementSizes.grid.top + i * cellHeight, - y2: elementSizes.grid.top + i * cellHeight, + x1: 0, + x2: panelSize.width, + y1: i * cellHeight, + y2: i * cellHeight, })); const cells = Object.values(cellMap); @@ -386,50 +392,59 @@ export function shapeViewModel( return { theme: heatmapTheme, - heatmapViewModel: { - gridOrigin: { - x: elementSizes.grid.left, - y: elementSizes.grid.top, - }, - gridLines: { - x: xLines, - y: yLines, - stroke: { - color: colorToRgba(heatmapTheme.grid.stroke.color), - width: gridStrokeWidth, + heatmapViewModels: getPerPanelMap(smScales, (anchor, h, v) => { + const primaryColumn = smScales.vertical.domain[0] === v; + const primaryRow = smScales.horizontal.domain[0] === h; + + return { + anchor, + panelSize, + gridOrigin: { + x: anchor.x + chartDimensions.left, + y: anchor.y + chartDimensions.top, }, - }, - pageSize: elementSizes.visibleNumberOfRows, - cells, - cellFontSize: (cell: Cell) => (heatmapTheme.cell.label.useGlobalMinFontSize ? tableMinFontSize : cell.fontSize), - xValues: textXValues, - yValues: textYValues, - titles: [ - { - origin: { - x: elementSizes.grid.left + elementSizes.grid.width / 2, - y: - elementSizes.grid.top + - elementSizes.grid.height + - elementSizes.xAxis.height + - innerPad(axisTitle.padding) + - axisTitle.fontSize / 2, + gridLines: { + x: xLines, + y: yLines, + stroke: { + color: colorToRgba(heatmapTheme.grid.stroke.color), + width: gridStrokeWidth, }, - ...axisTitleFont, - text: spec.xAxisTitle, - rotation: 0, }, - { - origin: { - x: elementSizes.yAxis.left - innerPad(axisTitle.padding) - axisTitle.fontSize / 2, - y: elementSizes.grid.top + elementSizes.grid.height / 2, + pageSize: elementSizes.visibleNumberOfRows, + cells, + cellFontSize: (cell: Cell) => (heatmapTheme.cell.label.useGlobalMinFontSize ? tableMinFontSize : cell.fontSize), + xValues: textXValues, + yValues: textYValues, + primaryColumn, + primaryRow, + titles: [ + primaryColumn && { + origin: { + x: panelSize.width / 2, + y: + chartDimensions.top + + chartDimensions.height + + elementSizes.xAxis.height + + innerPad(axisTitle.padding) + + axisTitle.fontSize / 2, + }, + ...axisTitleFont, + text: spec.xAxisTitle, + rotation: 0, }, - ...axisTitleFont, - text: spec.yAxisTitle, - rotation: -90, - }, - ], - }, + primaryRow && { + origin: { + x: -elementSizes.yAxis.left - innerPad(axisTitle.padding) - axisTitle.fontSize / 2, + y: chartDimensions.top + panelSize.height / 2, + }, + ...axisTitleFont, + text: spec.yAxisTitle, + rotation: -90, + }, + ].filter(Boolean), + }; + }), pickGridCell, pickQuads, pickDragArea, diff --git a/packages/charts/src/chart_types/heatmap/renderer/canvas/canvas_renderers.ts b/packages/charts/src/chart_types/heatmap/renderer/canvas/canvas_renderers.ts index 68602e6b14..33ff0b7abc 100644 --- a/packages/charts/src/chart_types/heatmap/renderer/canvas/canvas_renderers.ts +++ b/packages/charts/src/chart_types/heatmap/renderer/canvas/canvas_renderers.ts @@ -6,29 +6,30 @@ * Side Public License, v 1. */ -import { Color } from '../../../../common/colors'; import { clearCanvas, renderLayers, withContext } from '../../../../renderers/canvas'; import { radToDeg } from '../../../../utils/common'; import { horizontalPad } from '../../../../utils/dimensions'; -import { SharedGeometryStateStyle } from '../../../../utils/themes/theme'; import { renderMultiLine } from '../../../xy_chart/renderer/canvas/primitives/line'; import { renderRect } from '../../../xy_chart/renderer/canvas/primitives/rect'; import { renderText, TextFont, wrapLines } from '../../../xy_chart/renderer/canvas/primitives/text'; -import { ShapeViewModel } from '../../layout/types/viewmodel_types'; -import { ChartElementSizes } from '../../state/selectors/compute_chart_dimensions'; +import { ReactiveChartStateProps } from './connected_component'; import { getColorBandStyle, getGeometryStateStyle } from './utils'; /** @internal */ -export function renderCanvas2d( - ctx: CanvasRenderingContext2D, - dpr: number, - { theme, heatmapViewModel }: ShapeViewModel, - sharedGeometryStyle: SharedGeometryStateStyle, - background: Color, - elementSizes: ChartElementSizes, - debug: boolean, - highlightedLegendBands: Array<[start: number, end: number]>, -) { +export function renderHeatmapCanvas2d(ctx: CanvasRenderingContext2D, dpr: number, props: ReactiveChartStateProps) { + const { theme } = props.geometries; + const { heatmapViewModels } = props.geometries; + const { + theme: { sharedStyle: sharedGeometryStyle }, + background, + elementSizes, + highlightedLegendBands, + } = props; + if (heatmapViewModels.length === 0) return; + + // temporary for PR wip + const [heatmapViewModel] = heatmapViewModels; + withContext(ctx, () => { // set some defaults for the overall rendering @@ -55,145 +56,146 @@ export function renderCanvas2d( renderLayers(ctx, [ () => clearCanvas(ctx, background), - () => - debug && - withContext(ctx, () => { - ctx.strokeStyle = 'black'; - ctx.strokeRect( - elementSizes.grid.left, - elementSizes.grid.top, - elementSizes.grid.width, - elementSizes.grid.height, - ); - - ctx.strokeStyle = 'red'; - ctx.strokeRect( - elementSizes.xAxis.left, - elementSizes.xAxis.top, - elementSizes.xAxis.width, - elementSizes.xAxis.height, - ); - - ctx.strokeStyle = 'violet'; - ctx.strokeRect( - elementSizes.yAxis.left, - elementSizes.yAxis.top, - elementSizes.yAxis.width, - elementSizes.yAxis.height, - ); - }), + () => { // Grid - withContext(ctx, () => { - renderMultiLine(ctx, heatmapViewModel.gridLines.x, heatmapViewModel.gridLines.stroke); - renderMultiLine(ctx, heatmapViewModel.gridLines.y, heatmapViewModel.gridLines.stroke); + heatmapViewModels.forEach(({ gridOrigin: { x, y }, gridLines }) => { + withContext(ctx, () => { + ctx.translate(x, y); + renderMultiLine(ctx, gridLines.x, gridLines.stroke); + renderMultiLine(ctx, gridLines.y, gridLines.stroke); + }); }); }, () => // Cells - withContext(ctx, () => { - const { x, y } = heatmapViewModel.gridOrigin; - ctx.translate(x, y); - filteredCells.forEach((cell) => { - if (cell.visible) { - const geometryStateStyle = getGeometryStateStyle(cell, sharedGeometryStyle, highlightedLegendBands); - const style = getColorBandStyle(cell, geometryStateStyle); - renderRect(ctx, cell, style.fill, style.stroke); - } + heatmapViewModels.forEach(({ gridOrigin: { x, y } }) => { + withContext(ctx, () => { + ctx.translate(x, y); + filteredCells.forEach((cell) => { + if (cell.visible) { + const geometryStateStyle = getGeometryStateStyle(cell, sharedGeometryStyle, highlightedLegendBands); + const style = getColorBandStyle(cell, geometryStateStyle); + renderRect(ctx, cell, style.fill, style.stroke); + } + }); }); }), - () => - theme.cell.label.visible && - withContext(ctx, () => { - // Text on cells - const { x, y } = heatmapViewModel.gridOrigin; - ctx.translate(x, y); - filteredCells.forEach((cell) => { - const fontSize = heatmapViewModel.cellFontSize(cell); - if (cell.visible && Number.isFinite(fontSize)) - renderText(ctx, { x: cell.x + cell.width / 2, y: cell.y + cell.height / 2 }, cell.formatted, { - ...theme.cell.label, - fontSize, - align: 'center', - baseline: 'middle', - textColor: cell.textColor, - }); - }); - }), + // Text on cells + () => { + if (!theme.cell.label.visible) return; - () => - // render text on Y axis - theme.yAxisLabel.visible && - withContext(ctx, () => { - // the text is right aligned so the canvas needs to be aligned to the right of the Y axis box - ctx.translate(elementSizes.yAxis.left + elementSizes.yAxis.width, elementSizes.yAxis.top); - const font: TextFont = { ...theme.yAxisLabel, baseline: 'middle' /* fixed */, align: 'right' /* fixed */ }; - const { padding } = theme.yAxisLabel; - const horizontalPadding = horizontalPad(padding); - filteredYValues.forEach(({ x, y, text }) => { - const textLines = wrapLines( - ctx, - text, - font, - theme.yAxisLabel.fontSize, - Math.max(elementSizes.yAxis.width - horizontalPadding, 0), - theme.yAxisLabel.fontSize, - { shouldAddEllipsis: true, wrapAtWord: false }, - ).lines; - // TODO improve the `wrapLines` code to handle results with short width - renderText(ctx, { x, y }, textLines.length > 0 ? textLines[0] : '…', font); + heatmapViewModels.forEach(({ cellFontSize, gridOrigin: { x, y } }) => { + withContext(ctx, () => { + ctx.translate(x, y); + filteredCells.forEach((cell) => { + const fontSize = cellFontSize(cell); + if (cell.visible && Number.isFinite(fontSize)) + renderText(ctx, { x: cell.x + cell.width / 2, y: cell.y + cell.height / 2 }, cell.formatted, { + ...theme.cell.label, + fontSize, + align: 'center', + baseline: 'middle', + textColor: cell.textColor, + }); + }); }); - }), + }); + }, - () => - // render text on X axis - theme.xAxisLabel.visible && - withContext(ctx, () => { - ctx.translate(elementSizes.xAxis.left, elementSizes.xAxis.top); - heatmapViewModel.xValues - .filter((_, i) => i % elementSizes.xAxisTickCadence === 0) - .forEach(({ x, y, text, align }) => { + // render text on Y axis + () => { + if (!theme.yAxisLabel.visible) return; + + heatmapViewModels.forEach(({ primaryRow, gridOrigin: { x, y } }) => { + if (!primaryRow) return; + + withContext(ctx, () => { + // the text is right aligned so the canvas needs to be aligned to the right of the Y axis box + ctx.translate(x, y); + const font: TextFont = { + ...theme.yAxisLabel, + baseline: 'middle' /* fixed */, + align: 'right' /* fixed */, + }; + const { padding } = theme.yAxisLabel; + const horizontalPadding = horizontalPad(padding); + filteredYValues.forEach(({ x, y, text }) => { const textLines = wrapLines( ctx, text, - theme.xAxisLabel, - theme.xAxisLabel.fontSize, - // TODO wrap into multilines - Infinity, - 16, + font, + theme.yAxisLabel.fontSize, + Math.max(elementSizes.yAxis.width - horizontalPadding, 0), + theme.yAxisLabel.fontSize, { shouldAddEllipsis: true, wrapAtWord: false }, ).lines; - renderText( - ctx, - { x, y }, - textLines.length > 0 ? textLines[0] : '…', - { ...theme.xAxisLabel, baseline: 'middle', align }, - // negative rotation due to the canvas rotation direction - radToDeg(-elementSizes.xLabelRotation), - ); + // TODO improve the `wrapLines` code to handle results with short width + renderText(ctx, { x, y }, textLines.length > 0 ? textLines[0] : '…', font); }); - }), + }); + }); + }, + + // render text on X axis + () => { + if (!theme.xAxisLabel.visible) return; + + heatmapViewModels.forEach(({ xValues, primaryColumn, gridOrigin: { x, y } }) => { + withContext(ctx, () => { + if (!primaryColumn) return; + ctx.translate(x, y + elementSizes.xAxis.top); + xValues + .filter((_, i) => i % elementSizes.xAxisTickCadence === 0) + .forEach(({ x, y, text, align }) => { + const textLines = wrapLines( + ctx, + text, + theme.xAxisLabel, + theme.xAxisLabel.fontSize, + // TODO wrap into multilines + Infinity, + 16, + { shouldAddEllipsis: true, wrapAtWord: false }, + ).lines; + renderText( + ctx, + { x, y }, + textLines.length > 0 ? textLines[0] : '…', + { ...theme.xAxisLabel, baseline: 'middle', align }, + // negative rotation due to the canvas rotation direction + radToDeg(-elementSizes.xLabelRotation), + ); + }); + }); + }); + }, () => - withContext(ctx, () => { - heatmapViewModel.titles - .filter((t) => t.visible && t.text !== '') - .forEach((title) => { - renderText( - ctx, - title.origin, - title.text, - { - ...title, - baseline: 'middle', - align: 'center', - }, - title.rotation, - ); + heatmapViewModels + .filter(({ titles }) => titles.length > 0) + .forEach(({ titles, gridOrigin: { x, y } }) => { + withContext(ctx, () => { + ctx.translate(x, y); + titles + .filter((t) => t.visible && t.text !== '') + .forEach((title) => { + renderText( + ctx, + title.origin, + title.text, + { + ...title, + baseline: 'middle', + align: 'center', + }, + title.rotation, + ); + }); }); - }), + }), ]); }); } diff --git a/packages/charts/src/chart_types/heatmap/renderer/canvas/connected_component.tsx b/packages/charts/src/chart_types/heatmap/renderer/canvas/connected_component.tsx index 6fd1f7639b..ed304d6e4e 100644 --- a/packages/charts/src/chart_types/heatmap/renderer/canvas/connected_component.tsx +++ b/packages/charts/src/chart_types/heatmap/renderer/canvas/connected_component.tsx @@ -23,16 +23,18 @@ import { getChartThemeSelector } from '../../../../state/selectors/get_chart_the import { getInternalIsInitializedSelector, InitStatus } from '../../../../state/selectors/get_internal_is_intialized'; import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_spec'; import { Dimensions } from '../../../../utils/dimensions'; +import { deepEqual } from '../../../../utils/fast_deep_equal'; import { LIGHT_THEME } from '../../../../utils/themes/light_theme'; import { Theme } from '../../../../utils/themes/theme'; import { nullShapeViewModel, ShapeViewModel } from '../../layout/types/viewmodel_types'; -import { ChartElementSizes, computeChartElementSizesSelector } from '../../state/selectors/compute_chart_dimensions'; -import { getHeatmapGeometries } from '../../state/selectors/geometries'; +import { ChartElementSizes, computeChartElementSizesSelector } from '../../state/selectors/compute_chart_element_sizes'; import { getHeatmapContainerSizeSelector } from '../../state/selectors/get_heatmap_container_size'; import { getHighlightedLegendBandsSelector } from '../../state/selectors/get_highlighted_legend_bands'; -import { renderCanvas2d } from './canvas_renderers'; +import { getPerPanelHeatmapGeometries } from '../../state/selectors/get_per_panel_heatmap_geometries'; +import { renderHeatmapCanvas2d } from './canvas_renderers'; -interface ReactiveChartStateProps { +/** @internal */ +export interface ReactiveChartStateProps { initialized: boolean; geometries: ShapeViewModel; chartContainerDimensions: Dimensions; @@ -80,6 +82,10 @@ class Component extends React.Component { } } + shouldComponentUpdate(nextProps: ReactiveChartStateProps) { + return !deepEqual(this.props, nextProps); + } + componentDidUpdate() { if (!this.ctx) { this.tryCanvasContext(); @@ -97,19 +103,7 @@ class Component extends React.Component { private drawCanvas() { if (this.ctx) { - renderCanvas2d( - this.ctx, - this.devicePixelRatio, - { - ...this.props.geometries, - theme: this.props.geometries.theme, - }, - this.props.theme.sharedStyle, - this.props.background, - this.props.elementSizes, - this.props.debug, - this.props.highlightedLegendBands, - ); + renderHeatmapCanvas2d(this.ctx, this.devicePixelRatio, this.props); } } @@ -167,7 +161,6 @@ const DEFAULT_PROPS: ReactiveChartStateProps = { a11ySettings: DEFAULT_A11Y_SETTINGS, background: Colors.Transparent.keyword, elementSizes: { - grid: { width: 0, height: 0, left: 0, top: 0 }, xAxis: { width: 0, height: 0, left: 0, top: 0 }, yAxis: { width: 0, height: 0, left: 0, top: 0 }, fullHeatmapHeight: 0, @@ -185,7 +178,7 @@ const mapStateToProps = (state: GlobalChartState): ReactiveChartStateProps => { } return { initialized: true, - geometries: getHeatmapGeometries(state), + geometries: getPerPanelHeatmapGeometries(state), chartContainerDimensions: getHeatmapContainerSizeSelector(state), highlightedLegendBands: getHighlightedLegendBandsSelector(state), theme: getChartThemeSelector(state), diff --git a/packages/charts/src/chart_types/heatmap/renderer/dom/highlighter_brush.tsx b/packages/charts/src/chart_types/heatmap/renderer/dom/highlighter_brush.tsx index 9347e93c2f..0f1c44321a 100644 --- a/packages/charts/src/chart_types/heatmap/renderer/dom/highlighter_brush.tsx +++ b/packages/charts/src/chart_types/heatmap/renderer/dom/highlighter_brush.tsx @@ -11,10 +11,10 @@ import { connect } from 'react-redux'; import { GlobalChartState } from '../../../../state/chart_state'; import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; import { getInternalIsInitializedSelector, InitStatus } from '../../../../state/selectors/get_internal_is_intialized'; -import { computeChartElementSizesSelector } from '../../state/selectors/compute_chart_dimensions'; -import { getHeatmapGeometries } from '../../state/selectors/geometries'; +import { computeChartDimensionsSelector } from '../../state/selectors/compute_chart_dimensions'; import { getBrushedHighlightedShapesSelector } from '../../state/selectors/get_brushed_highlighted_shapes'; import { getHighlightedAreaSelector } from '../../state/selectors/get_highlighted_area'; +import { getPerPanelHeatmapGeometries } from '../../state/selectors/get_per_panel_heatmap_geometries'; import { DEFAULT_PROPS, HighlighterCellsComponent, HighlighterCellsProps } from './highlighter'; const brushMapStateToProps = (state: GlobalChartState): HighlighterCellsProps => { @@ -22,16 +22,16 @@ const brushMapStateToProps = (state: GlobalChartState): HighlighterCellsProps => return DEFAULT_PROPS; } - const { chartId } = state; - - const geoms = getHeatmapGeometries(state); - const canvasDimension = computeChartElementSizesSelector(state).grid; - let dragShape = getBrushedHighlightedShapesSelector(state); const highlightedArea = getHighlightedAreaSelector(state); + if (highlightedArea) { dragShape = highlightedArea; } + + const { chartId } = state; + const geoms = getPerPanelHeatmapGeometries(state); + const canvasDimension = computeChartDimensionsSelector(state).chartDimensions; const { brushMask, brushArea } = getChartThemeSelector(state).heatmap; return { diff --git a/packages/charts/src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx b/packages/charts/src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx index f9d0b47abd..05704a1705 100644 --- a/packages/charts/src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx +++ b/packages/charts/src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx @@ -15,10 +15,6 @@ import { ScreenReaderSummary } from '../../../../components/accessibility'; import { onChartRendered } from '../../../../state/actions/chart'; import { GlobalChartState } from '../../../../state/chart_state'; import { computePanelsSelectors, PanelGeoms } from '../../../../state/selectors/compute_panels'; -import { - computePerPanelAxesGeomsSelector, - PerPanelAxisGeoms, -} from '../../../../state/selectors/compute_per_panel_axes_geoms'; import { A11ySettings, DEFAULT_A11Y_SETTINGS, @@ -39,6 +35,10 @@ import { AnnotationDimensions } from '../../annotations/types'; import { computeAnnotationDimensionsSelector } from '../../state/selectors/compute_annotations'; import { computeChartDimensionsSelector } from '../../state/selectors/compute_chart_dimensions'; import { computeChartTransformSelector } from '../../state/selectors/compute_chart_transform'; +import { + computePerPanelAxesGeomsSelector, + PerPanelAxisGeoms, +} from '../../state/selectors/compute_per_panel_axes_geoms'; import { computeSeriesGeometriesSelector } from '../../state/selectors/compute_series_geometries'; import { getAxesStylesSelector } from '../../state/selectors/get_axis_styles'; import { getGridLinesSelector } from '../../state/selectors/get_grid_lines';