diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-heatmap-alpha-basic-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-heatmap-alpha-basic-visually-looks-correct-1-snap.png index 5529bde4dc..de60155e5b 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-heatmap-alpha-basic-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-heatmap-alpha-basic-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-heatmap-alpha-categorical-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-heatmap-alpha-categorical-visually-looks-correct-1-snap.png index 5500726550..38ad4f6a18 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-heatmap-alpha-categorical-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-heatmap-alpha-categorical-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-heatmap-alpha-theming-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-heatmap-alpha-theming-visually-looks-correct-1-snap.png index 4591d57f62..b1e6605905 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-heatmap-alpha-theming-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-heatmap-alpha-theming-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-heatmap-alpha-time-snap-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-heatmap-alpha-time-snap-visually-looks-correct-1-snap.png index 037b37feee..94a0d28d5a 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-heatmap-alpha-time-snap-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-heatmap-alpha-time-snap-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-heatmap-alpha-time-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-heatmap-alpha-time-visually-looks-correct-1-snap.png index c54cbb35cd..dcd6aa5b09 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-heatmap-alpha-time-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-heatmap-alpha-time-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-should-maximize-the-label-font-size-1-snap.png b/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-should-maximize-the-label-font-size-1-snap.png index 8c4ef8b3c7..36e7156e57 100644 Binary files a/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-should-maximize-the-label-font-size-1-snap.png and b/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-should-maximize-the-label-font-size-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-should-maximize-the-label-with-an-unique-font-size-1-snap.png b/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-should-maximize-the-label-with-an-unique-font-size-1-snap.png index 66f6906381..1490fdbeaa 100644 Binary files a/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-should-maximize-the-label-with-an-unique-font-size-1-snap.png and b/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-should-maximize-the-label-with-an-unique-font-size-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-should-not-have-brush-tool-extend-into-axes-1-snap.png b/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-should-not-have-brush-tool-extend-into-axes-1-snap.png index 926f57f8f4..242e2e4b5d 100644 Binary files a/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-should-not-have-brush-tool-extend-into-axes-1-snap.png and b/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-should-not-have-brush-tool-extend-into-axes-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-should-show-x-and-y-axis-titles-1-snap.png b/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-should-show-x-and-y-axis-titles-1-snap.png new file mode 100644 index 0000000000..db540d7d1e Binary files /dev/null and b/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-should-show-x-and-y-axis-titles-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-theme-dark-should-render-basic-heatmap-1-snap.png b/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-theme-dark-should-render-basic-heatmap-1-snap.png index 1f1d4de6cf..dae03889e5 100644 Binary files a/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-theme-dark-should-render-basic-heatmap-1-snap.png and b/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-theme-dark-should-render-basic-heatmap-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-theme-dark-should-render-correct-brush-area-1-snap.png b/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-theme-dark-should-render-correct-brush-area-1-snap.png index f0f683ea7f..7b622f1f92 100644 Binary files a/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-theme-dark-should-render-correct-brush-area-1-snap.png and b/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-theme-dark-should-render-correct-brush-area-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-theme-eui-dark-should-render-basic-heatmap-1-snap.png b/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-theme-eui-dark-should-render-basic-heatmap-1-snap.png index 515a27f52b..d862aeba34 100644 Binary files a/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-theme-eui-dark-should-render-basic-heatmap-1-snap.png and b/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-theme-eui-dark-should-render-basic-heatmap-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-theme-eui-dark-should-render-correct-brush-area-1-snap.png b/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-theme-eui-dark-should-render-correct-brush-area-1-snap.png index d5c5c8d86b..98266300d6 100644 Binary files a/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-theme-eui-dark-should-render-correct-brush-area-1-snap.png and b/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-theme-eui-dark-should-render-correct-brush-area-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-theme-eui-light-should-render-basic-heatmap-1-snap.png b/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-theme-eui-light-should-render-basic-heatmap-1-snap.png index db266ec5c7..55c4c15988 100644 Binary files a/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-theme-eui-light-should-render-basic-heatmap-1-snap.png and b/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-theme-eui-light-should-render-basic-heatmap-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-theme-eui-light-should-render-correct-brush-area-1-snap.png b/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-theme-eui-light-should-render-correct-brush-area-1-snap.png index 3d828725f7..8bc2ce8b6a 100644 Binary files a/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-theme-eui-light-should-render-correct-brush-area-1-snap.png and b/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-theme-eui-light-should-render-correct-brush-area-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-theme-light-should-render-basic-heatmap-1-snap.png b/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-theme-light-should-render-basic-heatmap-1-snap.png index 5529bde4dc..de60155e5b 100644 Binary files a/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-theme-light-should-render-basic-heatmap-1-snap.png and b/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-theme-light-should-render-basic-heatmap-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-theme-light-should-render-correct-brush-area-1-snap.png b/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-theme-light-should-render-correct-brush-area-1-snap.png index 55ce2ab79a..80d6534188 100644 Binary files a/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-theme-light-should-render-correct-brush-area-1-snap.png and b/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-theme-light-should-render-correct-brush-area-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-time-snap-with-dataset-2-1-snap.png b/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-time-snap-with-dataset-2-1-snap.png index 40d6391c0d..a25c920114 100644 Binary files a/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-time-snap-with-dataset-2-1-snap.png and b/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-time-snap-with-dataset-2-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-time-snap-with-dataset-3-1-snap.png b/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-time-snap-with-dataset-3-1-snap.png index 6a4d6535c2..9b33ca2cb2 100644 Binary files a/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-time-snap-with-dataset-3-1-snap.png and b/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-time-snap-with-dataset-3-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-time-snap-with-dataset-4-1-snap.png b/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-time-snap-with-dataset-4-1-snap.png index ff86455f23..af61e8f980 100644 Binary files a/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-time-snap-with-dataset-4-1-snap.png and b/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-time-snap-with-dataset-4-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-time-snap-with-dataset-5-1-snap.png b/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-time-snap-with-dataset-5-1-snap.png index 219f538773..e30790f604 100644 Binary files a/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-time-snap-with-dataset-5-1-snap.png and b/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-time-snap-with-dataset-5-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-time-snap-with-dataset-6-1-snap.png b/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-time-snap-with-dataset-6-1-snap.png index 29a81d3915..76415972c4 100644 Binary files a/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-time-snap-with-dataset-6-1-snap.png and b/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-time-snap-with-dataset-6-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-time-snap-with-dataset-7-1-snap.png b/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-time-snap-with-dataset-7-1-snap.png index ec87a2f11a..d83fac4b72 100644 Binary files a/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-time-snap-with-dataset-7-1-snap.png and b/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-time-snap-with-dataset-7-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-time-snap-with-dataset-8-1-snap.png b/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-time-snap-with-dataset-8-1-snap.png index cf9fa806b9..81ade0e7d8 100644 Binary files a/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-time-snap-with-dataset-8-1-snap.png and b/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-time-snap-with-dataset-8-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-time-snap-with-dataset-9-1-snap.png b/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-time-snap-with-dataset-9-1-snap.png index 97beb19e29..2c06168ef6 100644 Binary files a/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-time-snap-with-dataset-9-1-snap.png and b/integration/tests/__image_snapshots__/heatmap-stories-test-ts-heatmap-stories-time-snap-with-dataset-9-1-snap.png differ diff --git a/integration/tests/heatmap_stories.test.ts b/integration/tests/heatmap_stories.test.ts index 87404fbec3..b342a1f4b7 100644 --- a/integration/tests/heatmap_stories.test.ts +++ b/integration/tests/heatmap_stories.test.ts @@ -52,4 +52,10 @@ describe('Heatmap stories', () => { `http://localhost:9001/?path=/story/heatmap-alpha--time-snap&globals=theme:light&knob-dataset=${dataset}`, ); }); + + it('should show x and y axis titles', async () => { + await common.expectChartAtUrlToMatchScreenshot( + 'http://localhost:9001/?path=/story/heatmap-alpha--basic&knob-Show%20x%20axis%20title=true&knob-Show%20y%20axis%20title=true', + ); + }); }); diff --git a/packages/charts/api/charts.api.md b/packages/charts/api/charts.api.md index 6ce2bb0a78..0bdad597ad 100644 --- a/packages/charts/api/charts.api.md +++ b/packages/charts/api/charts.api.md @@ -1120,6 +1120,8 @@ export interface HeatmapSpec extends Spec { // (undocumented) xAxisLabelName: string; // (undocumented) + xAxisTitle: string; + // (undocumented) xScale: RasterTimeScale | OrdinalScale | LinearScale; // (undocumented) xSortPredicate: Predicate; @@ -1130,6 +1132,8 @@ export interface HeatmapSpec extends Spec { // (undocumented) yAxisLabelName: string; // (undocumented) + yAxisTitle: string; + // (undocumented) ySortPredicate: Predicate; } @@ -1196,7 +1200,7 @@ export interface HeatmapStyle { align: TextAlign; baseline: TextBaseline; visible: boolean; - padding: number; + padding: Pixels | Padding; }; // (undocumented) yAxisLabel: Font & { @@ -1206,12 +1210,7 @@ export interface HeatmapStyle { }; baseline: TextBaseline; visible: boolean; - padding: number | { - left?: number; - right?: number; - top?: number; - bottom?: number; - }; + padding: Pixels | Padding; }; } @@ -2132,9 +2131,9 @@ export type ShowAccessor = (value: PrimitiveValue) => boolean; // @public export interface SimplePadding { // (undocumented) - inner: number; + inner: Pixels; // (undocumented) - outer: number; + outer: Pixels; } // @alpha (undocumented) 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 3e1db5290e..dcf991e3bb 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,22 +9,15 @@ import { ChartType } from '../../..'; import { Color, Colors } from '../../../../common/colors'; import { Pixels } from '../../../../common/geometry'; -import { Box } from '../../../../common/text_utils'; +import { Box, Font } from '../../../../common/text_utils'; import { Fill, Line, Rect, Stroke } from '../../../../geoms/types'; import { HeatmapBrushEvent } from '../../../../specs/settings'; import { Point } from '../../../../utils/point'; import { LIGHT_THEME } from '../../../../utils/themes/light_theme'; -import { HeatmapStyle } from '../../../../utils/themes/theme'; +import { Visible, HeatmapStyle } from '../../../../utils/themes/theme'; import { PrimitiveValue } from '../../../partition_chart/layout/utils/group_by_rollup'; import { HeatmapCellDatum } from '../viewmodel/viewmodel'; -/** @internal */ -export interface Value { - order: number; - value: string | number; - formatted: string; -} - /** @public */ export interface Cell { x: number; @@ -65,6 +58,15 @@ export interface HeatmapViewModel { xValues: Array; yValues: Array; pageSize: number; + titles: Array< + Font & + Visible & { + fontSize: number; + text: string; + origin: Point; + rotation: 0 | -90; + } + >; } /** @internal */ @@ -122,6 +124,7 @@ export const nullHeatmapViewModel: HeatmapViewModel = { yValues: [], pageSize: 0, cellFontSize: () => 0, + titles: [], }; /** @internal */ diff --git a/packages/charts/src/chart_types/heatmap/state/selectors/scenegraph.ts b/packages/charts/src/chart_types/heatmap/layout/viewmodel/scenegraph.ts similarity index 68% rename from packages/charts/src/chart_types/heatmap/state/selectors/scenegraph.ts rename to packages/charts/src/chart_types/heatmap/layout/viewmodel/scenegraph.ts index 3a2e3cf682..d60c7c7876 100644 --- a/packages/charts/src/chart_types/heatmap/state/selectors/scenegraph.ts +++ b/packages/charts/src/chart_types/heatmap/layout/viewmodel/scenegraph.ts @@ -7,23 +7,20 @@ */ import { measureText } from '../../../../common/text_utils'; -import { Dimensions } from '../../../../utils/dimensions'; import { Theme } from '../../../../utils/themes/theme'; import { ShapeViewModel, nullShapeViewModel } from '../../layout/types/viewmodel_types'; import { shapeViewModel } from '../../layout/viewmodel/viewmodel'; import { HeatmapSpec } from '../../specs'; -import { HeatmapTable } from './compute_chart_dimensions'; -import { ColorScale } from './get_color_scale'; -import { GridHeightParams } from './get_grid_full_height'; +import { ChartElementSizes, HeatmapTable } from '../../state/selectors/compute_chart_dimensions'; +import { ColorScale } from '../../state/selectors/get_color_scale'; /** @internal */ export function render( spec: HeatmapSpec, - chartDimensions: Dimensions, + elementSizes: ChartElementSizes, heatmapTable: HeatmapTable, colorScale: ColorScale, bandsToHide: Array<[number, number]>, - gridHeightParams: GridHeightParams, theme: Theme, ): ShapeViewModel { const textMeasurer = document.createElement('canvas'); @@ -31,14 +28,5 @@ export function render( if (!textMeasurerCtx) { return nullShapeViewModel(); } - return shapeViewModel( - measureText(textMeasurerCtx), - spec, - theme, - chartDimensions, - heatmapTable, - colorScale, - bandsToHide, - gridHeightParams, - ); + return shapeViewModel(measureText(textMeasurerCtx), spec, theme, elementSizes, heatmapTable, colorScale, 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 77a8a2abad..0c72af9252 100644 --- a/packages/charts/src/chart_types/heatmap/layout/viewmodel/viewmodel.ts +++ b/packages/charts/src/chart_types/heatmap/layout/viewmodel/viewmodel.ts @@ -12,21 +12,20 @@ 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 { Box, maximiseFontSize, TextMeasure } from '../../../../common/text_utils'; +import { Box, Font, maximiseFontSize, TextMeasure } from '../../../../common/text_utils'; import { ScaleContinuous } from '../../../../scales'; import { ScaleType } from '../../../../scales/constants'; import { LinearScale, OrdinalScale, RasterTimeScale } from '../../../../specs'; import { withTextMeasure } from '../../../../utils/bbox/canvas_text_bbox_calculator'; import { addIntervalToTime } from '../../../../utils/chrono/elasticsearch'; import { clamp } from '../../../../utils/common'; -import { Dimensions } from '../../../../utils/dimensions'; +import { Dimensions, horizontalPad, innerPad, pad } from '../../../../utils/dimensions'; import { Logger } from '../../../../utils/logger'; -import { HeatmapStyle, Theme } from '../../../../utils/themes/theme'; +import { HeatmapStyle, Theme, Visible } from '../../../../utils/themes/theme'; import { PrimitiveValue } from '../../../partition_chart/layout/utils/group_by_rollup'; import { HeatmapSpec } from '../../specs'; -import { HeatmapTable } from '../../state/selectors/compute_chart_dimensions'; +import { ChartElementSizes, HeatmapTable } from '../../state/selectors/compute_chart_dimensions'; import { ColorScale } from '../../state/selectors/get_color_scale'; -import { GridHeightParams } from '../../state/selectors/get_grid_full_height'; import { Cell, PickDragFunction, @@ -61,7 +60,7 @@ function estimatedNonOverlappingTickCount( ): number { return withTextMeasure((textMeasure) => { const labelSample = formatter(Date.now()); - const { width } = textMeasure(labelSample, padding, fontSize, fontFamily); + const { width } = textMeasure(labelSample, horizontalPad(padding), fontSize, fontFamily); const maxTicks = chartWidth / width; // Dividing by 2 is a temp fix to make sure {@link ScaleContinuous} won't produce // to many ticks creating nice rounded tick values @@ -73,12 +72,11 @@ function estimatedNonOverlappingTickCount( export function shapeViewModel( textMeasure: TextMeasure, spec: HeatmapSpec, - { heatmap: heatmapTheme, background }: Theme, - chartDimensions: Dimensions, + { heatmap: heatmapTheme, axes: { axisTitle }, background }: Theme, + elementSizes: ChartElementSizes, heatmapTable: HeatmapTable, colorScale: ColorScale, bandsToHide: Array<[number, number]>, - { height, pageSize }: GridHeightParams, ): ShapeViewModel { const gridStrokeWidth = heatmapTheme.grid.stroke.width ?? 1; @@ -93,14 +91,18 @@ export function shapeViewModel( })); // compute the scale for the rows positions - const yScale = scaleBand>().domain(yValues).range([0, height]); + const yScale = scaleBand>().domain(yValues).range([0, elementSizes.fullHeatmapHeight]); - const yInvertedScale = scaleQuantize>().domain([0, height]).range(yValues); + const yInvertedScale = scaleQuantize>() + .domain([0, elementSizes.fullHeatmapHeight]) + .range(yValues); // compute the scale for the columns positions - const xScale = scaleBand>().domain(xValues).range([0, chartDimensions.width]); + const xScale = scaleBand>().domain(xValues).range([0, elementSizes.grid.width]); - const xInvertedScale = scaleQuantize>().domain([0, chartDimensions.width]).range(xValues); + const xInvertedScale = scaleQuantize>() + .domain([0, elementSizes.grid.width]) + .range(xValues); // compute the cell width (can be smaller then the available size depending on config const cellWidth = @@ -111,20 +113,19 @@ export function shapeViewModel( // compute the cell height (we already computed the max size for that) const cellHeight = yScale.bandwidth(); - const currentGridHeight = cellHeight * pageSize; + const currentGridHeight = elementSizes.grid.height; // compute the position of each column label - const textXValues = getXTicks(spec, heatmapTheme, chartDimensions, xScale, heatmapTable, currentGridHeight); + const textXValues = getXTicks(spec, heatmapTheme, elementSizes.grid, xScale, heatmapTable); const { padding } = heatmapTheme.yAxisLabel; - const rightPadding = typeof padding === 'number' ? padding : padding.right ?? 0; // compute the position of each row label const textYValues = boxedYValues.map((d) => { return { ...d, // position of the Y labels - x: chartDimensions.left - rightPadding, + x: -pad(padding, 'right'), y: cellHeight / 2 + (yScale(d.value) || 0), }; }); @@ -196,7 +197,12 @@ export function shapeViewModel( * @param y */ const pickQuads = (x: Pixels, y: Pixels): Array | TextBox => { - if (x > 0 && x < chartDimensions.left && y > chartDimensions.top && y < chartDimensions.height) { + if ( + x > 0 && + x < elementSizes.grid.left && + y > elementSizes.grid.top && + y < elementSizes.grid.top + elementSizes.grid.height + ) { // look up for a Y axis elements const yLabelKey = yInvertedScale(y); const yLabelValue = textYValues.find((v) => v.value === yLabelKey); @@ -205,13 +211,13 @@ export function shapeViewModel( } } - if (x < chartDimensions.left || y < chartDimensions.top) { + if (x < elementSizes.grid.left || y < elementSizes.grid.top) { return []; } - if (x > chartDimensions.width + chartDimensions.left || y > chartDimensions.height) { + if (x > elementSizes.grid.width + elementSizes.grid.left || y > elementSizes.grid.top + elementSizes.grid.height) { return []; } - const xValue = xInvertedScale(x - chartDimensions.left); + const xValue = xInvertedScale(x - elementSizes.grid.left); const yValue = yInvertedScale(y); if (xValue === undefined || yValue === undefined) { return []; @@ -230,7 +236,7 @@ export function shapeViewModel( const pickDragArea: PickDragFunction = (bound) => { const [start, end] = bound; - const { left, top, width } = chartDimensions; + const { left, top, width } = elementSizes.grid; 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]; @@ -288,7 +294,7 @@ export function shapeViewModel( return null; } - const xStart = chartDimensions.left + startFromScale; + const xStart = elementSizes.grid.left + startFromScale; // extend the range in case the right boundary has been selected const width = endFromScale - startFromScale + (isRightOutOfRange || isLeftOutOfRange ? cellWidth : 0); @@ -324,29 +330,39 @@ export function shapeViewModel( // vertical lines const xLines = Array.from({ length: xValues.length + 1 }, (d, i) => ({ - x1: chartDimensions.left + i * cellWidth, - x2: chartDimensions.left + i * cellWidth, - y1: chartDimensions.top, + x1: elementSizes.grid.left + i * cellWidth, + x2: elementSizes.grid.left + i * cellWidth, + y1: elementSizes.grid.top, y2: currentGridHeight, })); // horizontal lines - const yLines = Array.from({ length: pageSize + 1 }, (d, i) => ({ - x1: chartDimensions.left, - x2: chartDimensions.left + chartDimensions.width, + const yLines = Array.from({ length: elementSizes.visibleNumberOfRows + 1 }, (d, i) => ({ + x1: elementSizes.grid.left, + x2: elementSizes.grid.left + elementSizes.grid.width, y1: i * cellHeight, y2: i * cellHeight, })); const cells = Object.values(cellMap); const tableMinFontSize = cells.reduce((acc, { fontSize }) => Math.min(acc, fontSize), Infinity); + // TODO introduce missing styles into axes.axisTitle + const axisTitleFont: Visible & Font & { fontSize: Pixels } = { + visible: axisTitle.visible, + fontFamily: axisTitle.fontFamily, + fontStyle: axisTitle.fontStyle ?? 'normal', + fontVariant: 'normal', + fontWeight: 'bold', + textColor: axisTitle.fill, + fontSize: axisTitle.fontSize, + }; return { theme: heatmapTheme, heatmapViewModel: { gridOrigin: { - x: chartDimensions.left, - y: chartDimensions.top, + x: elementSizes.grid.left, + y: elementSizes.grid.top, }, gridLines: { x: xLines, @@ -356,11 +372,36 @@ export function shapeViewModel( width: gridStrokeWidth, }, }, - pageSize, + 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, + }, + ...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, + }, + ...axisTitleFont, + text: spec.yAxisTitle, + rotation: -90, + }, + ], }, pickQuads, pickDragArea, @@ -385,10 +426,9 @@ export function isRasterTimeScale(scale: RasterTimeScale | OrdinalScale | Linear function getXTicks( spec: HeatmapSpec, style: HeatmapStyle, - chartDimensions: Dimensions, + grid: Dimensions, xScale: ScaleBand, { xValues, xNumericExtent }: HeatmapTable, - gridHeight: number, ): Array { const getTextValue = ( formatter: HeatmapSpec['xAxisLabelFormatter'], @@ -399,8 +439,8 @@ function getXTicks( value, isValue: false, ...style.xAxisLabel, - x: chartDimensions.left + (scaleCallback(value) ?? 0), - y: gridHeight + style.xAxisLabel.fontSize / 2 + style.xAxisLabel.padding, + x: scaleCallback(value) ?? 0, + y: style.xAxisLabel.fontSize / 2 + pad(style.xAxisLabel.padding, 'top'), }; }; if (isRasterTimeScale(spec.xScale)) { @@ -408,15 +448,11 @@ function getXTicks( { type: ScaleType.Time, domain: xNumericExtent, - range: [0, chartDimensions.width], + range: [0, grid.width], nice: false, }, { - desiredTickCount: estimatedNonOverlappingTickCount( - chartDimensions.width, - spec.xAxisLabelFormatter, - style.xAxisLabel, - ), + desiredTickCount: estimatedNonOverlappingTickCount(grid.width, spec.xAxisLabelFormatter, style.xAxisLabel), timeZone: spec.timeZone, }, ); @@ -426,7 +462,7 @@ function getXTicks( return xValues.map((textBox: string | number) => { return { ...getTextValue(spec.xAxisLabelFormatter, xScale)(textBox), - x: chartDimensions.left + (xScale(textBox) || 0) + xScale.bandwidth() / 2, + x: (xScale(textBox) || 0) + xScale.bandwidth() / 2, }; }); } 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 8ec146a483..96bdd6cf48 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 @@ -13,6 +13,7 @@ import { renderMultiLine } from '../../../xy_chart/renderer/canvas/primitives/li import { renderRect } from '../../../xy_chart/renderer/canvas/primitives/rect'; import { renderText, wrapLines } from '../../../xy_chart/renderer/canvas/primitives/text'; import { ShapeViewModel } from '../../layout/types/viewmodel_types'; +import { ChartElementSizes } from '../../state/selectors/compute_chart_dimensions'; /** @internal */ export function renderCanvas2d( @@ -20,6 +21,8 @@ export function renderCanvas2d( dpr: number, { theme, heatmapViewModel }: ShapeViewModel, background: Color, + elementSizes: ChartElementSizes, + debug: boolean, ) { withContext(ctx, () => { // set some defaults for the overall rendering @@ -46,6 +49,33 @@ 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, () => { @@ -83,9 +113,10 @@ export function renderCanvas2d( }), () => - // Y Axis + // render text on Y axis theme.yAxisLabel.visible && - withContext(ctx, () => + withContext(ctx, () => { + ctx.translate(elementSizes.yAxis.left + elementSizes.yAxis.width, elementSizes.yAxis.top); filteredYValues.forEach((yValue) => { const font: Font = { fontFamily: theme.yAxisLabel.fontFamily, @@ -113,17 +144,37 @@ export function renderCanvas2d( // the alignment for y axis labels is fixed to the right { ...theme.yAxisLabel, align: 'right' }, ); - }), - ), + }); + }), () => - // Text on X axis + // render text on X axis theme.xAxisLabel.visible && - withContext(ctx, () => + withContext(ctx, () => { + ctx.translate(elementSizes.xAxis.left, elementSizes.xAxis.top); heatmapViewModel.xValues.forEach((xValue) => renderText(ctx, { x: xValue.x, y: xValue.y }, xValue.text, theme.xAxisLabel), - ), - ), + ); + }), + + () => + 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, + ); + }); + }), ]); }); } 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 24706e2acf..b189b6089b 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 @@ -21,8 +21,10 @@ import { } from '../../../../state/selectors/get_accessibility_config'; import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; import { getInternalIsInitializedSelector, InitStatus } from '../../../../state/selectors/get_internal_is_intialized'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; import { Dimensions } from '../../../../utils/dimensions'; import { nullShapeViewModel, ShapeViewModel } from '../../layout/types/viewmodel_types'; +import { ChartElementSizes, computeChartElementSizesSelector } from '../../state/selectors/compute_chart_dimensions'; import { getHeatmapGeometries } from '../../state/selectors/geometries'; import { getHeatmapContainerSizeSelector } from '../../state/selectors/get_heatmap_container_size'; import { renderCanvas2d } from './canvas_renderers'; @@ -33,6 +35,8 @@ interface ReactiveChartStateProps { chartContainerDimensions: Dimensions; a11ySettings: A11ySettings; background: Color; + elementSizes: ChartElementSizes; + debug: boolean; } interface ReactiveChartDispatchProps { @@ -96,6 +100,8 @@ class Component extends React.Component { theme: this.props.geometries.theme, }, this.props.background, + this.props.elementSizes, + this.props.debug, ); } } @@ -151,8 +157,16 @@ 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, + rowHeight: 0, + visibleNumberOfRows: 0, + }, + debug: false, }; - const mapStateToProps = (state: GlobalChartState): ReactiveChartStateProps => { if (getInternalIsInitializedSelector(state) !== InitStatus.Initialized) { return DEFAULT_PROPS; @@ -163,6 +177,8 @@ const mapStateToProps = (state: GlobalChartState): ReactiveChartStateProps => { chartContainerDimensions: getHeatmapContainerSizeSelector(state), a11ySettings: getA11ySettingsSelector(state), background: getChartThemeSelector(state).background.color, + elementSizes: computeChartElementSizesSelector(state), + debug: getSettingsSpecSelector(state).debug, }; }; 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 f9043b28eb..9347e93c2f 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,7 +11,7 @@ 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 { computeChartDimensionsSelector } from '../../state/selectors/compute_chart_dimensions'; +import { computeChartElementSizesSelector } from '../../state/selectors/compute_chart_dimensions'; import { getHeatmapGeometries } from '../../state/selectors/geometries'; import { getBrushedHighlightedShapesSelector } from '../../state/selectors/get_brushed_highlighted_shapes'; import { getHighlightedAreaSelector } from '../../state/selectors/get_highlighted_area'; @@ -25,7 +25,7 @@ const brushMapStateToProps = (state: GlobalChartState): HighlighterCellsProps => const { chartId } = state; const geoms = getHeatmapGeometries(state); - const canvasDimension = computeChartDimensionsSelector(state); + const canvasDimension = computeChartElementSizesSelector(state).grid; let dragShape = getBrushedHighlightedShapesSelector(state); const highlightedArea = getHighlightedAreaSelector(state); diff --git a/packages/charts/src/chart_types/heatmap/specs/heatmap.ts b/packages/charts/src/chart_types/heatmap/specs/heatmap.ts index 073f319eb8..ea2cb333cc 100644 --- a/packages/charts/src/chart_types/heatmap/specs/heatmap.ts +++ b/packages/charts/src/chart_types/heatmap/specs/heatmap.ts @@ -33,6 +33,8 @@ const defaultProps = { xSortPredicate: Predicate.AlphaAsc, ySortPredicate: Predicate.AlphaAsc, timeZone: 'UTC', + xAxisTitle: '', + yAxisTitle: '', xAxisLabelName: 'X Value', xAxisLabelFormatter: String, yAxisLabelName: 'Y Value', @@ -105,8 +107,10 @@ export interface HeatmapSpec extends Spec { name?: string; timeZone: string; onBrushEnd?: (brushArea: HeatmapBrushEvent) => void; + xAxisTitle: string; xAxisLabelName: string; xAxisLabelFormatter: (value: string | number) => string; + yAxisTitle: string; yAxisLabelName: string; yAxisLabelFormatter: (value: string | number) => string; } @@ -127,8 +131,10 @@ export const Heatmap: React.FunctionComponent(defaultProps), diff --git a/packages/charts/src/chart_types/heatmap/state/chart_state.tsx b/packages/charts/src/chart_types/heatmap/state/chart_state.tsx index 78dc04ae56..e2321d47fd 100644 --- a/packages/charts/src/chart_types/heatmap/state/chart_state.tsx +++ b/packages/charts/src/chart_types/heatmap/state/chart_state.tsx @@ -17,7 +17,7 @@ import { InitStatus } from '../../../state/selectors/get_internal_is_intialized' import { Dimensions } from '../../../utils/dimensions'; import { Heatmap } from '../renderer/canvas/connected_component'; import { HighlighterFromBrush } from '../renderer/dom/highlighter_brush'; -import { computeChartDimensionsSelector } from './selectors/compute_chart_dimensions'; +import { computeChartElementSizesSelector } from './selectors/compute_chart_dimensions'; import { computeLegendSelector } from './selectors/compute_legend'; import { getBrushAreaSelector } from './selectors/get_brush_area'; import { getPointerCursorSelector } from './selectors/get_cursor_pointer'; @@ -108,7 +108,7 @@ export class HeatmapState implements InternalChartState { } getMainProjectionArea(globalState: GlobalChartState): Dimensions { - return computeChartDimensionsSelector(globalState); + return computeChartElementSizesSelector(globalState).grid; } getBrushArea(globalState: GlobalChartState): Dimensions | null { diff --git a/packages/charts/src/chart_types/heatmap/state/selectors/compute_chart_dimensions.ts b/packages/charts/src/chart_types/heatmap/state/selectors/compute_chart_dimensions.ts index ca59652cf3..d6378e3c4c 100644 --- a/packages/charts/src/chart_types/heatmap/state/selectors/compute_chart_dimensions.ts +++ b/packages/charts/src/chart_types/heatmap/state/selectors/compute_chart_dimensions.ts @@ -8,16 +8,16 @@ import { max as d3Max } from 'd3-array'; -import { Box, measureText } from '../../../../common/text_utils'; +import { Box, measureText, TextMeasure } from '../../../../common/text_utils'; import { GlobalChartState } from '../../../../state/chart_state'; import { createCustomCachedSelector } from '../../../../state/create_selector'; import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; import { getLegendSizeSelector } from '../../../../state/selectors/get_legend_size'; -import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; -import { Position } from '../../../../utils/common'; -import { Dimensions } from '../../../../utils/dimensions'; +import { Dimensions, horizontalPad, innerPad, outerPad, verticalPad } from '../../../../utils/dimensions'; +import { isHorizontalLegend } from '../../../../utils/legend'; +import { AxisStyle, HeatmapStyle } from '../../../../utils/themes/theme'; import { HeatmapCellDatum } from '../../layout/viewmodel/viewmodel'; -import { getGridHeightParamsSelector } from './get_grid_full_height'; +import { getHeatmapSpecSelector } from './get_heatmap_spec'; import { getHeatmapTableSelector } from './get_heatmap_table'; import { getXAxisRightOverflow } from './get_x_axis_right_overflow'; @@ -32,73 +32,168 @@ export interface HeatmapTable { const getParentDimension = (state: GlobalChartState) => state.parentDimensions; +/** @internal */ +export type ChartElementSizes = { + yAxis: Dimensions; + xAxis: Dimensions; + grid: Dimensions; + fullHeatmapHeight: number; + rowHeight: number; + visibleNumberOfRows: number; +}; /** - * Gets charts grid area excluding legend and X,Y axis labels and paddings. + * Returns grid and axes sizes and positions. * @internal */ -export const computeChartDimensionsSelector = createCustomCachedSelector( +export const computeChartElementSizesSelector = createCustomCachedSelector( [ getParentDimension, getLegendSizeSelector, getHeatmapTableSelector, getChartThemeSelector, getXAxisRightOverflow, - getGridHeightParamsSelector, - getSettingsSpecSelector, + getHeatmapSpecSelector, ], ( - chartContainerDimensions, + container, legendSize, - heatmapTable, - { heatmap }, - rightOverflow, - { height }, - { showLegend, legendPosition }, - ): Dimensions => { - let { width, left } = chartContainerDimensions; - const { top } = chartContainerDimensions; - const { padding } = heatmap.yAxisLabel; + { yValues }, + { heatmap, axes: { axisTitle: axisTitleStyle } }, + rightOverflow, + { xAxisTitle, yAxisTitle }, + ): ChartElementSizes => { const textMeasurer = document.createElement('canvas'); const textMeasurerCtx = textMeasurer.getContext('2d'); - const textMeasure = measureText(textMeasurerCtx!); - - const totalHorizontalPadding = - typeof padding === 'number' ? padding * 2 : (padding.left ?? 0) + (padding.right ?? 0); - - if (heatmap.yAxisLabel.visible) { - // measure the text width of all rows values to get the grid area width - const boxedYValues = heatmapTable.yValues.map((value) => { - return { - text: String(value), - value, - isValue: false, - ...heatmap.yAxisLabel, - }; - }); - const measuredYValues = textMeasure(heatmap.yAxisLabel.fontSize, boxedYValues); - - let yColumnWidth: number = d3Max(measuredYValues, ({ width }) => width) ?? 0; - if (typeof heatmap.yAxisLabel.width === 'number') { - yColumnWidth = heatmap.yAxisLabel.width; - } else if (typeof heatmap.yAxisLabel.width === 'object' && yColumnWidth > heatmap.yAxisLabel.width.max) { - yColumnWidth = heatmap.yAxisLabel.width.max; - } - - width -= yColumnWidth + rightOverflow + totalHorizontalPadding; - left += yColumnWidth + totalHorizontalPadding; - } - let legendWidth = 0; - if (showLegend && (legendPosition === Position.Right || legendPosition === Position.Left)) { - legendWidth = legendSize.width - legendSize.margin * 2; + if (!textMeasurerCtx) { + return { + 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, + rowHeight: 0, + visibleNumberOfRows: 0, + }; } - width -= legendWidth; + const textMeasure = measureText(textMeasurerCtx); - return { - height, - width, - top, - left, + const isLegendHorizontal = isHorizontalLegend(legendSize.position); + const legendWidth = !isLegendHorizontal ? legendSize.width + legendSize.margin * 2 : 0; + const legendHeight = isLegendHorizontal ? heatmap.maxLegendHeight ?? legendSize.height + legendSize.margin * 2 : 0; + + const yAxisTitleHorizontalSize = getTextSizeDimension(yAxisTitle, axisTitleStyle, textMeasure, 'height'); + const yAxisWidth = getYAxisHorizontalUsedSpace(yValues, heatmap.yAxisLabel, textMeasure); + + const xAxisTitleVerticalSize = getTextSizeDimension(xAxisTitle, axisTitleStyle, textMeasure, 'height'); + const xAxisHeight = heatmap.xAxisLabel.visible + ? heatmap.xAxisLabel.fontSize + verticalPad(heatmap.xAxisLabel.padding) + : 0; + + const availableHeightForGrid = container.height - xAxisTitleVerticalSize - xAxisHeight - legendHeight; + + const rowHeight = getGridCellHeight(yValues.length, heatmap.grid, availableHeightForGrid); + const fullHeatmapHeight = rowHeight * yValues.length; + + const visibleNumberOfRows = + rowHeight > 0 && fullHeatmapHeight > availableHeightForGrid + ? Math.floor(availableHeightForGrid / rowHeight) + : yValues.length; + + const grid: Dimensions = { + width: container.width - yAxisWidth - yAxisTitleHorizontalSize - rightOverflow - legendWidth, + height: visibleNumberOfRows * rowHeight, + left: container.left + yAxisTitleHorizontalSize + yAxisWidth, + top: container.top, + }; + + const yAxis: Dimensions = { + width: yAxisWidth, + height: grid.height, + top: grid.top, + left: grid.left - yAxisWidth, + }; + + const xAxis: Dimensions = { + width: grid.width, + height: xAxisHeight, + top: grid.top + grid.height, + left: grid.left, }; + + return { grid, yAxis, xAxis, visibleNumberOfRows, fullHeatmapHeight, rowHeight }; }, ); + +function getYAxisHorizontalUsedSpace( + yValues: HeatmapTable['yValues'], + yAxisLabel: HeatmapStyle['yAxisLabel'], + textMeasure: TextMeasure, +) { + if (!yAxisLabel.visible) { + return 0; + } + + const labels = yValues.map((value) => { + return { + text: `${value}`, + value, + isValue: false, + ...yAxisLabel, + }; + }); + // account for the space required to show the longest Y axis label + const measuredLabels = textMeasure(yAxisLabel.fontSize, labels); + + const longestLabelWidth = d3Max(measuredLabels, (label) => label.width) ?? 0; + const labelsWidth = + yAxisLabel.width === 'auto' + ? longestLabelWidth + : typeof yAxisLabel.width === 'number' + ? yAxisLabel.width + : Math.max(longestLabelWidth, yAxisLabel.width.max); + + return labelsWidth + horizontalPad(yAxisLabel.padding); +} + +function getTextSizeDimension( + text: string, + style: AxisStyle['axisTitle'], + textMeasure: TextMeasure, + param: 'height' | 'width', +): number { + if (!style.visible || text === '') { + return 0; + } + const textPadding = innerPad(style.padding) + outerPad(style.padding); + if (param === 'height') { + return style.fontSize + textPadding; + } + + const textBox = textMeasure(style.fontSize, [ + { + fontVariant: 'normal', + fontWeight: 'bold', + fontStyle: style.fontStyle ?? 'normal', + fontFamily: style.fontFamily, + textColor: style.fill, + text, + }, + ]); + return textBox.length === 1 ? textBox[0].width + textPadding : 0; +} + +function getGridCellHeight(rows: number, grid: HeatmapStyle['grid'], height: number): number { + if (rows === 0) { + return height; // TODO check if this can be just 0 + } + const stretchedHeight = height / rows; + + if (stretchedHeight < grid.cellHeight.min) { + return grid.cellHeight.min; + } + if (grid.cellHeight.max !== 'fill' && stretchedHeight > grid.cellHeight.max) { + return grid.cellHeight.max; + } + + return stretchedHeight; +} diff --git a/packages/charts/src/chart_types/heatmap/state/selectors/geometries.ts b/packages/charts/src/chart_types/heatmap/state/selectors/geometries.ts index ab1e31bc71..3f4208d14b 100644 --- a/packages/charts/src/chart_types/heatmap/state/selectors/geometries.ts +++ b/packages/charts/src/chart_types/heatmap/state/selectors/geometries.ts @@ -10,12 +10,11 @@ import { GlobalChartState } from '../../../../state/chart_state'; import { createCustomCachedSelector } from '../../../../state/create_selector'; import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; import { nullShapeViewModel, ShapeViewModel } from '../../layout/types/viewmodel_types'; -import { computeChartDimensionsSelector } from './compute_chart_dimensions'; +import { render } from '../../layout/viewmodel/scenegraph'; +import { computeChartElementSizesSelector } from './compute_chart_dimensions'; import { getColorScale } from './get_color_scale'; -import { getGridHeightParamsSelector } from './get_grid_full_height'; import { getHeatmapSpecSelector } from './get_heatmap_spec'; import { getHeatmapTableSelector } from './get_heatmap_table'; -import { render } from './scenegraph'; const getDeselectedSeriesSelector = (state: GlobalChartState) => state.interactions.deselectedDataSeries; @@ -23,22 +22,13 @@ const getDeselectedSeriesSelector = (state: GlobalChartState) => state.interacti export const getHeatmapGeometries = createCustomCachedSelector( [ getHeatmapSpecSelector, - computeChartDimensionsSelector, + computeChartElementSizesSelector, getHeatmapTableSelector, getColorScale, getDeselectedSeriesSelector, - getGridHeightParamsSelector, getChartThemeSelector, ], - ( - heatmapSpec, - chartDimensions, - heatmapTable, - { bands, scale: colorScale }, - deselectedSeries, - gridHeightParams, - theme, - ): ShapeViewModel => { + (heatmapSpec, dims, heatmapTable, { bands, scale: colorScale }, deselectedSeries, theme): ShapeViewModel => { // instead of using the specId, each legend item is associated with an unique band label const disabledBandLabels = new Set( deselectedSeries.map(({ specId }) => { @@ -52,8 +42,6 @@ export const getHeatmapGeometries = createCustomCachedSelector( }) .map(({ start, end }) => [start, end]); - return heatmapSpec - ? render(heatmapSpec, chartDimensions, heatmapTable, colorScale, bandsToHide, gridHeightParams, theme) - : nullShapeViewModel(); + return heatmapSpec ? render(heatmapSpec, dims, heatmapTable, colorScale, bandsToHide, theme) : nullShapeViewModel(); }, ); diff --git a/packages/charts/src/chart_types/heatmap/state/selectors/get_brush_area.ts b/packages/charts/src/chart_types/heatmap/state/selectors/get_brush_area.ts index 61ede6a5a5..c9842838c7 100644 --- a/packages/charts/src/chart_types/heatmap/state/selectors/get_brush_area.ts +++ b/packages/charts/src/chart_types/heatmap/state/selectors/get_brush_area.ts @@ -12,9 +12,8 @@ import { createCustomCachedSelector } from '../../../../state/create_selector'; import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; import { clamp } from '../../../../utils/common'; import { Dimensions } from '../../../../utils/dimensions'; -import { computeChartDimensionsSelector } from './compute_chart_dimensions'; +import { computeChartElementSizesSelector } from './compute_chart_dimensions'; import { getBrushedHighlightedShapesSelector } from './get_brushed_highlighted_shapes'; -import { getGridHeightParamsSelector } from './get_grid_full_height'; const getMouseDownPosition = (state: GlobalChartState) => state.interactions.pointer.down; const getIsDragging = (state: GlobalChartState) => state.interactions.pointer.dragging; @@ -27,35 +26,33 @@ export const getBrushAreaSelector = createCustomCachedSelector( getMouseDownPosition, getCurrentPointerPosition, getSettingsSpecSelector, - computeChartDimensionsSelector, + computeChartElementSizesSelector, getBrushedHighlightedShapesSelector, - getGridHeightParamsSelector, ], - (isDragging, mouseDownPosition, end, { brushAxis }, chartDimensions, dragShape, gridParams): Dimensions | null => { + (isDragging, mouseDownPosition, end, { brushAxis }, dims, dragShape): Dimensions | null => { if (!isDragging || !mouseDownPosition || !dragShape) { return null; } const start = { - x: mouseDownPosition.position.x - chartDimensions.left, + x: mouseDownPosition.position.x - dims.grid.left, y: mouseDownPosition.position.y, }; - const clampedEndY = clamp(end.y, 0, gridParams.gridCellHeight * gridParams.pageSize); - + const clampedEndY = clamp(end.y, 0, dims.grid.height); switch (brushAxis) { case BrushAxis.Both: return { top: start.y, left: start.x, - width: end.x - start.x - chartDimensions.left, + width: end.x - start.x - dims.grid.left, height: clampedEndY - start.y, }; default: return { top: start.y, left: start.x, - width: end.x - start.x - chartDimensions.left, + width: end.x - start.x - dims.grid.left, height: clampedEndY - start.y, }; } diff --git a/packages/charts/src/chart_types/heatmap/state/selectors/get_grid_full_height.ts b/packages/charts/src/chart_types/heatmap/state/selectors/get_grid_full_height.ts deleted file mode 100644 index a5e7fd947c..0000000000 --- a/packages/charts/src/chart_types/heatmap/state/selectors/get_grid_full_height.ts +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { GlobalChartState } from '../../../../state/chart_state'; -import { createCustomCachedSelector } from '../../../../state/create_selector'; -import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; -import { getLegendSizeSelector } from '../../../../state/selectors/get_legend_size'; -import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; -import { isHorizontalLegend } from '../../../../utils/legend'; -import { HeatmapStyle } from '../../../../utils/themes/theme'; -import { getHeatmapTableSelector } from './get_heatmap_table'; - -/** @internal */ -export interface GridHeightParams { - height: number; - gridCellHeight: number; - pageSize: number; -} -const getParentDimension = (state: GlobalChartState) => state.parentDimensions; - -/** @internal */ -export const getGridHeightParamsSelector = createCustomCachedSelector( - [getLegendSizeSelector, getSettingsSpecSelector, getParentDimension, getChartThemeSelector, getHeatmapTableSelector], - ( - legendSize, - { showLegend }, - { height: containerHeight }, - { - heatmap: { - xAxisLabel: { padding, visible, fontSize }, - grid, - maxLegendHeight, - }, - }, - { yValues }, - ): GridHeightParams => { - const xAxisHeight = visible ? fontSize : 0; - const totalVerticalPadding = padding * 2; - let legendHeight = 0; - if (showLegend && isHorizontalLegend(legendSize.position)) { - legendHeight = maxLegendHeight ?? legendSize.height; - } - const verticalRemainingSpace = containerHeight - xAxisHeight - totalVerticalPadding - legendHeight; - - // compute the grid cell height - const gridCellHeight = getGridCellHeight(yValues, grid, verticalRemainingSpace); - const height = gridCellHeight * yValues.length; - - const pageSize = - gridCellHeight > 0 && height > containerHeight - ? Math.floor(verticalRemainingSpace / gridCellHeight) - : yValues.length; - return { - height, - gridCellHeight, - pageSize, - }; - }, -); - -function getGridCellHeight(yValues: Array, grid: HeatmapStyle['grid'], height: number): number { - if (yValues.length === 0) { - return height; - } - const stretchedHeight = height / yValues.length; - - if (stretchedHeight < grid.cellHeight.min) { - return grid.cellHeight.min; - } - if (grid.cellHeight.max !== 'fill' && stretchedHeight > grid.cellHeight.max) { - return grid.cellHeight.max; - } - - return stretchedHeight; -} diff --git a/packages/charts/src/chart_types/heatmap/state/selectors/get_picked_cells.ts b/packages/charts/src/chart_types/heatmap/state/selectors/get_picked_cells.ts index b0dc57bcf8..ddceafcc7f 100644 --- a/packages/charts/src/chart_types/heatmap/state/selectors/get_picked_cells.ts +++ b/packages/charts/src/chart_types/heatmap/state/selectors/get_picked_cells.ts @@ -9,31 +9,27 @@ import { createCustomCachedSelector } from '../../../../state/create_selector'; import { getLastDragSelector } from '../../../../state/selectors/get_last_drag'; import { PickDragFunction } from '../../layout/types/viewmodel_types'; -import { computeChartDimensionsSelector } from './compute_chart_dimensions'; +import { computeChartElementSizesSelector } from './compute_chart_dimensions'; import { getHeatmapGeometries } from './geometries'; -import { getGridHeightParamsSelector } from './get_grid_full_height'; /** @internal */ export const getPickedCells = createCustomCachedSelector( - [getHeatmapGeometries, getLastDragSelector, computeChartDimensionsSelector, getGridHeightParamsSelector], - (geoms, dragState, canvasDimensions, gridParams): ReturnType | null => { + [getHeatmapGeometries, getLastDragSelector, computeChartElementSizesSelector], + (geoms, dragState, dims): ReturnType | null => { if (!dragState) { return null; } // the pointer is not on the cells but over the y- axis and does not cross the y-axis - if (dragState.start.position.x < canvasDimensions.left && dragState.end.position.x < canvasDimensions.left) { - const fittedDragStateStart = { x: canvasDimensions.left, y: dragState.start.position.y }; + if (dragState.start.position.x < dims.grid.left && dragState.end.position.x < dims.grid.left) { + const fittedDragStateStart = { x: dims.grid.left, y: dragState.start.position.y }; const { y, cells } = geoms.pickDragArea([fittedDragStateStart, dragState.end.position]); return { x: [], y, cells }; } // the pointer is not on the cells by over the x-axis and does not cross the x-axis - if ( - dragState.start.position.y > gridParams.gridCellHeight * gridParams.pageSize && - dragState.end.position.y > gridParams.gridCellHeight * gridParams.pageSize - ) { - const fittedDragStateStart = { x: dragState.start.position.x, y: canvasDimensions.height }; + if (dragState.start.position.y > dims.grid.height && dragState.end.position.y > dims.grid.height) { + const fittedDragStateStart = { x: dragState.start.position.x, y: dims.grid.height }; const { x, cells } = geoms.pickDragArea([fittedDragStateStart, dragState.end.position]); return { x, y: [], cells }; } diff --git a/packages/charts/src/chart_types/heatmap/state/selectors/get_tooltip_anchor.ts b/packages/charts/src/chart_types/heatmap/state/selectors/get_tooltip_anchor.ts index 704e6d4946..122b989894 100644 --- a/packages/charts/src/chart_types/heatmap/state/selectors/get_tooltip_anchor.ts +++ b/packages/charts/src/chart_types/heatmap/state/selectors/get_tooltip_anchor.ts @@ -9,7 +9,7 @@ import { AnchorPosition } from '../../../../components/portal/types'; import { GlobalChartState } from '../../../../state/chart_state'; import { createCustomCachedSelector } from '../../../../state/create_selector'; -import { computeChartDimensionsSelector } from './compute_chart_dimensions'; +import { computeChartElementSizesSelector } from './compute_chart_dimensions'; import { getPickedShapes } from './picked_shapes'; function getCurrentPointerPosition(state: GlobalChartState) { @@ -18,14 +18,14 @@ function getCurrentPointerPosition(state: GlobalChartState) { /** @internal */ export const getTooltipAnchorSelector = createCustomCachedSelector( - [getPickedShapes, computeChartDimensionsSelector, getCurrentPointerPosition], - (shapes, chartDimensions, position): AnchorPosition => { + [getPickedShapes, computeChartElementSizesSelector, getCurrentPointerPosition], + (shapes, { grid }, position): AnchorPosition => { if (Array.isArray(shapes) && shapes.length > 0) { const firstShape = shapes[0]; return { - x: firstShape.x + chartDimensions.left, + x: firstShape.x + grid.left, width: firstShape.width, - y: firstShape.y - chartDimensions.top, + y: firstShape.y - grid.top, height: firstShape.height, }; } diff --git a/packages/charts/src/chart_types/heatmap/state/selectors/get_x_axis_right_overflow.ts b/packages/charts/src/chart_types/heatmap/state/selectors/get_x_axis_right_overflow.ts index 8502da5233..343c0f8ed2 100644 --- a/packages/charts/src/chart_types/heatmap/state/selectors/get_x_axis_right_overflow.ts +++ b/packages/charts/src/chart_types/heatmap/state/selectors/get_x_axis_right_overflow.ts @@ -11,6 +11,7 @@ import { ScaleType } from '../../../../scales/constants'; import { createCustomCachedSelector } from '../../../../state/create_selector'; import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; import { withTextMeasure } from '../../../../utils/bbox/canvas_text_bbox_calculator'; +import { horizontalPad } from '../../../../utils/dimensions'; import { getHeatmapSpecSelector } from './get_heatmap_spec'; import { getHeatmapTableSelector } from './get_heatmap_table'; @@ -40,7 +41,8 @@ export const getXAxisRightOverflow = createCustomCachedSelector( ) .ticks() .reduce( - (max, n) => Math.max(max, measure(xAxisLabelFormatter(n), padding, fontSize, fontFamily).width + padding), + (max, n) => + Math.max(max, measure(xAxisLabelFormatter(n), horizontalPad(padding), fontSize, fontFamily).width), 0, ); }) / 2; diff --git a/packages/charts/src/chart_types/heatmap/state/selectors/picked_shapes.ts b/packages/charts/src/chart_types/heatmap/state/selectors/picked_shapes.ts index f4fb69a9a5..3b735f6c7b 100644 --- a/packages/charts/src/chart_types/heatmap/state/selectors/picked_shapes.ts +++ b/packages/charts/src/chart_types/heatmap/state/selectors/picked_shapes.ts @@ -9,8 +9,8 @@ import { GlobalChartState } from '../../../../state/chart_state'; import { createCustomCachedSelector } from '../../../../state/create_selector'; import { Cell, TextBox } from '../../layout/types/viewmodel_types'; +import { computeChartElementSizesSelector } from './compute_chart_dimensions'; import { getHeatmapGeometries } from './geometries'; -import { getGridHeightParamsSelector } from './get_grid_full_height'; function getCurrentPointerPosition(state: GlobalChartState) { return state.interactions.pointer.current.position; @@ -18,13 +18,13 @@ function getCurrentPointerPosition(state: GlobalChartState) { /** @internal */ export const getPickedShapes = createCustomCachedSelector( - [getHeatmapGeometries, getCurrentPointerPosition, getGridHeightParamsSelector], - (geoms, pointerPosition, gridParams): Cell[] | TextBox => { + [getHeatmapGeometries, getCurrentPointerPosition, computeChartElementSizesSelector], + (geoms, pointerPosition, dims): Cell[] | TextBox => { const picker = geoms.pickQuads; const { x, y } = pointerPosition; const pickedData = picker(x, y); return Array.isArray(pickedData) - ? pickedData.filter(({ y }) => y < gridParams.gridCellHeight * gridParams.pageSize) + ? pickedData.filter(({ y }) => y < dims.rowHeight * dims.visibleNumberOfRows) : pickedData; }, ); diff --git a/packages/charts/src/mocks/specs/specs.ts b/packages/charts/src/mocks/specs/specs.ts index 5520dfe6ea..552634101e 100644 --- a/packages/charts/src/mocks/specs/specs.ts +++ b/packages/charts/src/mocks/specs/specs.ts @@ -201,6 +201,8 @@ export class MockSeriesSpec { xSortPredicate: Predicate.AlphaAsc, ySortPredicate: Predicate.AlphaAsc, timeZone: 'UTC', + xAxisTitle: '', + yAxisTitle: '', xAxisLabelName: 'X Value', xAxisLabelFormatter: String, yAxisLabelName: 'Y Value', diff --git a/packages/charts/src/utils/dimensions.ts b/packages/charts/src/utils/dimensions.ts index 405476da8b..1b5f6f2889 100644 --- a/packages/charts/src/utils/dimensions.ts +++ b/packages/charts/src/utils/dimensions.ts @@ -6,6 +6,8 @@ * Side Public License, v 1. */ +import { Pixels } from '../common/geometry'; + /** @internal */ export interface Dimensions { top: number; @@ -39,7 +41,7 @@ export interface PerSideDistance { export type Margins = PerSideDistance; /** - * todo seperate type with parition padding type that allows number + * todo separate type with partition padding type that allows number * @public */ export type Padding = PerSideDistance; @@ -49,8 +51,8 @@ export type Padding = PerSideDistance; * @public */ export interface SimplePadding { - outer: number; - inner: number; + outer: Pixels; + inner: Pixels; } /** @internal */ @@ -60,3 +62,15 @@ export const innerPad = (padding: number | Partial, minPadding = /** @internal */ export const outerPad = (padding: number | Partial, minPadding = 0) => Math.max(minPadding, typeof padding === 'number' ? padding : padding?.outer ?? 0); + +/** @internal */ +export const verticalPad = (padding: number | Padding, minPadding = 0) => + Math.max(minPadding, typeof padding === 'number' ? padding * 2 : padding.top + padding.bottom); + +/** @internal */ +export const horizontalPad = (padding: number | Padding, minPadding = 0) => + Math.max(minPadding, typeof padding === 'number' ? padding * 2 : padding.left + padding.right); + +/** @internal */ +export const pad = (padding: number | Padding, direction: keyof Padding, minPadding = 0) => + Math.max(minPadding, typeof padding === 'number' ? padding : padding[direction]); diff --git a/packages/charts/src/utils/themes/dark_theme.ts b/packages/charts/src/utils/themes/dark_theme.ts index 330e5b9883..c4cf60284b 100644 --- a/packages/charts/src/utils/themes/dark_theme.ts +++ b/packages/charts/src/utils/themes/dark_theme.ts @@ -325,6 +325,7 @@ export const DARK_THEME: Theme = { visible: false, fill: 'snow', }, + xAxisLabel: { visible: true, width: 'auto', @@ -336,7 +337,7 @@ export const DARK_THEME: Theme = { fontWeight: 'normal', align: 'center', baseline: 'middle', - padding: 6, + padding: { top: 5, bottom: 5, left: 5, right: 5 }, }, yAxisLabel: { visible: true, @@ -348,7 +349,7 @@ export const DARK_THEME: Theme = { fontVariant: 'normal', fontWeight: 'normal', baseline: 'middle', - padding: 5, + padding: { top: 5, bottom: 5, left: 5, right: 5 }, }, grid: { cellWidth: { diff --git a/packages/charts/src/utils/themes/light_theme.ts b/packages/charts/src/utils/themes/light_theme.ts index d7fb2d8818..471fc026c3 100644 --- a/packages/charts/src/utils/themes/light_theme.ts +++ b/packages/charts/src/utils/themes/light_theme.ts @@ -336,7 +336,7 @@ export const LIGHT_THEME: Theme = { fontWeight: 'normal', align: 'center', baseline: 'middle', - padding: 6, + padding: { top: 5, bottom: 5, left: 5, right: 5 }, }, yAxisLabel: { visible: true, @@ -348,7 +348,7 @@ export const LIGHT_THEME: Theme = { fontVariant: 'normal', fontWeight: 'normal', baseline: 'middle', - padding: 5, + padding: { top: 5, bottom: 5, left: 5, right: 5 }, }, grid: { cellWidth: { diff --git a/packages/charts/src/utils/themes/theme.ts b/packages/charts/src/utils/themes/theme.ts index 4f12d4568b..e66c074a20 100644 --- a/packages/charts/src/utils/themes/theme.ts +++ b/packages/charts/src/utils/themes/theme.ts @@ -12,7 +12,7 @@ import { Color } from '../../common/colors'; import { Pixels, Ratio } from '../../common/geometry'; import { Font, FontStyle, TextAlign, TextBaseline } from '../../common/text_utils'; import { ColorVariant, HorizontalAlignment, RecursivePartial, VerticalAlignment } from '../common'; -import { Margins, SimplePadding } from '../dimensions'; +import { Margins, Padding, SimplePadding } from '../dimensions'; import { Point } from '../point'; import { PartitionStyle } from './partition'; @@ -217,14 +217,14 @@ export interface HeatmapStyle { align: TextAlign; baseline: TextBaseline; visible: boolean; - padding: number; + padding: Pixels | Padding; }; yAxisLabel: Font & { fontSize: Pixels; width: Pixels | 'auto' | { max: Pixels }; baseline: TextBaseline; visible: boolean; - padding: number | { left?: number; right?: number; top?: number; bottom?: number }; + padding: Pixels | Padding; }; grid: { cellWidth: { diff --git a/storybook/stories/heatmap/1_basic.story.tsx b/storybook/stories/heatmap/1_basic.story.tsx index 1e3e4d3e0d..aee35f99cc 100644 --- a/storybook/stories/heatmap/1_basic.story.tsx +++ b/storybook/stories/heatmap/1_basic.story.tsx @@ -33,6 +33,9 @@ export const Example = () => { const persistCellsSelection = boolean('Persist cells selection', true); const debugState = boolean('Enable debug state', true); + const showXAxisTitle = boolean('Show x axis title', false); + const showYAxisTitle = boolean('Show y axis title', false); + const dataStateAction = action('DataState'); const handler = useCallback(() => { @@ -141,6 +144,8 @@ export const Example = () => { setSelection({ x: e.x, y: e.y }); }} highlightedData={persistCellsSelection ? selection : undefined} + xAxisTitle={showXAxisTitle ? 'Bottom axis' : undefined} + yAxisTitle={showYAxisTitle ? 'Left axis' : undefined} /> ); diff --git a/storybook/stories/heatmap/2_categorical.story.tsx b/storybook/stories/heatmap/2_categorical.story.tsx index c2eade6e94..9d43c5ebe5 100644 --- a/storybook/stories/heatmap/2_categorical.story.tsx +++ b/storybook/stories/heatmap/2_categorical.story.tsx @@ -28,6 +28,9 @@ export const Example = () => { const minCellHeight = number('min cell height', 10, { step: 1, min: 3, max: 8, range: true }, 'grid'); const maxCellHeight = number('max cell height', 30, { step: 1, min: 8, max: 45, range: true }, 'grid'); + const showXAxisTitle = boolean('Show x axis title', false); + const showYAxisTitle = boolean('Show y axis title', false); + return ( { valueAccessor={(d) => d[3]} valueFormatter={(value) => value.toFixed(0.2)} xSortPredicate="alphaAsc" + xAxisTitle={showXAxisTitle ? 'Bottom axis' : undefined} + yAxisTitle={showYAxisTitle ? 'Left axis' : undefined} /> ); diff --git a/storybook/stories/heatmap/4_test_time_snap.story.tsx b/storybook/stories/heatmap/4_test_time_snap.story.tsx index 31acc3711c..4aac32c189 100644 --- a/storybook/stories/heatmap/4_test_time_snap.story.tsx +++ b/storybook/stories/heatmap/4_test_time_snap.story.tsx @@ -56,6 +56,9 @@ const theme: PartialTheme = { width: 'auto', padding: { left: 10, right: 10 }, }, + xAxisLabel: { + padding: { top: 4, bottom: 4 }, + }, }, }; diff --git a/storybook/stories/heatmap/5_theming.story.tsx b/storybook/stories/heatmap/5_theming.story.tsx index b05c239782..735b2672c4 100644 --- a/storybook/stories/heatmap/5_theming.story.tsx +++ b/storybook/stories/heatmap/5_theming.story.tsx @@ -7,7 +7,7 @@ */ import { action } from '@storybook/addon-actions'; -import { boolean, color, number } from '@storybook/addon-knobs'; +import { boolean, color, number, text } from '@storybook/addon-knobs'; import React from 'react'; import { debounce } from 'ts-debounce'; @@ -23,12 +23,23 @@ import { } from '@elastic/charts'; import { DATA_6 } from '@elastic/charts/src/utils/data_samples/test_dataset_heatmap'; +import { AxisStyle } from '../../../packages/charts/src/utils/themes/theme'; import { useBaseTheme } from '../../use_base_theme'; export const Example = () => { const debugState = boolean('Enable debug state', true); const dataStateAction = action('DataState'); - + const axes: RecursivePartial = { + axisTitle: { + fontSize: number('axisTitle fontSize', 12, { range: true, min: 5, max: 20 }, 'Axis Title'), + fontFamily: 'sans-serif', + fill: color('axisTitle textColor', 'black', 'Axis Title'), + padding: { + inner: number('axisTitle inner pad', 8, { range: true, min: 0, max: 20 }, 'Axis Title'), + outer: number('axisTitle outer pad', 8, { range: true, min: 0, max: 20 }, 'Axis Title'), + }, + }, + }; const heatmap: RecursivePartial = { brushArea: { visible: boolean('brushArea visible', true, 'Theme'), @@ -99,7 +110,7 @@ export const Example = () => { brushAxis="both" xDomain={{ min: 1572868800000, max: 1572912000000, minInterval: 1800000 }} debugState={debugState} - theme={{ heatmap }} + theme={{ axes, heatmap }} baseTheme={useBaseTheme()} /> { xAxisLabelFormatter={(value) => { return niceTimeFormatter([1572825600000, 1572912000000])(value, { timeZone: 'UTC' }); }} + xAxisTitle={text('xAxisTitle', 'xAxis', 'Axis Title')} + yAxisTitle={text('yAxisTitle', 'yAxis', 'Axis Title')} timeZone={DATA_6.timeZone} />