diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c1d203a1001..23b05d28f3c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -9,8 +9,9 @@ env: on: push: - branches: + branches: - master + - alpha - next - '[0-9]+.[0-9]+.[0-9]+' - '[0-9]+.[0-9]+.x' @@ -18,6 +19,7 @@ on: pull_request: branches: - master + - alpha - next - '[0-9]+.[0-9]+.[0-9]+' - '[0-9]+.[0-9]+.x' 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 new file mode 100644 index 00000000000..8b313f304d8 Binary files /dev/null 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__/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 new file mode 100644 index 00000000000..1f1d4de6cf1 Binary files /dev/null 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 new file mode 100644 index 00000000000..f0f683ea7fd Binary files /dev/null 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 new file mode 100644 index 00000000000..650a786c198 Binary files /dev/null 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 new file mode 100644 index 00000000000..e417ac362bb Binary files /dev/null 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 new file mode 100644 index 00000000000..5529bde4dcb Binary files /dev/null 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 new file mode 100644 index 00000000000..55ce2ab79a3 Binary files /dev/null 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 new file mode 100644 index 00000000000..5529bde4dcb Binary files /dev/null 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 new file mode 100644 index 00000000000..55ce2ab79a3 Binary files /dev/null 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/heatmap_stories.test.ts b/integration/tests/heatmap_stories.test.ts index 706b6e26864..10e5ecd8bc4 100644 --- a/integration/tests/heatmap_stories.test.ts +++ b/integration/tests/heatmap_stories.test.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { eachTheme } from '../helpers'; import { common } from '../page_objects'; describe('Heatmap stories', () => { @@ -16,4 +17,20 @@ describe('Heatmap stories', () => { { left: 300, top: 300 }, ); }); + + eachTheme.describe((_, themeParams) => { + it('should render basic heatmap', async () => { + await common.expectChartAtUrlToMatchScreenshot( + `http://localhost:9001/?path=/story/heatmap-alpha--basic${themeParams}`, + ); + }); + + it('should render correct brush area', async () => { + await common.expectChartWithDragAtUrlToMatchScreenshot( + `http://localhost:9001/?path=/story/heatmap-alpha--basic${themeParams}`, + { left: 200, top: 100 }, + { left: 400, top: 250 }, + ); + }); + }); }); diff --git a/packages/charts/api/charts.api.md b/packages/charts/api/charts.api.md index 56ebff79247..6e5e52cef42 100644 --- a/packages/charts/api/charts.api.md +++ b/packages/charts/api/charts.api.md @@ -795,7 +795,7 @@ export function getNodeName(node: ArrayNode): string; // Warning: (ae-forgotten-export) The symbol "SpecOptionalProps" needs to be exported by the entry point index.d.ts // // @alpha (undocumented) -export const Goal: React_2.FunctionComponent; +export const Goal: React_2.FunctionComponent; // @alpha (undocumented) export type GoalLabelAccessor = LabelAccessor; @@ -917,8 +917,11 @@ export interface GroupBySpec extends Spec { // @public (undocumented) export type GroupId = string; +// Warning: (ae-forgotten-export) The symbol "SpecRequiredProps" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "SpecOptionalProps" needs to be exported by the entry point index.d.ts +// // @alpha (undocumented) -export const Heatmap: React_2.FunctionComponent & Partial>>; +export const Heatmap: React_2.FunctionComponent; // @alpha (undocumented) export interface HeatmapBandsColorScale { @@ -937,10 +940,58 @@ export type HeatmapBrushEvent = { }; // @public (undocumented) -export interface HeatmapConfig { +export type HeatmapElementEvent = [Cell, SeriesIdentifier]; + +// @alpha (undocumented) +export interface HeatmapSpec extends Spec { + // (undocumented) + chartType: typeof ChartType.Heatmap; + // (undocumented) + colorScale: HeatmapBandsColorScale; + // (undocumented) + data: Datum[]; + // (undocumented) + highlightedData?: { + x: Array; + y: Array; + }; + // (undocumented) + name?: string; + // (undocumented) + onBrushEnd?: (brushArea: HeatmapBrushEvent) => void; + // (undocumented) + specType: typeof SpecType.Series; + // (undocumented) + timeZone: string; + // (undocumented) + valueAccessor: Accessor | AccessorFn; + // (undocumented) + valueFormatter: (value: number) => string; + // (undocumented) + xAccessor: Accessor | AccessorFn; + // (undocumented) + xAxisLabelFormatter: (value: string | number) => string; + // (undocumented) + xAxisLabelName: string; + // (undocumented) + xScaleType: SeriesScales['xScaleType']; + // (undocumented) + xSortPredicate: Predicate; + // (undocumented) + yAccessor: Accessor | AccessorFn; + // (undocumented) + yAxisLabelFormatter: (value: string | number) => string; + // (undocumented) + yAxisLabelName: string; + // (undocumented) + ySortPredicate: Predicate; +} + +// @public (undocumented) +export interface HeatmapStyle { brushArea: { visible: boolean; - fill: Color; + fill?: Color; stroke: Color; strokeWidth: number; }; @@ -969,10 +1020,6 @@ export interface HeatmapConfig { stroke: Color; }; }; - // Warning: (ae-forgotten-export) The symbol "FontFamily" needs to be exported by the entry point index.d.ts - // - // (undocumented) - fontFamily: FontFamily; // (undocumented) grid: { cellWidth: { @@ -989,44 +1036,26 @@ export interface HeatmapConfig { }; }; // (undocumented) - height: Pixels; - // (undocumented) - margin: { - left: SizeRatio; - right: SizeRatio; - top: SizeRatio; - bottom: SizeRatio; - }; - // (undocumented) maxColumnWidth: Pixels; // (undocumented) maxLegendHeight?: number; - // (undocumented) - maxRowHeight: Pixels; - // (undocumented) - onBrushEnd?: (brushArea: HeatmapBrushEvent) => void; - // (undocumented) - timeZone: string; // Warning: (ae-forgotten-export) The symbol "Pixels" needs to be exported by the entry point index.d.ts // // (undocumented) - width: Pixels; + maxRowHeight: Pixels; // Warning: (ae-forgotten-export) The symbol "Font" needs to be exported by the entry point index.d.ts // // (undocumented) xAxisLabel: Font & { - name: string; fontSize: Pixels; width: Pixels | 'auto'; align: TextAlign; baseline: TextBaseline; visible: boolean; padding: number; - formatter: (value: string | number) => string; }; // (undocumented) yAxisLabel: Font & { - name: string; fontSize: Pixels; width: Pixels | 'auto' | { max: Pixels; @@ -1039,48 +1068,9 @@ export interface HeatmapConfig { top?: number; bottom?: number; }; - formatter: (value: string | number) => string; }; } -// @public (undocumented) -export type HeatmapElementEvent = [Cell, SeriesIdentifier]; - -// @alpha (undocumented) -export interface HeatmapSpec extends Spec { - // (undocumented) - chartType: typeof ChartType.Heatmap; - // (undocumented) - colorScale: HeatmapBandsColorScale; - // (undocumented) - config: RecursivePartial; - // (undocumented) - data: Datum[]; - // (undocumented) - highlightedData?: { - x: Array; - y: Array; - }; - // (undocumented) - name?: string; - // (undocumented) - specType: typeof SpecType.Series; - // (undocumented) - valueAccessor: Accessor | AccessorFn; - // (undocumented) - valueFormatter: (value: number) => string; - // (undocumented) - xAccessor: Accessor | AccessorFn; - // (undocumented) - xScaleType: SeriesScales['xScaleType']; - // (undocumented) - xSortPredicate: Predicate; - // (undocumented) - yAccessor: Accessor | AccessorFn; - // (undocumented) - ySortPredicate: Predicate; -} - // @public export const HIERARCHY_ROOT_KEY: Key; @@ -2128,6 +2118,8 @@ export interface Theme { // (undocumented) goal: GoalStyles; // (undocumented) + heatmap: HeatmapStyle; + // (undocumented) legend: LegendStyle; lineSeriesStyle: LineSeriesStyle; markSizeRatio?: number; @@ -2306,7 +2298,7 @@ export type WeightFn = $Values; // Warning: (ae-forgotten-export) The symbol "SpecOptionalProps" needs to be exported by the entry point index.d.ts // // @alpha (undocumented) -export const Wordcloud: React_2.FunctionComponent; +export const Wordcloud: React_2.FunctionComponent; // @public (undocumented) export interface WordcloudConfigs { @@ -2433,11 +2425,10 @@ export type YDomainRange = YDomainBase & DomainRange & LogScaleOptions; // Warnings were encountered during analysis: // -// src/chart_types/heatmap/layout/types/config_types.ts:20:13 - (ae-forgotten-export) The symbol "SizeRatio" needs to be exported by the entry point index.d.ts -// src/chart_types/heatmap/layout/types/config_types.ts:51:5 - (ae-forgotten-export) The symbol "TextAlign" needs to be exported by the entry point index.d.ts -// src/chart_types/heatmap/layout/types/config_types.ts:52:5 - (ae-forgotten-export) The symbol "TextBaseline" needs to be exported by the entry point index.d.ts // src/chart_types/partition_chart/layout/types/config_types.ts:139:5 - (ae-forgotten-export) The symbol "TimeMs" needs to be exported by the entry point index.d.ts // src/chart_types/partition_chart/layout/types/config_types.ts:140:5 - (ae-forgotten-export) The symbol "AnimKeyframe" needs to be exported by the entry point index.d.ts +// src/utils/themes/theme.ts:214:5 - (ae-forgotten-export) The symbol "TextAlign" needs to be exported by the entry point index.d.ts +// src/utils/themes/theme.ts:215:5 - (ae-forgotten-export) The symbol "TextBaseline" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/packages/charts/src/chart_types/heatmap/layout/config/config.ts b/packages/charts/src/chart_types/heatmap/layout/config/config.ts deleted file mode 100644 index 6957f4f208a..00000000000 --- a/packages/charts/src/chart_types/heatmap/layout/config/config.ts +++ /dev/null @@ -1,107 +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 { Config } from '../types/config_types'; - -/** @internal */ -export const config: Config = { - width: 500, - height: 500, - margin: { left: 0.01, right: 0.01, top: 0.01, bottom: 0.01 }, - maxRowHeight: 30, - maxColumnWidth: 30, - - fontFamily: 'Sans-Serif', - - onBrushEnd: undefined, - - brushArea: { - visible: true, - fill: 'black', // black === transparent - stroke: '#69707D', // euiColorDarkShade, - strokeWidth: 2, - }, - brushMask: { - visible: true, - fill: 'rgb(115 115 115 / 50%)', - }, - brushTool: { - visible: false, - fill: 'gray', - }, - - timeZone: 'UTC', - - xAxisLabel: { - name: 'X Value', - visible: true, - width: 'auto', - fontSize: 12, - fontFamily: 'Sans-Serif', - fontStyle: 'normal', - textColor: 'black', - fontVariant: 'normal', - fontWeight: 'normal', - textOpacity: 1, - align: 'center' as CanvasTextAlign, - baseline: 'verticalAlign' as CanvasTextBaseline, - padding: 6, - formatter: String, - }, - yAxisLabel: { - name: 'Y Value', - visible: true, - width: 'auto', - fontSize: 12, - fontFamily: 'Sans-Serif', - fontStyle: 'normal', - textColor: 'black', - fontVariant: 'normal', - fontWeight: 'normal', - textOpacity: 1, - baseline: 'verticalAlign' as CanvasTextBaseline, - padding: 5, - formatter: String, - }, - grid: { - cellWidth: { - min: 0, - max: 30, - }, - cellHeight: { - min: 12, - max: 30, - }, - stroke: { - width: 1, - color: 'gray', - }, - }, - cell: { - maxWidth: 'fill', - maxHeight: 'fill', - align: 'center', - label: { - visible: true, - maxWidth: 'fill', - fontSize: 10, - fontFamily: 'Sans-Serif', - fontStyle: 'normal', - textColor: 'black', - fontVariant: 'normal', - fontWeight: 'normal', - textOpacity: 1, - align: 'center' as CanvasTextAlign, - baseline: 'verticalAlign' as CanvasTextBaseline, - }, - border: { - strokeWidth: 1, - stroke: 'gray', - }, - }, -}; diff --git a/packages/charts/src/chart_types/heatmap/layout/types/config_types.ts b/packages/charts/src/chart_types/heatmap/layout/types/config_types.ts deleted file mode 100644 index a1b7a3fef28..00000000000 --- a/packages/charts/src/chart_types/heatmap/layout/types/config_types.ts +++ /dev/null @@ -1,104 +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 { Pixels, SizeRatio } from '../../../../common/geometry'; -import { Font, FontFamily, TextAlign, TextBaseline } from '../../../../common/text_utils'; -import { Color } from '../../../../utils/common'; -import { Cell } from './viewmodel_types'; - -/** - * @public - */ -export interface Config { - width: Pixels; - height: Pixels; - margin: { left: SizeRatio; right: SizeRatio; top: SizeRatio; bottom: SizeRatio }; - maxRowHeight: Pixels; - maxColumnWidth: Pixels; - // general text config - fontFamily: FontFamily; - - timeZone: string; - - onBrushEnd?: (brushArea: HeatmapBrushEvent) => void; - - /** - * Config of the mask over the area outside of the selected cells - */ - brushMask: { visible: boolean; fill: Color }; - /** - * Config of the mask over the selected cells - */ - brushArea: { visible: boolean; fill: Color; stroke: Color; strokeWidth: number }; - /** - * Config of the brushing tool - */ - brushTool: { - visible: boolean; - // TODO add support for changing the brush tool color - fill: Color; - }; - - xAxisLabel: Font & { - name: string; - fontSize: Pixels; - width: Pixels | 'auto'; - align: TextAlign; - baseline: TextBaseline; - visible: boolean; - padding: number; - formatter: (value: string | number) => string; - }; - yAxisLabel: Font & { - name: string; - fontSize: Pixels; - width: Pixels | 'auto' | { max: Pixels }; - baseline: TextBaseline; - visible: boolean; - padding: number | { left?: number; right?: number; top?: number; bottom?: number }; - formatter: (value: string | number) => string; - }; - grid: { - cellWidth: { - min: Pixels; - max: Pixels | 'fill'; - }; - cellHeight: { - min: Pixels; - max: Pixels | 'fill'; - }; - stroke: { - color: string; - width: number; - }; - }; - cell: { - maxWidth: Pixels | 'fill'; - maxHeight: Pixels | 'fill'; - align: 'center'; - label: Font & { - fontSize: Pixels; - maxWidth: Pixels | 'fill'; - align: TextAlign; - baseline: TextBaseline; - visible: boolean; - }; - border: { - strokeWidth: Pixels; - stroke: Color; - }; - }; - maxLegendHeight?: number; -} - -/** @public */ -export type HeatmapBrushEvent = { - cells: Cell[]; - x: (string | number)[]; - y: (string | number)[]; -}; 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 5210767e7e6..bf7d3efa691 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 @@ -11,10 +11,11 @@ import { Pixels } from '../../../../common/geometry'; import { Box } from '../../../../common/text_utils'; import { Fill, Line, Rect, Stroke } from '../../../../geoms/types'; import { Point } from '../../../../utils/point'; +import { LIGHT_THEME } from '../../../../utils/themes/light_theme'; +import { HeatmapStyle } from '../../../../utils/themes/theme'; import { PrimitiveValue } from '../../../partition_chart/layout/utils/group_by_rollup'; -import { config } from '../config/config'; +import { HeatmapBrushEvent } from '../../specs/heatmap'; import { HeatmapCellDatum } from '../viewmodel/viewmodel'; -import { Config, HeatmapBrushEvent } from './config_types'; /** @internal */ export interface Value { @@ -93,7 +94,7 @@ export type DragShape = ReturnType; /** @internal */ export type ShapeViewModel = { - config: Config; + theme: HeatmapStyle; heatmapViewModel: HeatmapViewModel; pickQuads: PickFunction; pickDragArea: PickDragFunction; @@ -119,8 +120,8 @@ export const nullHeatmapViewModel: HeatmapViewModel = { }; /** @internal */ -export const nullShapeViewModel = (specifiedConfig?: Config): ShapeViewModel => ({ - config: specifiedConfig || config, +export const nullShapeViewModel = (): ShapeViewModel => ({ + theme: LIGHT_THEME.heatmap, heatmapViewModel: nullHeatmapViewModel, pickQuads: () => [], pickDragArea: () => ({ cells: [], x: [], y: [], chartType: ChartType.Heatmap }), 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 bbeb135675d..72714cbe402 100644 --- a/packages/charts/src/chart_types/heatmap/layout/viewmodel/viewmodel.ts +++ b/packages/charts/src/chart_types/heatmap/layout/viewmodel/viewmodel.ts @@ -11,21 +11,20 @@ import { scaleBand, scaleQuantize } from 'd3-scale'; import { stringToRGB } from '../../../../common/color_library_wrappers'; import { Pixels } from '../../../../common/geometry'; -import { Box, TextMeasure } from '../../../../common/text_utils'; +import { Box } from '../../../../common/text_utils'; import { ScaleContinuous } from '../../../../scales'; import { ScaleType } from '../../../../scales/constants'; -import { SettingsSpec } from '../../../../specs'; import { withTextMeasure } from '../../../../utils/bbox/canvas_text_bbox_calculator'; import { snapDateToESInterval } from '../../../../utils/chrono/elasticsearch'; import { clamp, range } from '../../../../utils/common'; import { Dimensions } from '../../../../utils/dimensions'; import { ContinuousDomain } from '../../../../utils/domain'; +import { HeatmapStyle, Theme } 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 { ColorScale } from '../../state/selectors/get_color_scale'; import { GridHeightParams } from '../../state/selectors/get_grid_full_height'; -import { Config } from '../types/config_types'; import { Cell, PickDragFunction, @@ -54,9 +53,13 @@ function getValuesInRange( } /** - * Resolves the maximum number of ticks based on the chart width and sample label based on formatter config. + * Resolves the maximum number of ticks based on the chart width and sample label based on formatter heatmapTheme. */ -function getTicks(chartWidth: number, { formatter, padding, fontSize, fontFamily }: Config['xAxisLabel']): number { +function getTicks( + chartWidth: number, + formatter: HeatmapSpec['xAxisLabelFormatter'], + { padding, fontSize, fontFamily }: HeatmapStyle['xAxisLabel'], +): number { return withTextMeasure((textMeasure) => { const labelSample = formatter(Date.now()); const { width } = textMeasure(labelSample, padding, fontSize, fontFamily); @@ -70,26 +73,24 @@ function getTicks(chartWidth: number, { formatter, padding, fontSize, fontFamily /** @internal */ export function shapeViewModel( - textMeasure: TextMeasure, spec: HeatmapSpec, - config: Config, - settingsSpec: SettingsSpec, + { heatmap: heatmapTheme }: Theme, chartDimensions: Dimensions, heatmapTable: HeatmapTable, colorScale: ColorScale, bandsToHide: Array<[number, number]>, { height, pageSize }: GridHeightParams, ): ShapeViewModel { - const gridStrokeWidth = config.grid.stroke.width ?? 1; + const gridStrokeWidth = heatmapTheme.grid.stroke.width ?? 1; const { table, yValues, xDomain } = heatmapTable; // measure the text width of all rows values to get the grid area width const boxedYValues = yValues.map }>((value) => { return { - text: config.yAxisLabel.formatter(value), + text: spec.yAxisLabelFormatter(value), value, - ...config.yAxisLabel, + ...heatmapTheme.yAxisLabel, }; }); @@ -108,8 +109,8 @@ export function shapeViewModel( nice: false, }, { - desiredTickCount: getTicks(chartDimensions.width, config.xAxisLabel), - timeZone: config.timeZone, + desiredTickCount: getTicks(chartDimensions.width, spec.xAxisLabelFormatter, heatmapTheme.xAxisLabel), + timeZone: spec.timeZone, }, ) : null; @@ -132,8 +133,8 @@ export function shapeViewModel( // compute the cell width (can be smaller then the available size depending on config const cellWidth = - config.cell.maxWidth !== 'fill' && xScale.bandwidth() > config.cell.maxWidth - ? config.cell.maxWidth + 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) @@ -146,25 +147,25 @@ export function shapeViewModel( scaleCallback: (x: any) => number | undefined | null = xScale, ) => (value: any): TextBox => { return { - text: formatter(value, { timeZone: config.timeZone }), + text: formatter(value, { timeZone: spec.timeZone }), value, - ...config.xAxisLabel, + ...heatmapTheme.xAxisLabel, x: chartDimensions.left + (scaleCallback(value) || 0), - y: cellHeight * pageSize + config.xAxisLabel.fontSize / 2 + config.xAxisLabel.padding, + y: cellHeight * pageSize + heatmapTheme.xAxisLabel.fontSize / 2 + heatmapTheme.xAxisLabel.padding, }; }; // compute the position of each column label const textXValues: Array = timeScale - ? timeScale.ticks().map(getTextValue(config.xAxisLabel.formatter, (x: any) => timeScale.scale(x))) + ? timeScale.ticks().map(getTextValue(spec.xAxisLabelFormatter, (x: any) => timeScale.scale(x))) : xValues.map((textBox: any) => { return { - ...getTextValue(config.xAxisLabel.formatter)(textBox), + ...getTextValue(spec.xAxisLabelFormatter)(textBox), x: chartDimensions.left + (xScale(textBox) || 0) + xScale.bandwidth() / 2, }; }); - const { padding } = config.yAxisLabel; + const { padding } = heatmapTheme.yAxisLabel; const rightPadding = typeof padding === 'number' ? padding : padding.right ?? 0; // compute the position of each row label @@ -189,7 +190,8 @@ export function shapeViewModel( const cellKey = getCellKey(d.x, d.y); acc[cellKey] = { x: - (config.cell.maxWidth !== 'fill' ? x + xScale.bandwidth() / 2 - config.cell.maxWidth / 2 : x) + gridStrokeWidth, + (heatmapTheme.cell.maxWidth !== 'fill' ? x + xScale.bandwidth() / 2 - heatmapTheme.cell.maxWidth / 2 : x) + + gridStrokeWidth, y, yIndex, width: cellWidth - gridStrokeWidth * 2, @@ -199,8 +201,8 @@ export function shapeViewModel( color: stringToRGB(color), }, stroke: { - color: stringToRGB(config.cell.border.stroke), - width: config.cell.border.strokeWidth, + color: stringToRGB(heatmapTheme.cell.border.stroke), + width: heatmapTheme.cell.border.strokeWidth, }, value: d.value, visible: !isValueHidden(d.value, bandsToHide), @@ -356,7 +358,7 @@ export function shapeViewModel( } return { - config, + theme: heatmapTheme, heatmapViewModel: { gridOrigin: { x: chartDimensions.left, @@ -366,7 +368,7 @@ export function shapeViewModel( x: xLines, y: yLines, stroke: { - color: stringToRGB(config.grid.stroke.color), + color: stringToRGB(heatmapTheme.grid.stroke.color), width: gridStrokeWidth, }, }, 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 9f5b7e392ca..d66791418ba 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,7 +6,6 @@ * Side Public License, v 1. */ -import { Font } from '../../../../common/text_utils'; import { clearCanvas, renderLayers, withContext } from '../../../../renderers/canvas'; import { renderMultiLine } from '../../../xy_chart/renderer/canvas/primitives/line'; import { renderRect } from '../../../xy_chart/renderer/canvas/primitives/rect'; @@ -17,10 +16,8 @@ import { ShapeViewModel } from '../../layout/types/viewmodel_types'; export function renderCanvas2d( ctx: CanvasRenderingContext2D, dpr: number, - { config, heatmapViewModel }: ShapeViewModel, + { theme, heatmapViewModel }: ShapeViewModel, ) { - // eslint-disable-next-line no-empty-pattern - const {} = config; withContext(ctx, () => { // set some defaults for the overall rendering @@ -48,16 +45,16 @@ export function renderCanvas2d( clearCanvas, () => { + // Grid withContext(ctx, () => { - // render grid renderMultiLine(ctx, heatmapViewModel.gridLines.x, heatmapViewModel.gridLines.stroke); renderMultiLine(ctx, heatmapViewModel.gridLines.y, heatmapViewModel.gridLines.stroke); }); }, () => + // Cells withContext(ctx, () => { - // render cells const { x, y } = heatmapViewModel.gridOrigin; ctx.translate(x, y); filteredCells.forEach((cell) => { @@ -66,9 +63,9 @@ export function renderCanvas2d( }), () => - config.cell.label.visible && + // Text on cells + theme.cell.label.visible && withContext(ctx, () => { - // render text on cells const { x, y } = heatmapViewModel.gridOrigin; ctx.translate(x, y); filteredCells.forEach((cell) => { @@ -77,32 +74,24 @@ export function renderCanvas2d( ctx, { x: cell.x + cell.width / 2, y: cell.y + cell.height / 2 }, cell.formatted, - config.cell.label, + theme.cell.label, ); }); }), () => - // render text on Y axis - config.yAxisLabel.visible && + // Y Axis + theme.yAxisLabel.visible && withContext(ctx, () => filteredYValues.forEach((yValue) => { - const font: Font = { - fontFamily: config.yAxisLabel.fontFamily, - fontStyle: config.yAxisLabel.fontStyle ? config.yAxisLabel.fontStyle : 'normal', - fontVariant: 'normal', - fontWeight: 'normal', - textColor: 'black', - textOpacity: 1, - }; - const { padding } = config.yAxisLabel; + const { padding, ...font } = theme.yAxisLabel; const horizontalPadding = typeof padding === 'number' ? padding * 2 : (padding.left ?? 0) + (padding.right ?? 0); const [resultText] = wrapLines( ctx, yValue.text, font, - config.yAxisLabel.fontSize, + theme.yAxisLabel.fontSize, heatmapViewModel.gridOrigin.x - horizontalPadding, 16, { shouldAddEllipsis: true, wrapAtWord: false }, @@ -112,17 +101,17 @@ export function renderCanvas2d( { x: yValue.x, y: yValue.y }, resultText, // the alignment for y axis labels is fixed to the right - { ...config.yAxisLabel, align: 'right' }, + { ...theme.yAxisLabel, align: 'right' }, ); }), ), () => - // render text on X axis - config.xAxisLabel.visible && + // Text on X axis + theme.xAxisLabel.visible && withContext(ctx, () => heatmapViewModel.xValues.forEach((xValue) => - renderText(ctx, { x: xValue.x, y: xValue.y }, xValue.text, config.xAxisLabel), + renderText(ctx, { x: xValue.x, y: xValue.y }, xValue.text, theme.xAxisLabel), ), ), ]); 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 baa570a22da..9d04de1baec 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,7 +21,7 @@ import { import { getInternalIsInitializedSelector, InitStatus } from '../../../../state/selectors/get_internal_is_intialized'; import { Dimensions } from '../../../../utils/dimensions'; import { nullShapeViewModel, ShapeViewModel } from '../../layout/types/viewmodel_types'; -import { geometries } from '../../state/selectors/geometries'; +import { getHeatmapGeometries } from '../../state/selectors/geometries'; import { getHeatmapContainerSizeSelector } from '../../state/selectors/get_heatmap_container_size'; import { renderCanvas2d } from './canvas_renderers'; @@ -85,10 +85,9 @@ class Component extends React.Component { private drawCanvas() { if (this.ctx) { - const { width, height }: Dimensions = this.props.chartContainerDimensions; renderCanvas2d(this.ctx, this.devicePixelRatio, { ...this.props.geometries, - config: { ...this.props.geometries.config, width, height }, + theme: this.props.geometries.theme, }); } } @@ -151,7 +150,7 @@ const mapStateToProps = (state: GlobalChartState): ReactiveChartStateProps => { } return { initialized: true, - geometries: geometries(state), + geometries: getHeatmapGeometries(state), chartContainerDimensions: getHeatmapContainerSizeSelector(state), a11ySettings: getA11ySettingsSelector(state), }; diff --git a/packages/charts/src/chart_types/heatmap/renderer/dom/highlighter.tsx b/packages/charts/src/chart_types/heatmap/renderer/dom/highlighter.tsx index f2971fdd84f..0f4b4f88d05 100644 --- a/packages/charts/src/chart_types/heatmap/renderer/dom/highlighter.tsx +++ b/packages/charts/src/chart_types/heatmap/renderer/dom/highlighter.tsx @@ -9,8 +9,8 @@ import React, { FC } from 'react'; import { Dimensions } from '../../../../utils/dimensions'; -import { config } from '../../layout/config/config'; -import { Config } from '../../layout/types/config_types'; +import { LIGHT_THEME } from '../../../../utils/themes/light_theme'; +import { HeatmapStyle } from '../../../../utils/themes/theme'; import { DragShape, nullShapeViewModel, ShapeViewModel } from '../../layout/types/viewmodel_types'; /** @internal */ @@ -20,8 +20,8 @@ export interface HighlighterCellsProps { canvasDimension: Dimensions; geometries: ShapeViewModel; dragShape: DragShape | null; - brushMask: Config['brushMask']; - brushArea: Config['brushArea']; + brushMask: HeatmapStyle['brushMask']; + brushArea: HeatmapStyle['brushArea']; } /** @@ -145,6 +145,6 @@ export const DEFAULT_PROPS: HighlighterCellsProps = { }, geometries: nullShapeViewModel(), dragShape: { x: 0, y: 0, height: 0, width: 0 }, - brushArea: config.brushArea, - brushMask: config.brushMask, + brushArea: LIGHT_THEME.heatmap.brushArea, + brushMask: LIGHT_THEME.heatmap.brushMask, }; 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 4c5d5b5bd43..f9043b28eb4 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 @@ -9,11 +9,11 @@ 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 { geometries } from '../../state/selectors/geometries'; +import { getHeatmapGeometries } from '../../state/selectors/geometries'; import { getBrushedHighlightedShapesSelector } from '../../state/selectors/get_brushed_highlighted_shapes'; -import { getHeatmapConfigSelector } from '../../state/selectors/get_heatmap_config'; import { getHighlightedAreaSelector } from '../../state/selectors/get_highlighted_area'; import { DEFAULT_PROPS, HighlighterCellsComponent, HighlighterCellsProps } from './highlighter'; @@ -24,7 +24,7 @@ const brushMapStateToProps = (state: GlobalChartState): HighlighterCellsProps => const { chartId } = state; - const geoms = geometries(state); + const geoms = getHeatmapGeometries(state); const canvasDimension = computeChartDimensionsSelector(state); let dragShape = getBrushedHighlightedShapesSelector(state); @@ -32,7 +32,7 @@ const brushMapStateToProps = (state: GlobalChartState): HighlighterCellsProps => if (highlightedArea) { dragShape = highlightedArea; } - const { brushMask, brushArea } = getHeatmapConfigSelector(state); + const { brushMask, brushArea } = getChartThemeSelector(state).heatmap; return { chartId, diff --git a/packages/charts/src/chart_types/heatmap/specs/heatmap.ts b/packages/charts/src/chart_types/heatmap/specs/heatmap.ts index f1c10e0ebc2..609c6a5afd4 100644 --- a/packages/charts/src/chart_types/heatmap/specs/heatmap.ts +++ b/packages/charts/src/chart_types/heatmap/specs/heatmap.ts @@ -15,9 +15,8 @@ import { SeriesScales, Spec } from '../../../specs'; import { SpecType } from '../../../specs/constants'; import { getConnect, specComponentFactory } from '../../../state/spec_factory'; import { Accessor, AccessorFn } from '../../../utils/accessor'; -import { Color, Datum, RecursivePartial } from '../../../utils/common'; -import { config } from '../layout/config/config'; -import { Config } from '../layout/types/config_types'; +import { Color, Datum } from '../../../utils/common'; +import { Cell } from '../layout/types/viewmodel_types'; import { X_SCALE_DEFAULT } from './scale_defaults'; const defaultProps = { @@ -31,7 +30,11 @@ const defaultProps = { valueFormatter: (value: number) => `${value}`, xSortPredicate: Predicate.AlphaAsc, ySortPredicate: Predicate.AlphaAsc, - config, + timeZone: 'UTC', + xAxisLabelName: 'X Value', + xAxisLabelFormatter: String, + yAxisLabelName: 'Y Value', + yAxisLabelFormatter: String, }; /** @public */ @@ -57,6 +60,13 @@ export interface HeatmapBandsColorScale { labelFormatter?: (start: number, end: number) => string; } +/** @public */ +export type HeatmapBrushEvent = { + cells: Cell[]; + x: (string | number)[]; + y: (string | number)[]; +}; + /** @alpha */ export interface HeatmapSpec extends Spec { specType: typeof SpecType.Series; @@ -70,15 +80,22 @@ export interface HeatmapSpec extends Spec { xSortPredicate: Predicate; ySortPredicate: Predicate; xScaleType: SeriesScales['xScaleType']; - config: RecursivePartial; highlightedData?: { x: Array; y: Array }; name?: string; + + timeZone: string; + onBrushEnd?: (brushArea: HeatmapBrushEvent) => void; + xAxisLabelName: string; + xAxisLabelFormatter: (value: string | number) => string; + yAxisLabelName: string; + yAxisLabelFormatter: (value: string | number) => string; } +type SpecRequiredProps = Pick; +type SpecOptionalProps = Partial>; + /** @alpha */ -export const Heatmap: React.FunctionComponent< - Pick & Partial> -> = getConnect()( +export const Heatmap: React.FunctionComponent = getConnect()( specComponentFactory< HeatmapSpec, | 'xAccessor' @@ -88,7 +105,11 @@ export const Heatmap: React.FunctionComponent< | 'ySortPredicate' | 'xSortPredicate' | 'valueFormatter' - | 'config' | 'xScaleType' + | 'timeZone' + | 'xAxisLabelName' + | 'xAxisLabelFormatter' + | 'yAxisLabelName' + | 'yAxisLabelFormatter' >(defaultProps), ); 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 c365c2c5c31..d8cfd481b0f 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 @@ -11,6 +11,7 @@ import { max as d3Max } from 'd3-array'; import { Box, measureText } 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'; @@ -18,7 +19,6 @@ import { Dimensions } from '../../../../utils/dimensions'; import { XDomain } from '../../../xy_chart/domains/types'; import { HeatmapCellDatum } from '../../layout/viewmodel/viewmodel'; import { getGridHeightParamsSelector } from './get_grid_full_height'; -import { getHeatmapConfigSelector } from './get_heatmap_config'; import { getHeatmapTableSelector } from './get_heatmap_table'; import { getXAxisRightOverflow } from './get_x_axis_right_overflow'; @@ -43,7 +43,7 @@ export const computeChartDimensionsSelector = createCustomCachedSelector( getParentDimension, getLegendSizeSelector, getHeatmapTableSelector, - getHeatmapConfigSelector, + getChartThemeSelector, getXAxisRightOverflow, getGridHeightParamsSelector, getSettingsSpecSelector, @@ -52,14 +52,14 @@ export const computeChartDimensionsSelector = createCustomCachedSelector( chartContainerDimensions, legendSize, heatmapTable, - config, + { heatmap }, rightOverflow, { height }, { showLegend, legendPosition }, ): Dimensions => { let { width, left } = chartContainerDimensions; const { top } = chartContainerDimensions; - const { padding } = config.yAxisLabel; + const { padding } = heatmap.yAxisLabel; const textMeasurer = document.createElement('canvas'); const textMeasurerCtx = textMeasurer.getContext('2d'); @@ -68,22 +68,22 @@ export const computeChartDimensionsSelector = createCustomCachedSelector( const totalHorizontalPadding = typeof padding === 'number' ? padding * 2 : (padding.left ?? 0) + (padding.right ?? 0); - if (config.yAxisLabel.visible) { + 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, - ...config.yAxisLabel, + ...heatmap.yAxisLabel, }; }); - const measuredYValues = textMeasure(config.yAxisLabel.fontSize, boxedYValues); + const measuredYValues = textMeasure(heatmap.yAxisLabel.fontSize, boxedYValues); let yColumnWidth: number = d3Max(measuredYValues, ({ width }) => width) ?? 0; - if (typeof config.yAxisLabel.width === 'number') { - yColumnWidth = config.yAxisLabel.width; - } else if (typeof config.yAxisLabel.width === 'object' && yColumnWidth > config.yAxisLabel.width.max) { - yColumnWidth = config.yAxisLabel.width.max; + 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; 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 0bb15ff2b3f..ab1e31bc71a 100644 --- a/packages/charts/src/chart_types/heatmap/state/selectors/geometries.ts +++ b/packages/charts/src/chart_types/heatmap/state/selectors/geometries.ts @@ -8,7 +8,7 @@ import { GlobalChartState } from '../../../../state/chart_state'; import { createCustomCachedSelector } from '../../../../state/create_selector'; -import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; import { nullShapeViewModel, ShapeViewModel } from '../../layout/types/viewmodel_types'; import { computeChartDimensionsSelector } from './compute_chart_dimensions'; import { getColorScale } from './get_color_scale'; @@ -20,24 +20,24 @@ import { render } from './scenegraph'; const getDeselectedSeriesSelector = (state: GlobalChartState) => state.interactions.deselectedDataSeries; /** @internal */ -export const geometries = createCustomCachedSelector( +export const getHeatmapGeometries = createCustomCachedSelector( [ getHeatmapSpecSelector, computeChartDimensionsSelector, - getSettingsSpecSelector, getHeatmapTableSelector, getColorScale, getDeselectedSeriesSelector, getGridHeightParamsSelector, + getChartThemeSelector, ], ( heatmapSpec, chartDimensions, - settingSpec, heatmapTable, { bands, scale: colorScale }, deselectedSeries, gridHeightParams, + theme, ): ShapeViewModel => { // instead of using the specId, each legend item is associated with an unique band label const disabledBandLabels = new Set( @@ -53,7 +53,7 @@ export const geometries = createCustomCachedSelector( .map(({ start, end }) => [start, end]); return heatmapSpec - ? render(heatmapSpec, settingSpec, chartDimensions, heatmapTable, colorScale, bandsToHide, gridHeightParams) + ? render(heatmapSpec, chartDimensions, heatmapTable, colorScale, bandsToHide, gridHeightParams, theme) : nullShapeViewModel(); }, ); diff --git a/packages/charts/src/chart_types/heatmap/state/selectors/get_brushed_highlighted_shapes.test.ts b/packages/charts/src/chart_types/heatmap/state/selectors/get_brushed_highlighted_shapes.test.ts index f0c9dcce8d1..387430b7a6f 100644 --- a/packages/charts/src/chart_types/heatmap/state/selectors/get_brushed_highlighted_shapes.test.ts +++ b/packages/charts/src/chart_types/heatmap/state/selectors/get_brushed_highlighted_shapes.test.ts @@ -25,7 +25,26 @@ describe('Categorical heatmap brush', () => { onBrushEndMock = jest.fn(); MockStore.addSpecs( [ - MockGlobalSpec.settingsNoMargins(), + MockGlobalSpec.settingsNoMargins({ + theme: { + heatmap: { + grid: { + cellHeight: { + max: 'fill', + }, + cellWidth: { + max: 'fill', + }, + }, + xAxisLabel: { + visible: false, + }, + yAxisLabel: { + visible: false, + }, + }, + }, + }), MockSeriesSpec.heatmap({ xScaleType: ScaleType.Ordinal, data: [ @@ -39,24 +58,7 @@ describe('Categorical heatmap brush', () => { { x: 'b', y: 'yc', value: 8 }, { x: 'c', y: 'yc', value: 9 }, ], - config: { - grid: { - cellHeight: { - max: 'fill', - }, - cellWidth: { - max: 'fill', - }, - }, - xAxisLabel: { - visible: false, - }, - yAxisLabel: { - visible: false, - }, - margin: { top: 0, bottom: 0, left: 0, right: 0 }, - onBrushEnd: onBrushEndMock, - }, + onBrushEnd: onBrushEndMock, }), ], store, @@ -86,7 +88,26 @@ describe('Temporal heatmap brush', () => { onBrushEndMock = jest.fn(); MockStore.addSpecs( [ - MockGlobalSpec.settingsNoMargins(), + MockGlobalSpec.settingsNoMargins({ + theme: { + heatmap: { + grid: { + cellHeight: { + max: 'fill', + }, + cellWidth: { + max: 'fill', + }, + }, + xAxisLabel: { + visible: false, + }, + yAxisLabel: { + visible: false, + }, + }, + }, + }), MockSeriesSpec.heatmap({ xScaleType: ScaleType.Time, data: [ @@ -100,24 +121,7 @@ describe('Temporal heatmap brush', () => { { x: start.plus({ days: 1 }).toMillis(), y: 'yc', value: 8 }, { x: start.plus({ days: 2 }).toMillis(), y: 'yc', value: 9 }, ], - config: { - grid: { - cellHeight: { - max: 'fill', - }, - cellWidth: { - max: 'fill', - }, - }, - xAxisLabel: { - visible: false, - }, - yAxisLabel: { - visible: false, - }, - margin: { top: 0, bottom: 0, left: 0, right: 0 }, - onBrushEnd: onBrushEndMock, - }, + onBrushEnd: onBrushEndMock, }), ], store, diff --git a/packages/charts/src/chart_types/heatmap/state/selectors/get_brushed_highlighted_shapes.ts b/packages/charts/src/chart_types/heatmap/state/selectors/get_brushed_highlighted_shapes.ts index f814ab03d12..8b58e01aa4a 100644 --- a/packages/charts/src/chart_types/heatmap/state/selectors/get_brushed_highlighted_shapes.ts +++ b/packages/charts/src/chart_types/heatmap/state/selectors/get_brushed_highlighted_shapes.ts @@ -9,7 +9,7 @@ import { GlobalChartState } from '../../../../state/chart_state'; import { createCustomCachedSelector } from '../../../../state/create_selector'; import { DragShape } from '../../layout/types/viewmodel_types'; -import { geometries } from './geometries'; +import { getHeatmapGeometries } from './geometries'; function getCurrentPointerStates(state: GlobalChartState) { return state.interactions.pointer; @@ -17,7 +17,7 @@ function getCurrentPointerStates(state: GlobalChartState) { /** @internal */ export const getBrushedHighlightedShapesSelector = createCustomCachedSelector( - [geometries, getCurrentPointerStates], + [getHeatmapGeometries, getCurrentPointerStates], (geoms, pointerStates): DragShape => { if (!pointerStates.dragging || !pointerStates.down) { return null; diff --git a/packages/charts/src/chart_types/heatmap/state/selectors/get_debug_state.ts b/packages/charts/src/chart_types/heatmap/state/selectors/get_debug_state.ts index c9a76931345..626710cb80d 100644 --- a/packages/charts/src/chart_types/heatmap/state/selectors/get_debug_state.ts +++ b/packages/charts/src/chart_types/heatmap/state/selectors/get_debug_state.ts @@ -12,17 +12,16 @@ import { createCustomCachedSelector } from '../../../../state/create_selector'; import { DebugState, DebugStateLegend } from '../../../../state/types'; import { Position } from '../../../../utils/common'; import { computeLegendSelector } from './compute_legend'; -import { geometries } from './geometries'; +import { getHeatmapGeometries } from './geometries'; import { getHighlightedAreaSelector, getHighlightedDataSelector } from './get_highlighted_area'; -import { getPickedCells } from './get_picked_cells'; /** * Returns a stringified version of the `debugState` * @internal */ export const getDebugStateSelector = createCustomCachedSelector( - [geometries, computeLegendSelector, getHighlightedAreaSelector, getPickedCells, getHighlightedDataSelector], - (geoms, legend, pickedArea, pickedCells, highlightedData): DebugState => { + [getHeatmapGeometries, computeLegendSelector, getHighlightedAreaSelector, getHighlightedDataSelector], + (geoms, legend, pickedArea, highlightedData): DebugState => { return { // Common debug state legend: getLegendState(legend), 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 index 7e629ed6fc6..a5e7fd947ce 100644 --- 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 @@ -8,11 +8,11 @@ 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 { Config } from '../../layout/types/config_types'; -import { getHeatmapConfigSelector } from './get_heatmap_config'; +import { HeatmapStyle } from '../../../../utils/themes/theme'; import { getHeatmapTableSelector } from './get_heatmap_table'; /** @internal */ @@ -25,18 +25,18 @@ const getParentDimension = (state: GlobalChartState) => state.parentDimensions; /** @internal */ export const getGridHeightParamsSelector = createCustomCachedSelector( - [ - getLegendSizeSelector, - getSettingsSpecSelector, - getParentDimension, - getHeatmapConfigSelector, - getHeatmapTableSelector, - ], + [getLegendSizeSelector, getSettingsSpecSelector, getParentDimension, getChartThemeSelector, getHeatmapTableSelector], ( legendSize, { showLegend }, { height: containerHeight }, - { xAxisLabel: { padding, visible, fontSize }, grid, maxLegendHeight }, + { + heatmap: { + xAxisLabel: { padding, visible, fontSize }, + grid, + maxLegendHeight, + }, + }, { yValues }, ): GridHeightParams => { const xAxisHeight = visible ? fontSize : 0; @@ -63,7 +63,7 @@ export const getGridHeightParamsSelector = createCustomCachedSelector( }, ); -function getGridCellHeight(yValues: Array, grid: Config['grid'], height: number): number { +function getGridCellHeight(yValues: Array, grid: HeatmapStyle['grid'], height: number): number { if (yValues.length === 0) { return height; } diff --git a/packages/charts/src/chart_types/heatmap/state/selectors/get_heatmap_config.ts b/packages/charts/src/chart_types/heatmap/state/selectors/get_heatmap_config.ts deleted file mode 100644 index e781b14bc88..00000000000 --- a/packages/charts/src/chart_types/heatmap/state/selectors/get_heatmap_config.ts +++ /dev/null @@ -1,21 +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 { createCustomCachedSelector } from '../../../../state/create_selector'; -import { mergePartial } from '../../../../utils/common'; -import { config as defaultConfig } from '../../layout/config/config'; -import { Config } from '../../layout/types/config_types'; -import { getHeatmapSpecSelector } from './get_heatmap_spec'; - -/** @internal */ -export const getHeatmapConfigSelector = createCustomCachedSelector( - [getHeatmapSpecSelector], - (spec): Config => { - return mergePartial(defaultConfig, spec.config, { mergeOptionalPartialValues: true }); - }, -); diff --git a/packages/charts/src/chart_types/heatmap/state/selectors/get_heatmap_container_size.ts b/packages/charts/src/chart_types/heatmap/state/selectors/get_heatmap_container_size.ts index dfb79372720..5a9d3af837f 100644 --- a/packages/charts/src/chart_types/heatmap/state/selectors/get_heatmap_container_size.ts +++ b/packages/charts/src/chart_types/heatmap/state/selectors/get_heatmap_container_size.ts @@ -8,11 +8,11 @@ import { GlobalChartState } from '../../../../state/chart_state'; import { createCustomCachedSelector } from '../../../../state/create_selector'; +import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; import { getLegendConfigSelector } from '../../../../state/selectors/get_legend_config_selector'; import { getLegendSizeSelector } from '../../../../state/selectors/get_legend_size'; import { LayoutDirection } from '../../../../utils/common'; import { Dimensions } from '../../../../utils/dimensions'; -import { getHeatmapConfigSelector } from './get_heatmap_config'; const getParentDimension = (state: GlobalChartState) => state.parentDimensions; @@ -21,8 +21,8 @@ const getParentDimension = (state: GlobalChartState) => state.parentDimensions; * @internal */ export const getHeatmapContainerSizeSelector = createCustomCachedSelector( - [getParentDimension, getLegendSizeSelector, getHeatmapConfigSelector, getLegendConfigSelector], - (parentDimensions, legendSize, { maxLegendHeight }, { showLegend, legendPosition }): Dimensions => { + [getParentDimension, getLegendSizeSelector, getChartThemeSelector, getLegendConfigSelector], + (parentDimensions, legendSize, { heatmap: { maxLegendHeight } }, { showLegend, legendPosition }): Dimensions => { if (!showLegend || legendPosition.floating) { return parentDimensions; } diff --git a/packages/charts/src/chart_types/heatmap/state/selectors/get_highlighted_area.ts b/packages/charts/src/chart_types/heatmap/state/selectors/get_highlighted_area.ts index dd168175643..db154964b47 100644 --- a/packages/charts/src/chart_types/heatmap/state/selectors/get_highlighted_area.ts +++ b/packages/charts/src/chart_types/heatmap/state/selectors/get_highlighted_area.ts @@ -7,7 +7,7 @@ */ import { createCustomCachedSelector } from '../../../../state/create_selector'; -import { geometries } from './geometries'; +import { getHeatmapGeometries } from './geometries'; import { getHeatmapSpecSelector } from './get_heatmap_spec'; import { isBrushingSelector } from './is_brushing'; @@ -29,7 +29,7 @@ export const getHighlightedDataSelector = createCustomCachedSelector( * @internal */ export const getHighlightedAreaSelector = createCustomCachedSelector( - [geometries, getHeatmapSpecSelector, isBrushingSelector], + [getHeatmapGeometries, getHeatmapSpecSelector, isBrushingSelector], (geoms, spec, isBrushing) => { if (!spec.highlightedData || isBrushing) { return null; 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 370ac8d772a..bc01c27bacd 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,11 +9,11 @@ import { createCustomCachedSelector } from '../../../../state/create_selector'; import { getLastDragSelector } from '../../../../state/selectors/get_last_drag'; import { PickDragFunction } from '../../layout/types/viewmodel_types'; -import { geometries } from './geometries'; +import { getHeatmapGeometries } from './geometries'; /** @internal */ export const getPickedCells = createCustomCachedSelector( - [geometries, getLastDragSelector], + [getHeatmapGeometries, getLastDragSelector], (geoms, dragState): ReturnType | null => { if (!dragState) { return null; 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 a06de807171..eb660c44e9c 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 @@ -9,8 +9,9 @@ import { ScaleContinuous } from '../../../../scales'; 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 { getHeatmapConfigSelector } from './get_heatmap_config'; +import { getHeatmapSpecSelector } from './get_heatmap_spec'; import { getHeatmapTableSelector } from './get_heatmap_table'; /** @@ -18,8 +19,16 @@ import { getHeatmapTableSelector } from './get_heatmap_table'; * Gets color scale based on specification and values range. */ export const getXAxisRightOverflow = createCustomCachedSelector( - [getHeatmapConfigSelector, getHeatmapTableSelector], - ({ xAxisLabel: { fontSize, fontFamily, padding, formatter, width }, timeZone }, { xDomain }): number => { + [getChartThemeSelector, getHeatmapSpecSelector, getHeatmapTableSelector], + ( + { + heatmap: { + xAxisLabel: { fontSize, fontFamily, padding, width }, + }, + }, + { timeZone, xAxisLabelFormatter }, + { xDomain }, + ): number => { if (xDomain.type !== ScaleType.Time) { return 0; } @@ -40,7 +49,7 @@ export const getXAxisRightOverflow = createCustomCachedSelector( return withTextMeasure( (textMeasure) => timeScale.ticks().reduce((acc, d) => { - return Math.max(acc, textMeasure(formatter(d), padding, fontSize, fontFamily, 1).width + padding); + return Math.max(acc, textMeasure(xAxisLabelFormatter(d), padding, fontSize, fontFamily, 1).width + padding); }, 0) / 2, ); }, diff --git a/packages/charts/src/chart_types/heatmap/state/selectors/is_brush_available.ts b/packages/charts/src/chart_types/heatmap/state/selectors/is_brush_available.ts index 77436686d79..57ce7c79c50 100644 --- a/packages/charts/src/chart_types/heatmap/state/selectors/is_brush_available.ts +++ b/packages/charts/src/chart_types/heatmap/state/selectors/is_brush_available.ts @@ -7,17 +7,21 @@ */ import { createCustomCachedSelector } from '../../../../state/create_selector'; -import { getHeatmapConfigSelector } from './get_heatmap_config'; +import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; +import { getHeatmapSpecSelector } from './get_heatmap_spec'; /** * The brush is available only if a onBrushEnd listener is configured * @internal */ -export const isBrushAvailableSelector = createCustomCachedSelector([getHeatmapConfigSelector], (config): boolean => { - return Boolean(config.onBrushEnd) && config.brushTool.visible; -}); +export const isBrushAvailableSelector = createCustomCachedSelector( + [getHeatmapSpecSelector, getChartThemeSelector], + ({ onBrushEnd }, { heatmap }): boolean => { + return Boolean(onBrushEnd) && heatmap.brushTool.visible; + }, +); /** @internal */ -export const isBrushEndProvided = createCustomCachedSelector([getHeatmapConfigSelector], (config): boolean => { - return Boolean(config.onBrushEnd); +export const isBrushEndProvided = createCustomCachedSelector([getHeatmapSpecSelector], ({ onBrushEnd }): boolean => { + return Boolean(onBrushEnd); }); diff --git a/packages/charts/src/chart_types/heatmap/state/selectors/on_brush_end_caller.ts b/packages/charts/src/chart_types/heatmap/state/selectors/on_brush_end_caller.ts index 597bc7ddd96..ef2cb88d070 100644 --- a/packages/charts/src/chart_types/heatmap/state/selectors/on_brush_end_caller.ts +++ b/packages/charts/src/chart_types/heatmap/state/selectors/on_brush_end_caller.ts @@ -13,7 +13,7 @@ import { GlobalChartState } from '../../../../state/chart_state'; import { createCustomCachedSelector } from '../../../../state/create_selector'; import { getLastDragSelector } from '../../../../state/selectors/get_last_drag'; import { DragCheckProps, hasDragged } from '../../../../utils/events'; -import { getHeatmapConfigSelector } from './get_heatmap_config'; +import { getHeatmapSpecSelector } from './get_heatmap_spec'; import { getPickedCells } from './get_picked_cells'; import { getSpecOrNull } from './heatmap_spec'; import { isBrushEndProvided } from './is_brush_available'; @@ -35,7 +35,7 @@ export function createOnBrushEndCaller(): (state: GlobalChartState) => void { return; } selector = createCustomCachedSelector( - [getLastDragSelector, getSpecOrNull, getHeatmapConfigSelector, getPickedCells], + [getLastDragSelector, getSpecOrNull, getHeatmapSpecSelector, getPickedCells], (lastDrag, spec, { onBrushEnd }, pickedCells): void => { const nextProps: DragCheckProps = { lastDrag, 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 3fdbb45aa6f..979c2889584 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,7 +9,7 @@ import { GlobalChartState } from '../../../../state/chart_state'; import { createCustomCachedSelector } from '../../../../state/create_selector'; import { Cell, TextBox } from '../../layout/types/viewmodel_types'; -import { geometries } from './geometries'; +import { getHeatmapGeometries } from './geometries'; function getCurrentPointerPosition(state: GlobalChartState) { return state.interactions.pointer.current.position; @@ -17,7 +17,7 @@ function getCurrentPointerPosition(state: GlobalChartState) { /** @internal */ export const getPickedShapes = createCustomCachedSelector( - [geometries, getCurrentPointerPosition], + [getHeatmapGeometries, getCurrentPointerPosition], (geoms, pointerPosition): Cell[] | TextBox => { const picker = geoms.pickQuads; const { x, y } = pointerPosition; diff --git a/packages/charts/src/chart_types/heatmap/state/selectors/scenegraph.ts b/packages/charts/src/chart_types/heatmap/state/selectors/scenegraph.ts index 55545febcac..a822e1d337d 100644 --- a/packages/charts/src/chart_types/heatmap/state/selectors/scenegraph.ts +++ b/packages/charts/src/chart_types/heatmap/state/selectors/scenegraph.ts @@ -6,12 +6,8 @@ * Side Public License, v 1. */ -import { measureText } from '../../../../common/text_utils'; -import { SettingsSpec } from '../../../../specs'; -import { RecursivePartial, mergePartial } from '../../../../utils/common'; import { Dimensions } from '../../../../utils/dimensions'; -import { config as defaultConfig } from '../../layout/config/config'; -import { Config } from '../../layout/types/config_types'; +import { Theme } from '../../../../utils/themes/theme'; import { ShapeViewModel, nullShapeViewModel } from '../../layout/types/viewmodel_types'; import { shapeViewModel } from '../../layout/viewmodel/viewmodel'; import { HeatmapSpec } from '../../specs'; @@ -22,31 +18,18 @@ import { GridHeightParams } from './get_grid_full_height'; /** @internal */ export function render( spec: HeatmapSpec, - settingsSpec: SettingsSpec, chartDimensions: Dimensions, heatmapTable: HeatmapTable, colorScale: ColorScale, bandsToHide: Array<[number, number]>, gridHeightParams: GridHeightParams, + theme: Theme, ): ShapeViewModel { const textMeasurer = document.createElement('canvas'); const textMeasurerCtx = textMeasurer.getContext('2d'); if (!textMeasurerCtx) { return nullShapeViewModel(); } - const { width, height } = chartDimensions; - const { config: specConfig } = spec; - const partialConfig: RecursivePartial = { ...specConfig, width, height }; - const config = mergePartial(defaultConfig, partialConfig, { mergeOptionalPartialValues: true }); - return shapeViewModel( - measureText(textMeasurerCtx), - spec, - config, - settingsSpec, - chartDimensions, - heatmapTable, - colorScale, - bandsToHide, - gridHeightParams, - ); + + return shapeViewModel(spec, theme, chartDimensions, heatmapTable, colorScale, bandsToHide, gridHeightParams); } diff --git a/packages/charts/src/chart_types/heatmap/state/selectors/tooltip.ts b/packages/charts/src/chart_types/heatmap/state/selectors/tooltip.ts index 15218ae5b5e..9a3867c5622 100644 --- a/packages/charts/src/chart_types/heatmap/state/selectors/tooltip.ts +++ b/packages/charts/src/chart_types/heatmap/state/selectors/tooltip.ts @@ -9,7 +9,6 @@ import { RGBtoString } from '../../../../common/color_library_wrappers'; import { TooltipInfo } from '../../../../components/tooltip/types'; import { createCustomCachedSelector } from '../../../../state/create_selector'; -import { getHeatmapConfigSelector } from './get_heatmap_config'; import { getSpecOrNull } from './heatmap_spec'; import { getPickedShapes } from './picked_shapes'; @@ -20,8 +19,8 @@ const EMPTY_TOOLTIP = Object.freeze({ /** @internal */ export const getTooltipInfoSelector = createCustomCachedSelector( - [getSpecOrNull, getHeatmapConfigSelector, getPickedShapes], - (spec, config, pickedShapes): TooltipInfo => { + [getSpecOrNull, getPickedShapes], + (spec, pickedShapes): TooltipInfo => { if (!spec) { return EMPTY_TOOLTIP; } @@ -37,7 +36,7 @@ export const getTooltipInfoSelector = createCustomCachedSelector( .forEach((shape) => { // X-axis value tooltipInfo.values.push({ - label: config.xAxisLabel.name, + label: spec.xAxisLabelName, color: 'transparent', isHighlighted: false, isVisible: true, @@ -46,13 +45,13 @@ export const getTooltipInfoSelector = createCustomCachedSelector( key: spec.id, }, value: `${shape.datum.x}`, - formattedValue: config.xAxisLabel.formatter(shape.datum.x), + formattedValue: spec.xAxisLabelFormatter(shape.datum.x), datum: shape.datum, }); // Y-axis value tooltipInfo.values.push({ - label: config.yAxisLabel.name, + label: spec.yAxisLabelName, color: 'transparent', isHighlighted: false, isVisible: true, @@ -61,7 +60,7 @@ export const getTooltipInfoSelector = createCustomCachedSelector( key: spec.id, }, value: `${shape.datum.y}`, - formattedValue: config.yAxisLabel.formatter(shape.datum.y), + formattedValue: spec.yAxisLabelFormatter(shape.datum.y), datum: shape.datum, }); diff --git a/packages/charts/src/index.ts b/packages/charts/src/index.ts index e8284900767..249edfcc96b 100644 --- a/packages/charts/src/index.ts +++ b/packages/charts/src/index.ts @@ -65,7 +65,7 @@ export * from './chart_types/partition_chart/layout/utils/group_by_rollup'; // heatmap export { Cell } from './chart_types/heatmap/layout/types/viewmodel_types'; -export { Config as HeatmapConfig, HeatmapBrushEvent } from './chart_types/heatmap/layout/types/config_types'; +export { HeatmapBrushEvent } from './chart_types/heatmap/specs'; export { ColorBand, HeatmapBandsColorScale } from './chart_types/heatmap/specs/heatmap'; // utilities diff --git a/packages/charts/src/mocks/specs/specs.ts b/packages/charts/src/mocks/specs/specs.ts index 5d7a7f80b6d..b39086eef91 100644 --- a/packages/charts/src/mocks/specs/specs.ts +++ b/packages/charts/src/mocks/specs/specs.ts @@ -190,7 +190,11 @@ export class MockSeriesSpec { valueFormatter: (value: number) => `${value}`, xSortPredicate: Predicate.AlphaAsc, ySortPredicate: Predicate.AlphaAsc, - config: {}, + timeZone: 'UTC', + xAxisLabelName: 'X Value', + xAxisLabelFormatter: String, + yAxisLabelName: 'Y Value', + yAxisLabelFormatter: String, }; static bar(partial?: Partial): BarSeriesSpec { diff --git a/packages/charts/src/utils/events.ts b/packages/charts/src/utils/events.ts index e246c97a726..5fb4f926469 100644 --- a/packages/charts/src/utils/events.ts +++ b/packages/charts/src/utils/events.ts @@ -20,7 +20,7 @@ export function isValidPointerOverEvent( /** @internal */ export interface DragCheckProps { - onBrushEnd: BrushEndListener | HeatmapSpec['config']['onBrushEnd'] | undefined; + onBrushEnd: BrushEndListener | HeatmapSpec['onBrushEnd'] | undefined; lastDrag: DragState | null; } diff --git a/packages/charts/src/utils/themes/dark_theme.ts b/packages/charts/src/utils/themes/dark_theme.ts index 3659ae33045..c3c62537429 100644 --- a/packages/charts/src/utils/themes/dark_theme.ts +++ b/packages/charts/src/utils/themes/dark_theme.ts @@ -230,4 +230,84 @@ export const DARK_THEME: Theme = { stroke: 'white', }, }, + heatmap: { + maxRowHeight: 30, + maxColumnWidth: 30, + brushArea: { + visible: true, + stroke: '#D3DAE6', // euiColorLightShade, + strokeWidth: 2, + }, + brushMask: { + visible: true, + fill: '#8c8c8c80', + }, + brushTool: { + visible: false, + fill: 'snow', + }, + xAxisLabel: { + visible: true, + width: 'auto', + fontSize: 12, + fontFamily: 'Sans-Serif', + fontStyle: 'normal', + textColor: 'white', + fontVariant: 'normal', + fontWeight: 'normal', + textOpacity: 1, + align: 'center', + baseline: 'middle', + padding: 6, + }, + yAxisLabel: { + visible: true, + width: 'auto', + fontSize: 12, + fontFamily: 'Sans-Serif', + fontStyle: 'normal', + textColor: 'white', + fontVariant: 'normal', + fontWeight: 'normal', + textOpacity: 1, + baseline: 'middle', + padding: 5, + }, + grid: { + cellWidth: { + min: 0, + max: 30, + }, + cellHeight: { + min: 12, + max: 30, + }, + stroke: { + width: 1, + color: 'snow', + }, + }, + cell: { + maxWidth: 'fill', + maxHeight: 'fill', + align: 'center', + label: { + visible: true, + maxWidth: 'fill', + fontSize: 10, + fontFamily: 'Sans-Serif', + fontStyle: 'normal', + textColor: 'white', + fontVariant: 'normal', + fontWeight: 'normal', + textOpacity: 1, + align: 'center', + baseline: 'middle', + }, + border: { + strokeWidth: 1, + stroke: 'snow', + }, + }, + }, }; diff --git a/packages/charts/src/utils/themes/light_theme.ts b/packages/charts/src/utils/themes/light_theme.ts index 6cb327139e3..81772450684 100644 --- a/packages/charts/src/utils/themes/light_theme.ts +++ b/packages/charts/src/utils/themes/light_theme.ts @@ -230,4 +230,84 @@ export const LIGHT_THEME: Theme = { stroke: 'black', }, }, + heatmap: { + maxRowHeight: 30, + maxColumnWidth: 30, + brushArea: { + visible: true, + stroke: '#69707D', // euiColorDarkShade, + strokeWidth: 2, + }, + brushMask: { + visible: true, + fill: '#73737380', + }, + brushTool: { + visible: false, + fill: 'gray', + }, + xAxisLabel: { + visible: true, + width: 'auto', + fontSize: 12, + fontFamily: 'Sans-Serif', + fontStyle: 'normal', + textColor: 'black', + fontVariant: 'normal', + fontWeight: 'normal', + textOpacity: 1, + align: 'center', + baseline: 'middle', + padding: 6, + }, + yAxisLabel: { + visible: true, + width: 'auto', + fontSize: 12, + fontFamily: 'Sans-Serif', + fontStyle: 'normal', + textColor: 'black', + fontVariant: 'normal', + fontWeight: 'normal', + textOpacity: 1, + baseline: 'middle', + padding: 5, + }, + grid: { + cellWidth: { + min: 0, + max: 30, + }, + cellHeight: { + min: 12, + max: 30, + }, + stroke: { + width: 1, + color: 'gray', + }, + }, + cell: { + maxWidth: 'fill', + maxHeight: 'fill', + align: 'center', + label: { + visible: true, + maxWidth: 'fill', + fontSize: 10, + fontFamily: 'Sans-Serif', + fontStyle: 'normal', + textColor: 'black', + fontVariant: 'normal', + fontWeight: 'normal', + textOpacity: 1, + align: 'center', + baseline: 'middle', + }, + border: { + strokeWidth: 1, + stroke: 'gray', + }, + }, + }, }; diff --git a/packages/charts/src/utils/themes/theme.ts b/packages/charts/src/utils/themes/theme.ts index f1224c08d4b..62356b25ea0 100644 --- a/packages/charts/src/utils/themes/theme.ts +++ b/packages/charts/src/utils/themes/theme.ts @@ -9,7 +9,7 @@ import { $Values } from 'utility-types'; import { Pixels, Ratio } from '../../common/geometry'; -import { FontStyle } from '../../common/text_utils'; +import { Font, FontStyle, TextAlign, TextBaseline } from '../../common/text_utils'; import { Color, ColorVariant, HorizontalAlignment, RecursivePartial, VerticalAlignment } from '../common'; import { Margins, SimplePadding } from '../dimensions'; import { Point } from '../point'; @@ -186,6 +186,76 @@ export interface GoalStyles { maxFontSize: number; } +/** + * @public + */ +export interface HeatmapStyle { + maxRowHeight: Pixels; + maxColumnWidth: Pixels; + /** + * Config of the mask over the area outside of the selected cells + */ + brushMask: { visible: boolean; fill: Color }; + /** + * Config of the mask over the selected cells + */ + brushArea: { visible: boolean; fill?: Color; stroke: Color; strokeWidth: number }; + /** + * Config of the brushing tool + */ + brushTool: { + visible: boolean; + // TODO add support for changing the brush tool color + fill: Color; + }; + xAxisLabel: Font & { + fontSize: Pixels; + width: Pixels | 'auto'; + align: TextAlign; + baseline: TextBaseline; + visible: boolean; + padding: number; + }; + yAxisLabel: Font & { + fontSize: Pixels; + width: Pixels | 'auto' | { max: Pixels }; + baseline: TextBaseline; + visible: boolean; + padding: number | { left?: number; right?: number; top?: number; bottom?: number }; + }; + grid: { + cellWidth: { + min: Pixels; + max: Pixels | 'fill'; + }; + cellHeight: { + min: Pixels; + max: Pixels | 'fill'; + }; + stroke: { + color: string; + width: number; + }; + }; + cell: { + maxWidth: Pixels | 'fill'; + maxHeight: Pixels | 'fill'; + align: 'center'; + label: Font & { + fontSize: Pixels; + maxWidth: Pixels | 'fill'; + align: TextAlign; + baseline: TextBaseline; + visible: boolean; + }; + border: { + strokeWidth: Pixels; + stroke: Color; + }; + }; + maxLegendHeight?: number; +} + /** @public */ export interface ScalesConfig { /** @@ -322,6 +392,7 @@ export interface Theme { */ background: BackgroundStyle; goal: GoalStyles; + heatmap: HeatmapStyle; } /** @public */ diff --git a/storybook/stories/heatmap/1_basic.story.tsx b/storybook/stories/heatmap/1_basic.story.tsx index ac5bd978dfb..d25950e9fe2 100644 --- a/storybook/stories/heatmap/1_basic.story.tsx +++ b/storybook/stories/heatmap/1_basic.story.tsx @@ -16,12 +16,12 @@ import { DebugState, Heatmap, HeatmapElementEvent, + HeatmapStyle, niceTimeFormatter, RecursivePartial, ScaleType, Settings, } from '@elastic/charts'; -import { Config } from '@elastic/charts/src/chart_types/heatmap/layout/types/config_types'; import { SWIM_LANE_DATA } from '@elastic/charts/src/utils/data_samples/test_anomaly_swim_lane'; import { useBaseTheme } from '../../use_base_theme'; @@ -39,8 +39,8 @@ export const Example = () => { button('Clear cells selection', handler); - const config: RecursivePartial = useMemo( - () => ({ + const heatmap = useMemo(() => { + const styles: RecursivePartial = { brushTool: { visible: true, }, @@ -69,17 +69,10 @@ export const Example = () => { width: 'auto', padding: { left: 10, right: 10 }, }, - xAxisLabel: { - formatter: (value: string | number) => { - return niceTimeFormatter([1572825600000, 1572912000000])(value, { timeZone: 'UTC' }); - }, - }, - onBrushEnd: ((e) => { - setSelection({ x: e.x, y: e.y }); - }) as Config['onBrushEnd'], - }), - [], - ); + }; + + return styles; + }, []); const logDebugState = debounce(() => { if (!debugState) return; @@ -112,6 +105,7 @@ export const Example = () => { brushAxis="both" xDomain={{ min: 1572825600000, max: 1572912000000, minInterval: 1800000 }} debugState={debugState} + theme={{ heatmap }} baseTheme={useBaseTheme()} /> { valueFormatter={(d) => `${Number(d.toFixed(2))}℃`} ySortPredicate="numAsc" xScaleType={ScaleType.Time} - config={config} + xAxisLabelFormatter={(value) => { + return niceTimeFormatter([1572825600000, 1572912000000])(value, { timeZone: 'UTC' }); + }} + onBrushEnd={(e) => { + setSelection({ x: e.x, y: e.y }); + }} highlightedData={persistCellsSelection ? selection : undefined} /> diff --git a/storybook/stories/heatmap/2_categorical.story.tsx b/storybook/stories/heatmap/2_categorical.story.tsx index ed93832e454..a319bb4b3c5 100644 --- a/storybook/stories/heatmap/2_categorical.story.tsx +++ b/storybook/stories/heatmap/2_categorical.story.tsx @@ -25,6 +25,29 @@ export const Example = () => { legendPosition="right" brushAxis="both" baseTheme={useBaseTheme()} + theme={{ + heatmap: { + grid: { + stroke: { + width: 0, + }, + }, + cell: { + maxWidth: 'fill', + maxHeight: 20, + label: { + visible: true, + }, + border: { + stroke: 'transparent', + strokeWidth: 1, + }, + }, + yAxisLabel: { + visible: true, + }, + }, + }} /> { valueAccessor={(d) => d[3]} valueFormatter={(value) => value.toFixed(0.2)} xSortPredicate="alphaAsc" - config={{ - grid: { - stroke: { - width: 0, - }, - }, - cell: { - maxWidth: 'fill', - maxHeight: 20, - label: { - visible: true, - }, - border: { - stroke: 'transparent', - strokeWidth: 1, - }, - }, - yAxisLabel: { - visible: true, - }, - onBrushEnd: action('onBrushEnd'), - }} + onBrushEnd={action('onBrushEnd')} /> ); diff --git a/storybook/stories/heatmap/3_time.story.tsx b/storybook/stories/heatmap/3_time.story.tsx index 5586a3dcdfa..da027f8d79c 100644 --- a/storybook/stories/heatmap/3_time.story.tsx +++ b/storybook/stories/heatmap/3_time.story.tsx @@ -10,9 +10,11 @@ import { number } from '@storybook/addon-knobs'; import { DateTime } from 'luxon'; import React, { useMemo } from 'react'; -import { Chart, Heatmap, RecursivePartial, ScaleType, Settings, HeatmapConfig } from '@elastic/charts'; +import { Chart, Heatmap, RecursivePartial, ScaleType, Settings, HeatmapStyle } from '@elastic/charts'; import { getRandomNumberGenerator } from '@elastic/charts/src/mocks/utils'; +import { useBaseTheme } from '../../use_base_theme'; + const rng = getRandomNumberGenerator(); const start = DateTime.fromISO('2021-03-27T20:00:00'); const end = DateTime.fromISO('2021-03-28T11:00:00'); @@ -27,8 +29,8 @@ const data = [...new Array(14)].flatMap((d, i) => { }); export const Example = () => { - const config: RecursivePartial = useMemo( - () => ({ + const heatmap = useMemo(() => { + const styles: RecursivePartial = { grid: { cellHeight: { min: 20, @@ -54,14 +56,10 @@ export const Example = () => { width: 'auto', padding: { left: 10, right: 10 }, }, - xAxisLabel: { - formatter: (value: string | number) => { - return DateTime.fromMillis(value as number).toFormat('HH:mm:ss', { timeZone: 'UTC' }); - }, - }, - }), - [], - ); + }; + + return styles; + }, []); const startTimeOffset = number('start time offset', 0, { min: -1000 * 60 * 60 * 24, @@ -90,6 +88,8 @@ export const Example = () => { minInterval: 1000 * 60 * 60, }} showLegend + theme={{ heatmap }} + baseTheme={useBaseTheme()} /> { valueFormatter={(d) => d.toFixed(2)} ySortPredicate="numAsc" xScaleType={ScaleType.Time} - config={config} + xAxisLabelFormatter={(value: string | number) => { + return DateTime.fromMillis(value as number).toFormat('HH:mm:ss', { timeZone: 'UTC' }); + }} /> diff --git a/storybook/stories/heatmap/4_theming.story.tsx b/storybook/stories/heatmap/4_theming.story.tsx new file mode 100644 index 00000000000..36a6cb06282 --- /dev/null +++ b/storybook/stories/heatmap/4_theming.story.tsx @@ -0,0 +1,134 @@ +/* + * 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 { action } from '@storybook/addon-actions'; +import { boolean, color, number } from '@storybook/addon-knobs'; +import React from 'react'; +import { debounce } from 'ts-debounce'; + +import { + Chart, + DebugState, + Heatmap, + HeatmapStyle, + niceTimeFormatter, + RecursivePartial, + ScaleType, + Settings, +} from '@elastic/charts'; +import { SWIM_LANE_DATA } from '@elastic/charts/src/utils/data_samples/test_anomaly_swim_lane'; + +import { useBaseTheme } from '../../use_base_theme'; + +export const Example = () => { + const debugState = boolean('Enable debug state', true); + const dataStateAction = action('DataState'); + + const heatmap: RecursivePartial = { + brushArea: { + visible: boolean('brushArea visible', true, 'Theme'), + fill: color('brushArea fill', 'black', 'Theme'), + stroke: color('brushArea stroke', '#69707D', 'Theme'), + strokeWidth: number('brushArea strokeWidth', 2, { range: true, min: 1, max: 10 }, 'Theme'), + }, + brushMask: { + visible: boolean('brushMask visible', true, 'Theme'), + fill: color('brushMask fill', 'rgb(115 115 115 / 50%)', 'Theme'), + }, + brushTool: { + visible: boolean('brushTool visible', false, 'Theme'), + fill: color('brushTool fill color', 'gray', 'Theme'), + }, + xAxisLabel: { + visible: boolean('xAxisLabel visible', true, 'Theme'), + fontSize: number('xAxisLabel fontSize', 12, { range: true, min: 5, max: 20 }, 'Theme'), + textColor: color('xAxisLabel textColor', 'black', 'Theme'), + padding: number('xAxisLabel padding', 6, { range: true, min: 0, max: 15 }, 'Theme'), + }, + yAxisLabel: { + visible: boolean('yAxisLabel visible', true, 'Theme'), + fontSize: number('yAxisLabel fontSize', 12, { range: true, min: 5, max: 20 }, 'Theme'), + textColor: color('yAxisLabel textColor', 'black', 'Theme'), + padding: number('yAxisLabel padding', 5, { range: true, min: 0, max: 15 }, 'Theme'), + }, + grid: { + stroke: { + color: color('grid stroke color', 'gray', 'Theme'), + }, + }, + cell: { + label: { + visible: boolean('cell label visible', false, 'Theme'), + fontSize: number('cell label fontSize', 10, { range: true, min: 5, max: 20 }, 'Theme'), + textColor: color('cell label textColor', 'black', 'Theme'), + }, + border: { + strokeWidth: number('border strokeWidth', 1, { range: true, min: 1, max: 5 }, 'Theme'), + stroke: color('border stroke color', 'gray', 'Theme'), + }, + }, + }; + + const logDebugState = debounce(() => { + if (!debugState) return; + + const statusEl = document.querySelector('.echChartStatus'); + + if (statusEl) { + const dataState = statusEl.dataset.echDebugState + ? (JSON.parse(statusEl.dataset.echDebugState) as DebugState) + : null; + dataStateAction(dataState); + } + }, 100); + + return ( + + + ({ ...v, time: v.time * 1000 }))} + xAccessor={(d) => d.time} + yAccessor={(d) => d.laneLabel} + valueAccessor={(d) => d.value} + valueFormatter={(d) => `${Number(d.toFixed(2))}℃`} + ySortPredicate="numAsc" + xScaleType={ScaleType.Time} + xAxisLabelFormatter={(value) => { + return niceTimeFormatter([1572825600000, 1572912000000])(value, { timeZone: 'UTC' }); + }} + /> + + ); +}; + +Example.parameters = { + markdown: ` +> __Warning:__ ⚠️ default \`Theme\` styles are overrided by knob controls. Toggling between themes may show incorrect styles. + `, +}; diff --git a/storybook/stories/heatmap/heatmap.stories.tsx b/storybook/stories/heatmap/heatmap.stories.tsx index 4d7781aab15..374dab5aae4 100644 --- a/storybook/stories/heatmap/heatmap.stories.tsx +++ b/storybook/stories/heatmap/heatmap.stories.tsx @@ -13,3 +13,4 @@ export default { export { Example as basic } from './1_basic.story'; export { Example as time } from './3_time.story'; export { Example as categorical } from './2_categorical.story'; +export { Example as theming } from './4_theming.story';