diff --git a/e2e/screenshots/all.test.ts-snapshots/baselines/interactions/click-hovers-on-legend-items-area-chart-chrome-linux.png b/e2e/screenshots/all.test.ts-snapshots/baselines/interactions/click-hovers-on-legend-items-area-chart-chrome-linux.png index 85ecdb179c3..ef89d24f7b8 100644 Binary files a/e2e/screenshots/all.test.ts-snapshots/baselines/interactions/click-hovers-on-legend-items-area-chart-chrome-linux.png and b/e2e/screenshots/all.test.ts-snapshots/baselines/interactions/click-hovers-on-legend-items-area-chart-chrome-linux.png differ diff --git a/e2e/screenshots/all.test.ts-snapshots/baselines/interactions/line-area-bar-point-clicks-and-hovers-chrome-linux.png b/e2e/screenshots/all.test.ts-snapshots/baselines/interactions/line-area-bar-point-clicks-and-hovers-chrome-linux.png index 4f4cc7e51fd..80781823594 100644 Binary files a/e2e/screenshots/all.test.ts-snapshots/baselines/interactions/line-area-bar-point-clicks-and-hovers-chrome-linux.png and b/e2e/screenshots/all.test.ts-snapshots/baselines/interactions/line-area-bar-point-clicks-and-hovers-chrome-linux.png differ diff --git a/e2e/screenshots/all.test.ts-snapshots/baselines/stylings/dimmed-highlight-style-chrome-linux.png b/e2e/screenshots/all.test.ts-snapshots/baselines/stylings/dimmed-highlight-style-chrome-linux.png new file mode 100644 index 00000000000..977715d340a Binary files /dev/null and b/e2e/screenshots/all.test.ts-snapshots/baselines/stylings/dimmed-highlight-style-chrome-linux.png differ diff --git a/e2e/screenshots/flame_stories.test.ts-snapshots/flame-stories/should-dim-icicle-chart-linear-renderer-on-legend-hover-fill-dimming-chrome-linux.png b/e2e/screenshots/flame_stories.test.ts-snapshots/flame-stories/should-dim-icicle-chart-linear-renderer-on-legend-hover-fill-dimming-chrome-linux.png new file mode 100644 index 00000000000..d02a1ccdc23 Binary files /dev/null and b/e2e/screenshots/flame_stories.test.ts-snapshots/flame-stories/should-dim-icicle-chart-linear-renderer-on-legend-hover-fill-dimming-chrome-linux.png differ diff --git a/e2e/screenshots/flame_stories.test.ts-snapshots/flame-stories/should-dim-icicle-chart-linear-renderer-on-legend-hover-opacity-dimming-chrome-linux.png b/e2e/screenshots/flame_stories.test.ts-snapshots/flame-stories/should-dim-icicle-chart-linear-renderer-on-legend-hover-opacity-dimming-chrome-linux.png new file mode 100644 index 00000000000..b218e789584 Binary files /dev/null and b/e2e/screenshots/flame_stories.test.ts-snapshots/flame-stories/should-dim-icicle-chart-linear-renderer-on-legend-hover-opacity-dimming-chrome-linux.png differ diff --git a/e2e/screenshots/legend_stories.test.ts-snapshots/legend-stories/should-all-values-in-stacked-chart-with-filtered-series-nick-chrome-linux.png b/e2e/screenshots/legend_stories.test.ts-snapshots/legend-stories/should-all-values-in-stacked-chart-with-filtered-series-nick-chrome-linux.png index 57659f094ed..3ecd57bd8f7 100644 Binary files a/e2e/screenshots/legend_stories.test.ts-snapshots/legend-stories/should-all-values-in-stacked-chart-with-filtered-series-nick-chrome-linux.png and b/e2e/screenshots/legend_stories.test.ts-snapshots/legend-stories/should-all-values-in-stacked-chart-with-filtered-series-nick-chrome-linux.png differ diff --git a/e2e/screenshots/legend_stories.test.ts-snapshots/legend-stories/should-render-legend-action-on-mouse-hover-chrome-linux.png b/e2e/screenshots/legend_stories.test.ts-snapshots/legend-stories/should-render-legend-action-on-mouse-hover-chrome-linux.png index 6d1d2eca2fc..f7f352c3cc5 100644 Binary files a/e2e/screenshots/legend_stories.test.ts-snapshots/legend-stories/should-render-legend-action-on-mouse-hover-chrome-linux.png and b/e2e/screenshots/legend_stories.test.ts-snapshots/legend-stories/should-render-legend-action-on-mouse-hover-chrome-linux.png differ diff --git a/e2e/screenshots/metric_stories.test.ts-snapshots/metric/should-render-metric-with-value-in-middle-position-chrome-linux.png b/e2e/screenshots/metric_stories.test.ts-snapshots/metric/should-render-metric-with-value-in-middle-position-chrome-linux.png index 649a8edfe79..91f8e213faa 100644 Binary files a/e2e/screenshots/metric_stories.test.ts-snapshots/metric/should-render-metric-with-value-in-middle-position-chrome-linux.png and b/e2e/screenshots/metric_stories.test.ts-snapshots/metric/should-render-metric-with-value-in-middle-position-chrome-linux.png differ diff --git a/e2e/screenshots/partition_stories.test.ts-snapshots/axis-stories/theme-dark/should-dim-slices-on-legend-hover-sunburst-dark-chrome-linux.png b/e2e/screenshots/partition_stories.test.ts-snapshots/axis-stories/theme-dark/should-dim-slices-on-legend-hover-sunburst-dark-chrome-linux.png new file mode 100644 index 00000000000..4b94ca7f83b Binary files /dev/null and b/e2e/screenshots/partition_stories.test.ts-snapshots/axis-stories/theme-dark/should-dim-slices-on-legend-hover-sunburst-dark-chrome-linux.png differ diff --git a/e2e/screenshots/partition_stories.test.ts-snapshots/axis-stories/theme-dark/should-dim-slices-on-legend-hover-treemap-dark-chrome-linux.png b/e2e/screenshots/partition_stories.test.ts-snapshots/axis-stories/theme-dark/should-dim-slices-on-legend-hover-treemap-dark-chrome-linux.png new file mode 100644 index 00000000000..1c41fd3e0ae Binary files /dev/null and b/e2e/screenshots/partition_stories.test.ts-snapshots/axis-stories/theme-dark/should-dim-slices-on-legend-hover-treemap-dark-chrome-linux.png differ diff --git a/e2e/screenshots/partition_stories.test.ts-snapshots/axis-stories/theme-light/should-dim-slices-on-legend-hover-sunburst-light-chrome-linux.png b/e2e/screenshots/partition_stories.test.ts-snapshots/axis-stories/theme-light/should-dim-slices-on-legend-hover-sunburst-light-chrome-linux.png new file mode 100644 index 00000000000..897598a84ef Binary files /dev/null and b/e2e/screenshots/partition_stories.test.ts-snapshots/axis-stories/theme-light/should-dim-slices-on-legend-hover-sunburst-light-chrome-linux.png differ diff --git a/e2e/screenshots/partition_stories.test.ts-snapshots/axis-stories/theme-light/should-dim-slices-on-legend-hover-treemap-light-chrome-linux.png b/e2e/screenshots/partition_stories.test.ts-snapshots/axis-stories/theme-light/should-dim-slices-on-legend-hover-treemap-light-chrome-linux.png new file mode 100644 index 00000000000..066e6e7b8f6 Binary files /dev/null and b/e2e/screenshots/partition_stories.test.ts-snapshots/axis-stories/theme-light/should-dim-slices-on-legend-hover-treemap-light-chrome-linux.png differ diff --git a/e2e/screenshots/stylings_stories.test.ts-snapshots/stylings-stories/dimmed-highlight-style/should-apply-opacity-only-dimming-on-bar-chart-light-theme-chrome-linux.png b/e2e/screenshots/stylings_stories.test.ts-snapshots/stylings-stories/dimmed-highlight-style/should-apply-opacity-only-dimming-on-bar-chart-light-theme-chrome-linux.png new file mode 100644 index 00000000000..622baaa9479 Binary files /dev/null and b/e2e/screenshots/stylings_stories.test.ts-snapshots/stylings-stories/dimmed-highlight-style/should-apply-opacity-only-dimming-on-bar-chart-light-theme-chrome-linux.png differ diff --git a/e2e/screenshots/stylings_stories.test.ts-snapshots/stylings-stories/dimmed-highlight-style/should-apply-opacity-only-dimming-on-partition-chart-dark-theme-chrome-linux.png b/e2e/screenshots/stylings_stories.test.ts-snapshots/stylings-stories/dimmed-highlight-style/should-apply-opacity-only-dimming-on-partition-chart-dark-theme-chrome-linux.png new file mode 100644 index 00000000000..44c64bfcca6 Binary files /dev/null and b/e2e/screenshots/stylings_stories.test.ts-snapshots/stylings-stories/dimmed-highlight-style/should-apply-opacity-only-dimming-on-partition-chart-dark-theme-chrome-linux.png differ diff --git a/e2e/screenshots/stylings_stories.test.ts-snapshots/stylings-stories/dimmed-highlight-style/theme-dark/should-dim-partition-chart-slices-on-legend-hover-dark-chrome-linux.png b/e2e/screenshots/stylings_stories.test.ts-snapshots/stylings-stories/dimmed-highlight-style/theme-dark/should-dim-partition-chart-slices-on-legend-hover-dark-chrome-linux.png new file mode 100644 index 00000000000..30ef0749deb Binary files /dev/null and b/e2e/screenshots/stylings_stories.test.ts-snapshots/stylings-stories/dimmed-highlight-style/theme-dark/should-dim-partition-chart-slices-on-legend-hover-dark-chrome-linux.png differ diff --git a/e2e/screenshots/stylings_stories.test.ts-snapshots/stylings-stories/dimmed-highlight-style/theme-dark/should-dim-xy-chart-series-on-bars-2-legend-hover-dark-chrome-linux.png b/e2e/screenshots/stylings_stories.test.ts-snapshots/stylings-stories/dimmed-highlight-style/theme-dark/should-dim-xy-chart-series-on-bars-2-legend-hover-dark-chrome-linux.png new file mode 100644 index 00000000000..4bba0aa50ab Binary files /dev/null and b/e2e/screenshots/stylings_stories.test.ts-snapshots/stylings-stories/dimmed-highlight-style/theme-dark/should-dim-xy-chart-series-on-bars-2-legend-hover-dark-chrome-linux.png differ diff --git a/e2e/screenshots/stylings_stories.test.ts-snapshots/stylings-stories/dimmed-highlight-style/theme-dark/should-dim-xy-chart-series-on-legend-hover-dark-chrome-linux.png b/e2e/screenshots/stylings_stories.test.ts-snapshots/stylings-stories/dimmed-highlight-style/theme-dark/should-dim-xy-chart-series-on-legend-hover-dark-chrome-linux.png new file mode 100644 index 00000000000..92b36ac9080 Binary files /dev/null and b/e2e/screenshots/stylings_stories.test.ts-snapshots/stylings-stories/dimmed-highlight-style/theme-dark/should-dim-xy-chart-series-on-legend-hover-dark-chrome-linux.png differ diff --git a/e2e/screenshots/stylings_stories.test.ts-snapshots/stylings-stories/dimmed-highlight-style/theme-light/should-dim-partition-chart-slices-on-legend-hover-light-chrome-linux.png b/e2e/screenshots/stylings_stories.test.ts-snapshots/stylings-stories/dimmed-highlight-style/theme-light/should-dim-partition-chart-slices-on-legend-hover-light-chrome-linux.png new file mode 100644 index 00000000000..55a7da5838e Binary files /dev/null and b/e2e/screenshots/stylings_stories.test.ts-snapshots/stylings-stories/dimmed-highlight-style/theme-light/should-dim-partition-chart-slices-on-legend-hover-light-chrome-linux.png differ diff --git a/e2e/screenshots/stylings_stories.test.ts-snapshots/stylings-stories/dimmed-highlight-style/theme-light/should-dim-xy-chart-series-on-bars-2-legend-hover-light-chrome-linux.png b/e2e/screenshots/stylings_stories.test.ts-snapshots/stylings-stories/dimmed-highlight-style/theme-light/should-dim-xy-chart-series-on-bars-2-legend-hover-light-chrome-linux.png new file mode 100644 index 00000000000..ce9c81c18f0 Binary files /dev/null and b/e2e/screenshots/stylings_stories.test.ts-snapshots/stylings-stories/dimmed-highlight-style/theme-light/should-dim-xy-chart-series-on-bars-2-legend-hover-light-chrome-linux.png differ diff --git a/e2e/screenshots/stylings_stories.test.ts-snapshots/stylings-stories/dimmed-highlight-style/theme-light/should-dim-xy-chart-series-on-legend-hover-light-chrome-linux.png b/e2e/screenshots/stylings_stories.test.ts-snapshots/stylings-stories/dimmed-highlight-style/theme-light/should-dim-xy-chart-series-on-legend-hover-light-chrome-linux.png new file mode 100644 index 00000000000..586ab7221e6 Binary files /dev/null and b/e2e/screenshots/stylings_stories.test.ts-snapshots/stylings-stories/dimmed-highlight-style/theme-light/should-dim-xy-chart-series-on-legend-hover-light-chrome-linux.png differ diff --git a/e2e/screenshots/stylings_stories.test.ts-snapshots/stylings-stories/texture/bar/should-use-hover-opacity-for-texture-chrome-linux.png b/e2e/screenshots/stylings_stories.test.ts-snapshots/stylings-stories/texture/bar/should-use-hover-opacity-for-texture-chrome-linux.png index d53db0e0454..f10c97e9972 100644 Binary files a/e2e/screenshots/stylings_stories.test.ts-snapshots/stylings-stories/texture/bar/should-use-hover-opacity-for-texture-chrome-linux.png and b/e2e/screenshots/stylings_stories.test.ts-snapshots/stylings-stories/texture/bar/should-use-hover-opacity-for-texture-chrome-linux.png differ diff --git a/e2e/screenshots/waffle_stories.test.ts-snapshots/waffle/theme-dark/should-dim-cells-on-legend-hover-dark-chrome-linux.png b/e2e/screenshots/waffle_stories.test.ts-snapshots/waffle/theme-dark/should-dim-cells-on-legend-hover-dark-chrome-linux.png new file mode 100644 index 00000000000..252ec7933a3 Binary files /dev/null and b/e2e/screenshots/waffle_stories.test.ts-snapshots/waffle/theme-dark/should-dim-cells-on-legend-hover-dark-chrome-linux.png differ diff --git a/e2e/screenshots/waffle_stories.test.ts-snapshots/waffle/theme-light/should-dim-cells-on-legend-hover-light-chrome-linux.png b/e2e/screenshots/waffle_stories.test.ts-snapshots/waffle/theme-light/should-dim-cells-on-legend-hover-light-chrome-linux.png new file mode 100644 index 00000000000..92502701bde Binary files /dev/null and b/e2e/screenshots/waffle_stories.test.ts-snapshots/waffle/theme-light/should-dim-cells-on-legend-hover-light-chrome-linux.png differ diff --git a/e2e/tests/flame_stories.test.ts b/e2e/tests/flame_stories.test.ts index 2e6aff30af5..4e3f9509d61 100644 --- a/e2e/tests/flame_stories.test.ts +++ b/e2e/tests/flame_stories.test.ts @@ -38,4 +38,24 @@ test.describe('Flame stories', () => { }, ); }); + + test('should dim icicle chart (linear renderer) on legend hover - fill dimming', async ({ page }) => { + const action = async () => { + await page.locator('.echLegendItem').first().hover(); + }; + await common.expectChartAtUrlToMatchScreenshot(page)( + `http://localhost:9001/?path=/story/flame-alpha--icicle-chart&globals=theme:light&knob-Clip%20text%20%28use%20linear%20renderer%29=true`, + { action }, + ); + }); + + test('should dim icicle chart (linear renderer) on legend hover - opacity dimming', async ({ page }) => { + const action = async () => { + await page.locator('.echLegendItem').first().hover(); + }; + await common.expectChartAtUrlToMatchScreenshot(page)( + `http://localhost:9001/?path=/story/flame-alpha--icicle-chart&globals=theme:light&knob-Clip%20text%20%28use%20linear%20renderer%29=true&knob-Use%20opacity-only%20dimming=true`, + { action }, + ); + }); }); diff --git a/e2e/tests/partition_stories.test.ts b/e2e/tests/partition_stories.test.ts index f7d9818fd35..126a413b804 100644 --- a/e2e/tests/partition_stories.test.ts +++ b/e2e/tests/partition_stories.test.ts @@ -106,4 +106,26 @@ test.describe('Axis stories', () => { { left: 300, top: 100 }, ); }); + + eachTheme.describe(({ theme, urlParam }) => { + test(`should dim slices on legend hover - sunburst - ${theme}`, async ({ page }) => { + const action = async () => { + await common.moveMouseRelativeToDOMElement(page)({ left: 5, top: 5 }, '.echLegendItem'); + }; + await common.expectChartAtUrlToMatchScreenshot(page)( + `http://localhost:9001/?path=/story/legend--piechart&${urlParam}&knob-Partition Layout=sunburst&knob-flatLegend=true&knob-legendMaxDepth=2`, + { action }, + ); + }); + + test(`should dim slices on legend hover - treemap - ${theme}`, async ({ page }) => { + const action = async () => { + await common.moveMouseRelativeToDOMElement(page)({ left: 5, top: 5 }, '.echLegendItem'); + }; + await common.expectChartAtUrlToMatchScreenshot(page)( + `http://localhost:9001/?path=/story/legend--piechart&${urlParam}&knob-Partition Layout=treemap&knob-flatLegend=true&knob-legendMaxDepth=2`, + { action }, + ); + }); + }); }); diff --git a/e2e/tests/stylings_stories.test.ts b/e2e/tests/stylings_stories.test.ts index c66325e7aec..a729eb51abf 100644 --- a/e2e/tests/stylings_stories.test.ts +++ b/e2e/tests/stylings_stories.test.ts @@ -9,7 +9,7 @@ import { test } from '@playwright/test'; import { SeriesType } from '../constants'; -import { pwEach } from '../helpers'; +import { eachTheme, pwEach } from '../helpers'; import { common } from '../page_objects'; test.describe('Stylings stories', () => { @@ -89,4 +89,58 @@ test.describe('Stylings stories', () => { { top: 150, right: 150 }, ); }); + + test.describe('Dimmed highlight style', () => { + eachTheme.describe(({ theme, urlParam }) => { + test(`should dim XY chart series on Bars 2 legend hover - ${theme}`, async ({ page }) => { + const action = async () => { + // Hover on the "Bars 2" legend item + const legendItem = page.locator('[data-ech-series-name="Bars 2"]'); + await legendItem.hover(); + }; + await common.expectChartAtUrlToMatchScreenshot(page)( + // Hide partition chart so XY chart is the first .echChart + `http://localhost:9001/?path=/story/stylings--dimmed-highlight-style&${urlParam}&knob-Show Partition Chart=false`, + { action }, + ); + }); + + test(`should dim partition chart slices on legend hover - ${theme}`, async ({ page }) => { + const action = async () => { + // Hover on the first legend item + const legendItem = page.locator('.echLegendItem').first(); + await legendItem.hover(); + }; + await common.expectChartAtUrlToMatchScreenshot(page)( + // Hide XY chart so partition chart is the only .echChart + `http://localhost:9001/?path=/story/stylings--dimmed-highlight-style&${urlParam}&knob-Show XY Chart=false`, + { action }, + ); + }); + }); + + test('should apply opacity-only dimming on bar chart - light theme', async ({ page }) => { + const action = async () => { + // Hover on the "Bars 2" legend item + const legendItem = page.locator('[data-ech-series-name="Bars 2"]'); + await legendItem.hover(); + }; + await common.expectChartAtUrlToMatchScreenshot(page)( + `http://localhost:9001/?path=/story/stylings--dimmed-highlight-style&globals=theme:light&knob-Show Partition Chart=false&knob-Use opacity-only dimming=true&knob-Alpha (opacity)=0.10`, + { action }, + ); + }); + + test('should apply opacity-only dimming on partition chart - dark theme', async ({ page }) => { + const action = async () => { + // Hover on the first legend item + const legendItem = page.locator('.echLegendItem').first(); + await legendItem.hover(); + }; + await common.expectChartAtUrlToMatchScreenshot(page)( + `http://localhost:9001/?path=/story/stylings--dimmed-highlight-style&globals=theme:dark&knob-Show XY Chart=false&knob-Use opacity-only dimming=true&knob-Alpha (opacity)=0.15`, + { action }, + ); + }); + }); }); diff --git a/e2e/tests/waffle_stories.test.ts b/e2e/tests/waffle_stories.test.ts index e873ccc5ac6..1819dc097ed 100644 --- a/e2e/tests/waffle_stories.test.ts +++ b/e2e/tests/waffle_stories.test.ts @@ -8,6 +8,7 @@ import { test } from '@playwright/test'; +import { eachTheme } from '../helpers'; import { common } from '../page_objects/common'; test.describe('Waffle', () => { @@ -21,4 +22,16 @@ test.describe('Waffle', () => { 'http://localhost:9001/?path=/story/waffle-alpha--test&globals=toggles.showHeader:true;toggles.showChartTitle:false;toggles.showChartDescription:false;theme:light&knob-use alpha=true', ); }); + + eachTheme.describe(({ theme, urlParam }) => { + test(`should dim cells on legend hover - ${theme}`, async ({ page }) => { + const action = async () => { + await common.moveMouseRelativeToDOMElement(page)({ left: 5, top: 5 }, '.echLegendItem'); + }; + await common.expectChartAtUrlToMatchScreenshot(page)( + `http://localhost:9001/?path=/story/waffle-alpha--simple&${urlParam}`, + { action }, + ); + }); + }); }); diff --git a/packages/charts/api/charts.api.md b/packages/charts/api/charts.api.md index e3a79cea6af..7b878f5e81e 100644 --- a/packages/charts/api/charts.api.md +++ b/packages/charts/api/charts.api.md @@ -2315,6 +2315,13 @@ export type PartialTheme = RecursivePartial; // @public export const Partition: (props: SFProps, keyof (typeof buildProps)["overrides"], keyof (typeof buildProps)["defaults"], keyof (typeof buildProps)["optionals"], keyof (typeof buildProps)["requires"]>) => null; +// @public (undocumented) +export type PartitionDimmedStyle = { + opacity: number; +} | { + fill: Color | ColorVariant; +}; + // @public export type PartitionElementEvent = [layers: Array, seriesIdentifier: SeriesIdentifier]; @@ -2370,6 +2377,7 @@ export interface PartitionStyle extends FillFontSizeRange { // // (undocumented) circlePadding: Distance; + dimmed: PartitionDimmedStyle; // Warning: (ae-forgotten-export) The symbol "SizeRatio" needs to be exported by the entry point index.d.ts emptySizeRatio: SizeRatio; // (undocumented) @@ -2655,6 +2663,14 @@ export interface RectBorderStyle { // @public (undocumented) export interface RectStyle { + dimmed: { + opacity: number; + } | { + fill: Color | ColorVariant; + texture: { + opacity: number; + }; + }; fill?: Color | ColorVariant; opacity: number; texture?: TexturedStyles; diff --git a/packages/charts/src/chart_types/partition_chart/renderer/canvas/canvas_linear_renderers.ts b/packages/charts/src/chart_types/partition_chart/renderer/canvas/canvas_linear_renderers.ts index dea35066ae6..48e4dffa233 100644 --- a/packages/charts/src/chart_types/partition_chart/renderer/canvas/canvas_linear_renderers.ts +++ b/packages/charts/src/chart_types/partition_chart/renderer/canvas/canvas_linear_renderers.ts @@ -7,8 +7,15 @@ */ import type { AnimationState, ContinuousDomainFocus } from './partition'; +import { multiplyColorOpacity } from '../../../../common/color_library_wrappers'; import { Colors } from '../../../../common/colors'; -import type { ShapeViewModel } from '../../layout/types/viewmodel_types'; +import type { LegendPath } from '../../../../state/actions/legend'; +import { getColorFromVariant } from '../../../../utils/common'; +import { getDimmedColor } from '../../../../utils/themes/dimmed_colors'; +import type { PartitionStyle } from '../../../../utils/themes/partition'; +import type { QuadViewModel, ShapeViewModel } from '../../layout/types/viewmodel_types'; +import type { LegendStrategy } from '../../layout/utils/highlighted_geoms'; +import { highlightedGeoms } from '../../layout/utils/highlighted_geoms'; const linear = (x: number) => x; const easeInOut = (alpha: number) => (x: number) => x ** alpha / (x ** alpha + (1 - x) ** alpha); @@ -30,8 +37,19 @@ export function renderLinearPartitionCanvas2d( animation, }: ShapeViewModel, { currentFocusX0, currentFocusX1, prevFocusX0, prevFocusX1 }: ContinuousDomainFocus, + highlightedLegendPath: LegendPath, + legendStrategy: LegendStrategy | undefined, + flatLegend: boolean | undefined, + partitionStyle: PartitionStyle, animationState: AnimationState, ) { + // Calculate which quads are highlighted for legend dimming + const highlightedQuadSet = new Set(); + if (highlightedLegendPath.length > 0) { + const highlighted = highlightedGeoms(legendStrategy, flatLegend, quadViewModel, highlightedLegendPath); + highlighted.forEach((quad) => highlightedQuadSet.add(quad)); + } + if (animation?.duration) { window.cancelAnimationFrame(animationState.rafId); render(0); @@ -72,7 +90,8 @@ export function renderLinearPartitionCanvas2d( ctx.translate(diskCenter.x, diskCenter.y); ctx.clearRect(0, 0, width, height); - quadViewModel.forEach(({ fillColor, x0, x1, y0px: y0, y1px: y1, dataName, textColor, depth }) => { + quadViewModel.forEach((quad) => { + const { fillColor, x0, x1, y0px: y0, y1px: y1, dataName, textColor, depth } = quad; if (y1 - y0 <= padding) return; const fx0 = Math.max((x0 - focusX0) * scale, 0); @@ -87,7 +106,18 @@ export function renderLinearPartitionCanvas2d( const fWidth = fx1 - fx0; const fPadding = Math.min(padding, MAX_PADDING_RATIO * fWidth); - ctx.fillStyle = fillColor; + const isDimmed = highlightedQuadSet.size > 0 && !highlightedQuadSet.has(quad); + const baseFillColor = getColorFromVariant( + fillColor, + getDimmedColor(isDimmed, partitionStyle.dimmed, 'fill', fillColor), + ); + // Apply opacity when dimmed with opacity config + const dimmedFillColor = + isDimmed && 'opacity' in partitionStyle.dimmed + ? multiplyColorOpacity(baseFillColor, partitionStyle.dimmed.opacity) + : baseFillColor; + + ctx.fillStyle = dimmedFillColor; ctx.beginPath(); ctx.rect(fx0 + fPadding, y0 + padding / 2, fWidth - fPadding, y1 - y0 - padding); ctx.fill(); diff --git a/packages/charts/src/chart_types/partition_chart/renderer/canvas/canvas_renderers.ts b/packages/charts/src/chart_types/partition_chart/renderer/canvas/canvas_renderers.ts index 5cba1a6b5e7..7c148703ec2 100644 --- a/packages/charts/src/chart_types/partition_chart/renderer/canvas/canvas_renderers.ts +++ b/packages/charts/src/chart_types/partition_chart/renderer/canvas/canvas_renderers.ts @@ -6,13 +6,17 @@ * Side Public License, v 1. */ -import { colorToRgba, RGBATupleToString } from '../../../../common/color_library_wrappers'; +import { colorToRgba, multiplyColorOpacity, RGBATupleToString } from '../../../../common/color_library_wrappers'; import type { Color } from '../../../../common/colors'; import { TAU } from '../../../../common/constants'; import type { Pixels } from '../../../../common/geometry'; import { cssFontShorthand, HorizontalAlignment } from '../../../../common/text_utils'; import { renderLayers, withContext } from '../../../../renderers/canvas'; import { MIN_STROKE_WIDTH } from '../../../../renderers/canvas/primitives/line'; +import type { LegendPath } from '../../../../state/actions/legend'; +import { getColorFromVariant } from '../../../../utils/common'; +import { getDimmedColor } from '../../../../utils/themes/dimmed_colors'; +import type { PartitionStyle } from '../../../../utils/themes/partition'; import type { LinkLabelVM, OutsideLinksViewModel, @@ -22,6 +26,8 @@ import type { ShapeViewModel, TextRow, } from '../../layout/types/viewmodel_types'; +import type { LegendStrategy } from '../../layout/utils/highlighted_geoms'; +import { highlightedGeoms } from '../../layout/utils/highlighted_geoms'; import type { LinkLabelsViewModelSpec } from '../../layout/viewmodel/link_text_layout'; import { isSunburst } from '../../layout/viewmodel/viewmodel'; @@ -103,7 +109,8 @@ function renderRowSets(ctx: CanvasRenderingContext2D, rowSets: RowSet[], linkLab function renderTaperedBorder( ctx: CanvasRenderingContext2D, - { strokeWidth, strokeStyle, fillColor, x0, x1, y0px, y1px }: QuadViewModel, + { strokeWidth, strokeStyle, x0, x1, y0px, y1px }: QuadViewModel, + fillColor: Color, ) { const X0 = x0 - TAU / 4; const X1 = x1 - TAU / 4; @@ -149,22 +156,56 @@ function renderTaperedBorder( } } -function renderSectors(ctx: CanvasRenderingContext2D, quadViewModel: QuadViewModel[]) { +function renderSectors( + ctx: CanvasRenderingContext2D, + quadViewModel: QuadViewModel[], + highlightedQuadSet: Set, + partitionStyle: PartitionStyle, +) { withContext(ctx, () => { ctx.scale(1, -1); // D3 and Canvas2d use a left-handed coordinate system (+y = down) but the ViewModel uses +y = up, so we must locally invert Y quadViewModel.forEach((quad: QuadViewModel) => { - if (quad.x0 !== quad.x1) renderTaperedBorder(ctx, quad); + if (quad.x0 === quad.x1) return; + + const isDimmed = highlightedQuadSet.size > 0 && !highlightedQuadSet.has(quad); + const baseFillColor = getColorFromVariant( + quad.fillColor, + getDimmedColor(isDimmed, partitionStyle.dimmed, 'fill', quad.fillColor), + ); + // Apply opacity when dimmed with opacity config + const fillColor = + isDimmed && 'opacity' in partitionStyle.dimmed + ? multiplyColorOpacity(baseFillColor, partitionStyle.dimmed.opacity) + : baseFillColor; + renderTaperedBorder(ctx, quad, fillColor); }); }); } -function renderRectangles(ctx: CanvasRenderingContext2D, quadViewModel: QuadViewModel[]) { +function renderRectangles( + ctx: CanvasRenderingContext2D, + quadViewModel: QuadViewModel[], + highlightedQuadSet: Set, + partitionStyle: PartitionStyle, +) { withContext(ctx, () => { ctx.scale(1, -1); // D3 and Canvas2d use a left-handed coordinate system (+y = down) but the ViewModel uses +y = up, so we must locally invert Y - quadViewModel.forEach(({ strokeWidth, fillColor, x0, x1, y0px, y1px }) => { + quadViewModel.forEach((quad) => { + const { strokeWidth, fillColor, x0, x1, y0px, y1px } = quad; // only draw a shape if it would show up at all if (x1 - x0 >= 1 && y1px - y0px >= 1) { - ctx.fillStyle = fillColor; + const isDimmed = highlightedQuadSet.size > 0 && !highlightedQuadSet.has(quad); + const baseFillColor = getColorFromVariant( + fillColor, + getDimmedColor(isDimmed, partitionStyle.dimmed, 'fill', fillColor), + ); + // Apply opacity when dimmed with opacity config + const dimmedFillColor = + isDimmed && 'opacity' in partitionStyle.dimmed + ? multiplyColorOpacity(baseFillColor, partitionStyle.dimmed.opacity) + : baseFillColor; + + ctx.fillStyle = dimmedFillColor; ctx.beginPath(); ctx.moveTo(x0, y0px); ctx.lineTo(x0, y1px); @@ -277,6 +318,10 @@ export function renderPartitionCanvas2d( panel, chartDimensions, }: ShapeViewModel, + highlightedLegendPath: LegendPath, + legendStrategy: LegendStrategy | undefined, + flatLegend: boolean | undefined, + partitionStyle: PartitionStyle, ) { const { sectorLineWidth, sectorLineStroke, linkLabel } = style; @@ -322,13 +367,24 @@ export function renderPartitionCanvas2d( ctx.strokeStyle = sectorLineStroke; ctx.lineWidth = sectorLineWidth; + // Calculate which quads are highlighted for legend dimming + const highlightedQuadSet = new Set(); + if (highlightedLegendPath.length > 0) { + // Use highlightedGeoms to determine which quads match the legend path + const highlighted = highlightedGeoms(legendStrategy, flatLegend, quadViewModel, highlightedLegendPath); + highlighted.forEach((quad) => highlightedQuadSet.add(quad)); + } + // painter's algorithm, like that of SVG: the sequence determines what overdraws what; first element of the array is drawn first // (of course, with SVG, it's for ambiguous situations only, eg. when 3D transforms with different Z values aren't used, but // unlike SVG and esp. WebGL, Canvas2d doesn't support the 3rd dimension well, see ctx.transform / ctx.setTransform). // The layers are callbacks, because of the need to not bake in the `ctx`, it feels more composable and uncoupled this way. renderLayers(ctx, [ // bottom layer: sectors (pie slices, ring sectors etc.) - () => (isSunburst(layout) ? renderSectors(ctx, quadViewModel) : renderRectangles(ctx, quadViewModel)), + () => + isSunburst(layout) + ? renderSectors(ctx, quadViewModel, highlightedQuadSet, partitionStyle) + : renderRectangles(ctx, quadViewModel, highlightedQuadSet, partitionStyle), // all the fill-based, potentially multirow text, whether inside or outside the sector () => renderRowSets(ctx, rowSets, linkLineColor), diff --git a/packages/charts/src/chart_types/partition_chart/renderer/canvas/canvas_wrapped_renderers.ts b/packages/charts/src/chart_types/partition_chart/renderer/canvas/canvas_wrapped_renderers.ts index b0787bcd177..90dfe49d7e9 100644 --- a/packages/charts/src/chart_types/partition_chart/renderer/canvas/canvas_wrapped_renderers.ts +++ b/packages/charts/src/chart_types/partition_chart/renderer/canvas/canvas_wrapped_renderers.ts @@ -6,7 +6,14 @@ * Side Public License, v 1. */ -import type { ShapeViewModel } from '../../layout/types/viewmodel_types'; +import { multiplyColorOpacity } from '../../../../common/color_library_wrappers'; +import type { LegendPath } from '../../../../state/actions/legend'; +import { getColorFromVariant } from '../../../../utils/common'; +import { getDimmedColor } from '../../../../utils/themes/dimmed_colors'; +import type { PartitionStyle } from '../../../../utils/themes/partition'; +import type { QuadViewModel, ShapeViewModel } from '../../layout/types/viewmodel_types'; +import type { LegendStrategy } from '../../layout/utils/highlighted_geoms'; +import { highlightedGeoms } from '../../layout/utils/highlighted_geoms'; const MAX_PADDING_RATIO = 0.25; @@ -22,11 +29,22 @@ export function renderWrappedPartitionCanvas2d( height: panelHeight, chartDimensions: { width: containerWidth, height: containerHeight }, }: ShapeViewModel, + highlightedLegendPath: LegendPath, + legendStrategy: LegendStrategy | undefined, + flatLegend: boolean | undefined, + partitionStyle: PartitionStyle, ) { const width = containerWidth * panelWidth; const height = containerHeight * panelHeight; const cornerRatio = 0.2; + // Calculate which quads are highlighted for legend dimming + const highlightedQuadSet = new Set(); + if (highlightedLegendPath.length > 0) { + const highlighted = highlightedGeoms(legendStrategy, flatLegend, quadViewModel, highlightedLegendPath); + highlighted.forEach((quad) => highlightedQuadSet.add(quad)); + } + ctx.save(); ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; @@ -36,7 +54,8 @@ export function renderWrappedPartitionCanvas2d( ctx.translate(diskCenter.x, diskCenter.y); ctx.clearRect(0, 0, width, height); - quadViewModel.forEach(({ fillColor, x0, x1, y0px: y0, y1px: y1 }) => { + quadViewModel.forEach((quad) => { + const { x0, x1, y0px: y0, y1px: y1 } = quad; if (y1 - y0 <= padding) return; const fWidth = x1 - x0; @@ -47,6 +66,17 @@ export function renderWrappedPartitionCanvas2d( const y = y0 + padding / 2; const r = cornerRatio * Math.min(w, h); + const isDimmed = highlightedQuadSet.size > 0 && !highlightedQuadSet.has(quad); + const baseFillColor = getColorFromVariant( + quad.fillColor, + getDimmedColor(isDimmed, partitionStyle.dimmed, 'fill', quad.fillColor), + ); + // Apply opacity when dimmed with opacity config + const fillColor = + isDimmed && 'opacity' in partitionStyle.dimmed + ? multiplyColorOpacity(baseFillColor, partitionStyle.dimmed.opacity) + : baseFillColor; + ctx.fillStyle = fillColor; ctx.beginPath(); diff --git a/packages/charts/src/chart_types/partition_chart/renderer/canvas/partition.tsx b/packages/charts/src/chart_types/partition_chart/renderer/canvas/partition.tsx index 08bb92422bd..f33abf072c2 100644 --- a/packages/charts/src/chart_types/partition_chart/renderer/canvas/partition.tsx +++ b/packages/charts/src/chart_types/partition_chart/renderer/canvas/partition.tsx @@ -21,6 +21,7 @@ import { ScreenReaderSummary } from '../../../../components/accessibility'; import { clearCanvas } from '../../../../renderers/canvas'; import type { SettingsSpec } from '../../../../specs/settings'; import { onChartRendered } from '../../../../state/actions/chart'; +import type { LegendPath } from '../../../../state/actions/legend'; import type { GlobalChartState } from '../../../../state/chart_state'; import type { A11ySettings } from '../../../../state/selectors/get_accessibility_config'; import { DEFAULT_A11Y_SETTINGS, getA11ySettingsSelector } from '../../../../state/selectors/get_accessibility_config'; @@ -29,6 +30,8 @@ import { getChartThemeSelector } from '../../../../state/selectors/get_chart_the import { getInternalIsInitializedSelector, InitStatus } from '../../../../state/selectors/get_internal_is_intialized'; import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_spec'; import type { Dimensions } from '../../../../utils/dimensions'; +import { LIGHT_THEME } from '../../../../utils/themes/light_theme'; +import type { PartitionStyle } from '../../../../utils/themes/partition'; import { MODEL_KEY } from '../../layout/config'; import type { QuadViewModel, ShapeViewModel, SmallMultiplesDescriptors } from '../../layout/types/viewmodel_types'; import { hasMostlyRTLLabels, nullShapeViewModel } from '../../layout/types/viewmodel_types'; @@ -58,6 +61,10 @@ interface ReactiveChartStateProps { a11ySettings: A11ySettings; debug: SettingsSpec['debug']; background: Color; + highlightedLegendPath: LegendPath; + legendStrategy: SettingsSpec['legendStrategy']; + flatLegend: SettingsSpec['flatLegend']; + partitionStyle: PartitionStyle; } interface ReactiveChartDispatchProps { @@ -182,12 +189,39 @@ class PartitionComponent extends React.Component { const focus = props.geometriesFoci[geometryIndex]; if (!focus) return; - const renderer = isSimpleLinear(geometries.layout, geometries.style.fillLabel, geometries.layers) - ? renderLinearPartitionCanvas2d - : isWaffle(geometries.layout) - ? renderWrappedPartitionCanvas2d - : renderPartitionCanvas2d; - renderer(ctx, devicePixelRatio, geometries, focus, this.animationState); + if (isSimpleLinear(geometries.layout, geometries.style.fillLabel, geometries.layers)) { + renderLinearPartitionCanvas2d( + ctx, + devicePixelRatio, + geometries, + focus, + props.highlightedLegendPath, + props.legendStrategy, + props.flatLegend, + props.partitionStyle, + this.animationState, + ); + } else if (isWaffle(geometries.layout)) { + renderWrappedPartitionCanvas2d( + ctx, + devicePixelRatio, + geometries, + props.highlightedLegendPath, + props.legendStrategy, + props.flatLegend, + props.partitionStyle, + ); + } else { + renderPartitionCanvas2d( + ctx, + devicePixelRatio, + geometries, + props.highlightedLegendPath, + props.legendStrategy, + props.flatLegend, + props.partitionStyle, + ); + } }); } } @@ -216,6 +250,10 @@ const DEFAULT_PROPS: ReactiveChartStateProps = { a11ySettings: DEFAULT_A11Y_SETTINGS, debug: false, background: Colors.Transparent.keyword, + highlightedLegendPath: [], + legendStrategy: undefined, + flatLegend: undefined, + partitionStyle: LIGHT_THEME.partition, }; const mapStateToProps = (state: GlobalChartState): ReactiveChartStateProps => { @@ -223,6 +261,8 @@ const mapStateToProps = (state: GlobalChartState): ReactiveChartStateProps => { return DEFAULT_PROPS; } const multiGeometries = partitionMultiGeometries(state); + const settings = getSettingsSpecSelector(state); + const theme = getChartThemeSelector(state); return { isRTL: hasMostlyRTLLabels(multiGeometries), @@ -232,8 +272,12 @@ const mapStateToProps = (state: GlobalChartState): ReactiveChartStateProps => { chartDimensions: getChartContainerDimensionsSelector(state), geometriesFoci: partitionDrilldownFocus(state), a11ySettings: getA11ySettingsSelector(state), - debug: getSettingsSpecSelector(state).debug, - background: getChartThemeSelector(state).background.color, + debug: settings.debug, + background: theme.background.color, + highlightedLegendPath: state.interactions.highlightedLegendPath, + legendStrategy: settings.legendStrategy, + flatLegend: settings.flatLegend, + partitionStyle: theme.partition, }; }; diff --git a/packages/charts/src/chart_types/partition_chart/renderer/dom/highlighter_legend.tsx b/packages/charts/src/chart_types/partition_chart/renderer/dom/highlighter_legend.tsx deleted file mode 100644 index 6ef3786f1d2..00000000000 --- a/packages/charts/src/chart_types/partition_chart/renderer/dom/highlighter_legend.tsx +++ /dev/null @@ -1,46 +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 { connect } from 'react-redux'; - -import type { HighlighterProps } from './highlighter'; -import { HighlighterComponent, DEFAULT_PROPS, highlightSetMapper } from './highlighter'; -import type { GlobalChartState } from '../../../../state/chart_state'; -import { getChartContainerDimensionsSelector } from '../../../../state/selectors/get_chart_container_dimensions'; -import { getInternalIsInitializedSelector, InitStatus } from '../../../../state/selectors/get_internal_is_intialized'; -import { partitionDrilldownFocus, partitionMultiGeometries } from '../../state/selectors/geometries'; -import { legendHoverHighlightNodes } from '../../state/selectors/get_highlighted_shapes'; - -const legendMapStateToProps = (state: GlobalChartState): HighlighterProps => { - if (getInternalIsInitializedSelector(state) !== InitStatus.Initialized) { - return DEFAULT_PROPS; - } - - const { chartId } = state; - - const geometries = legendHoverHighlightNodes(state); - const geometriesFoci = partitionDrilldownFocus(state); - const canvasDimension = getChartContainerDimensionsSelector(state); - const multiGeometries = partitionMultiGeometries(state); - const highlightMapper = highlightSetMapper(geometries, geometriesFoci); - const highlightSets = multiGeometries.map(highlightMapper); - - return { - chartId, - initialized: true, - renderAsOverlay: false, - canvasDimension, - highlightSets, - }; -}; - -/** - * Partition chart highlighter from legend events - * @internal - */ -export const HighlighterFromLegend = connect(legendMapStateToProps)(HighlighterComponent); diff --git a/packages/charts/src/chart_types/partition_chart/renderer/dom/layered_partition_chart.tsx b/packages/charts/src/chart_types/partition_chart/renderer/dom/layered_partition_chart.tsx index 92bf779f9f6..76443f49169 100644 --- a/packages/charts/src/chart_types/partition_chart/renderer/dom/layered_partition_chart.tsx +++ b/packages/charts/src/chart_types/partition_chart/renderer/dom/layered_partition_chart.tsx @@ -10,12 +10,16 @@ import type { RefObject } from 'react'; import React from 'react'; import { HighlighterFromHover } from './highlighter_hover'; -import { HighlighterFromLegend } from './highlighter_legend'; import { Tooltip } from '../../../../components/tooltip/tooltip'; import type { BackwardRef, ChartRenderer } from '../../../../state/internal_chart_renderer'; import { Partition } from '../canvas/partition'; -/** @internal */ +/** + * Partition chart renderer + * - HighlighterFromHover: SVG overlay for direct slice hover (immediate visual feedback) + * - Canvas dimming: Used for legend hover (consistent with bar/line/area charts) + * @internal + */ export const chartRenderer: ChartRenderer = ( containerRef: BackwardRef, forwardStageRef: RefObject, @@ -24,6 +28,5 @@ export const chartRenderer: ChartRenderer = ( - ); diff --git a/packages/charts/src/chart_types/xy_chart/renderer/canvas/bars.ts b/packages/charts/src/chart_types/xy_chart/renderer/canvas/bars.ts index 9738dddb3a1..9b9dc5ae4cf 100644 --- a/packages/charts/src/chart_types/xy_chart/renderer/canvas/bars.ts +++ b/packages/charts/src/chart_types/xy_chart/renderer/canvas/bars.ts @@ -38,7 +38,16 @@ export function renderBars( const { x, y, width, height, color, seriesStyle: style, seriesIdentifier } = barGeometry; const rect = { x, y, width, height }; const geometryStateStyle = getGeometryStateStyle(seriesIdentifier, sharedStyle, highlightedLegendItem); - const barStyle = buildBarStyle(ctx, imgCanvas, color, style.rect, style.rectBorder, geometryStateStyle, rect); + const barStyle = buildBarStyle( + ctx, + imgCanvas, + color, + style.rect, + style.rectBorder, + geometryStateStyle, + sharedStyle, + rect, + ); renderRect(ctx, rect, barStyle.fill, barStyle.stroke); }), { area: getPanelClipping(panel, rotation), shouldClip: true }, diff --git a/packages/charts/src/chart_types/xy_chart/renderer/canvas/points.ts b/packages/charts/src/chart_types/xy_chart/renderer/canvas/points.ts index f7672db3429..6887eff1f78 100644 --- a/packages/charts/src/chart_types/xy_chart/renderer/canvas/points.ts +++ b/packages/charts/src/chart_types/xy_chart/renderer/canvas/points.ts @@ -16,6 +16,7 @@ import type { Rotation } from '../../../../utils/common'; import { getColorFromVariant } from '../../../../utils/common'; import type { Dimensions } from '../../../../utils/dimensions'; import type { GeometryHighlightState, PointGeometry } from '../../../../utils/geometry'; +import { getDimmedColor } from '../../../../utils/themes/dimmed_colors'; import type { GeometryStateStyle, PointStyle } from '../../../../utils/themes/theme'; import { isolatedPointRadius } from '../../rendering/points'; /** @@ -42,13 +43,12 @@ export function renderPoints( const useIsolatedPointRadius = hideDataPoints && !hasConnectingLine; - const opacity = - highlightState === 'dimmed' && 'opacity' in pointStyle.dimmed ? pointStyle.dimmed.opacity : pointStyle.opacity; + const isDimmed = highlightState === 'dimmed'; + const opacity = isDimmed && 'opacity' in pointStyle.dimmed ? pointStyle.dimmed.opacity : pointStyle.opacity; - const dimmedFill = - highlightState === 'dimmed' && 'fill' in pointStyle.dimmed ? colorToRgba(pointStyle.dimmed.fill) : undefined; - const dimmedStroke = - highlightState === 'dimmed' && 'stroke' in pointStyle.dimmed ? colorToRgba(pointStyle.dimmed.stroke) : undefined; + // Pre-compute dimmed color config for use inside the loop (resolved per-point via getColorFromVariant) + const dimmedFillColorVariant = getDimmedColor(isDimmed, pointStyle.dimmed, 'fill', undefined); + const dimmedStrokeColorVariant = getDimmedColor(isDimmed, pointStyle.dimmed, 'stroke', undefined); const focusedStrokeWidth = highlightState === 'focused' && pointStyle.focused ? pointStyle.focused.strokeWidth : undefined; @@ -69,6 +69,13 @@ export function renderPoints( ? colorToRgba(getColorFromVariant(color, pointStyle.stroke)) : style.fill.color; + const dimmedFill = dimmedFillColorVariant + ? colorToRgba(getColorFromVariant(color, dimmedFillColorVariant)) + : undefined; + const dimmedStroke = dimmedStrokeColorVariant + ? colorToRgba(getColorFromVariant(color, dimmedStrokeColorVariant)) + : undefined; + const fill = { color: overrideOpacity(dimmedFill ?? fillColor, (fillOpacity) => fillOpacity * opacity) }; const stroke = { diff --git a/packages/charts/src/chart_types/xy_chart/renderer/canvas/styles/area.ts b/packages/charts/src/chart_types/xy_chart/renderer/canvas/styles/area.ts index 76f0ffda3f6..7937335dc0f 100644 --- a/packages/charts/src/chart_types/xy_chart/renderer/canvas/styles/area.ts +++ b/packages/charts/src/chart_types/xy_chart/renderer/canvas/styles/area.ts @@ -12,6 +12,7 @@ import type { Fill } from '../../../../../geoms/types'; import type { ColorVariant } from '../../../../../utils/common'; import { getColorFromVariant } from '../../../../../utils/common'; import type { GeometryHighlightState } from '../../../../../utils/geometry'; +import { getDimmedColor } from '../../../../../utils/themes/dimmed_colors'; import type { AreaStyle, TexturedStyles } from '../../../../../utils/themes/theme'; import { getTextureStyles } from '../../../utils/texture'; @@ -34,7 +35,7 @@ export function buildAreaStyles( highlightState: GeometryHighlightState, ): Fill { const isDimmed = highlightState === 'dimmed'; - const fillColor = isDimmed && 'fill' in themeAreaStyle.dimmed ? themeAreaStyle.dimmed.fill : seriesColor; + const fillColor = getDimmedColor(isDimmed, themeAreaStyle.dimmed, 'fill', seriesColor); const opacity = isDimmed && 'opacity' in themeAreaStyle.dimmed ? themeAreaStyle.dimmed.opacity * themeAreaStyle.opacity diff --git a/packages/charts/src/chart_types/xy_chart/renderer/canvas/styles/bar.test.ts b/packages/charts/src/chart_types/xy_chart/renderer/canvas/styles/bar.test.ts index 5438d663140..10ad7f4a4f7 100644 --- a/packages/charts/src/chart_types/xy_chart/renderer/canvas/styles/bar.test.ts +++ b/packages/charts/src/chart_types/xy_chart/renderer/canvas/styles/bar.test.ts @@ -12,6 +12,7 @@ import { colorToRgba } from '../../../../../common/color_library_wrappers'; import type { Fill, Rect, Stroke } from '../../../../../geoms/types'; import { MockStyles } from '../../../../../mocks'; import * as common from '../../../../../utils/common'; +import type { SharedGeometryStateStyle } from '../../../../../utils/themes/theme'; import { getTextureStyles } from '../../../utils/texture'; jest.mock('../../../utils/texture'); @@ -30,6 +31,7 @@ describe('Bar styles', () => { let themeRectStyle = MockStyles.rect(); let themeRectBorderStyle = MockStyles.rectBorder(); let geometryStateStyle = MockStyles.geometryState(); + let sharedStyle: SharedGeometryStateStyle; const rect: Rect = { height: 250, width: 200, @@ -42,30 +44,59 @@ describe('Bar styles', () => { themeRectStyle = MockStyles.rect(); themeRectBorderStyle = MockStyles.rectBorder(); geometryStateStyle = MockStyles.geometryState(); + sharedStyle = { + default: MockStyles.geometryState({ opacity: 1 }), + highlighted: MockStyles.geometryState({ opacity: 0.5 }), + unhighlighted: MockStyles.geometryState({ opacity: 0.25 }), + }; } beforeEach(() => { - result = buildBarStyle(ctx, imgCanvas, baseColor, themeRectStyle, themeRectBorderStyle, geometryStateStyle, rect); + setDefaults(); }); - it('should call getColorFromVariant with correct args for fill', () => { - expect(common.getColorFromVariant).toHaveBeenNthCalledWith(1, baseColor, themeRectStyle.fill); - }); + describe('Default', () => { + beforeEach(() => { + result = buildBarStyle( + ctx, + imgCanvas, + baseColor, + themeRectStyle, + themeRectBorderStyle, + geometryStateStyle, + sharedStyle, + rect, + ); + }); - it('should call getColorFromVariant with correct args for border', () => { - expect(common.getColorFromVariant).toHaveBeenNthCalledWith(1, baseColor, themeRectBorderStyle.stroke); + it('should call getColorFromVariant with correct args for fill', () => { + expect(common.getColorFromVariant).toHaveBeenNthCalledWith(1, baseColor, themeRectStyle.fill); + }); + + it('should call getColorFromVariant with correct args for border', () => { + expect(common.getColorFromVariant).toHaveBeenNthCalledWith(1, baseColor, themeRectBorderStyle.stroke); + }); }); describe('Colors', () => { const fillColor = '#4aefb8'; const strokeColor = '#a740cf'; - beforeAll(() => { - setDefaults(); + beforeEach(() => { (common.getColorFromVariant as jest.Mock).mockImplementation(() => { const { length } = (common.getColorFromVariant as jest.Mock).mock.calls; return length === 1 ? fillColor : strokeColor; }); + result = buildBarStyle( + ctx, + imgCanvas, + baseColor, + themeRectStyle, + themeRectBorderStyle, + geometryStateStyle, + sharedStyle, + rect, + ); }); it('should call colorToRgba with values from getColorFromVariant', () => { @@ -90,16 +121,32 @@ describe('Bar styles', () => { const fillOpacity = 0.6; const strokeOpacity = 0.8; const geoOpacity = 0.75; + let localSharedStyle: SharedGeometryStateStyle; - beforeAll(() => { - setDefaults(); + beforeEach(() => { themeRectStyle = MockStyles.rect({ opacity: fillOpacity }); themeRectBorderStyle = MockStyles.rectBorder({ strokeOpacity }); geometryStateStyle = MockStyles.geometryState({ opacity: geoOpacity }); + // Create a sharedStyle where none of the states match geometryStateStyle + localSharedStyle = { + default: MockStyles.geometryState({ opacity: 1 }), + highlighted: MockStyles.geometryState({ opacity: 0.5 }), + unhighlighted: MockStyles.geometryState({ opacity: 0.25 }), + }; (common.getColorFromVariant as jest.Mock).mockImplementation(() => { const { length } = (common.getColorFromVariant as jest.Mock).mock.calls; return length === 1 ? fillColor : strokeColor; }); + result = buildBarStyle( + ctx, + imgCanvas, + baseColor, + themeRectStyle, + themeRectBorderStyle, + geometryStateStyle, + localSharedStyle, + rect, + ); }); it('should return correct fill opacity', () => { @@ -113,11 +160,21 @@ describe('Bar styles', () => { }); describe('themeRectBorderStyle opacity is undefined', () => { - beforeAll(() => { + beforeEach(() => { themeRectBorderStyle = { ...MockStyles.rectBorder(), strokeOpacity: undefined, }; + result = buildBarStyle( + ctx, + imgCanvas, + baseColor, + themeRectStyle, + themeRectBorderStyle, + geometryStateStyle, + localSharedStyle, + rect, + ); }); it('should use themeRectStyle opacity', () => { @@ -129,8 +186,18 @@ describe('Bar styles', () => { describe('Width', () => { describe('visible is set to false', () => { - beforeAll(() => { + beforeEach(() => { themeRectBorderStyle = MockStyles.rectBorder({ visible: false }); + result = buildBarStyle( + ctx, + imgCanvas, + baseColor, + themeRectStyle, + themeRectBorderStyle, + geometryStateStyle, + sharedStyle, + rect, + ); }); it('should set stroke width to zero', () => { @@ -141,8 +208,18 @@ describe('Bar styles', () => { describe('visible is set to true', () => { const strokeWidth = 22; - beforeAll(() => { + beforeEach(() => { themeRectBorderStyle = MockStyles.rectBorder({ visible: true, strokeWidth }); + result = buildBarStyle( + ctx, + imgCanvas, + baseColor, + themeRectStyle, + themeRectBorderStyle, + geometryStateStyle, + sharedStyle, + rect, + ); }); it('should set stroke width to strokeWidth', () => { @@ -155,10 +232,19 @@ describe('Bar styles', () => { const texture = {}; const mockTexture = {}; - beforeAll(() => { - setDefaults(); + beforeEach(() => { themeRectStyle = MockStyles.rect({ texture }); (getTextureStyles as jest.Mock).mockReturnValue(mockTexture); + result = buildBarStyle( + ctx, + imgCanvas, + baseColor, + themeRectStyle, + themeRectBorderStyle, + geometryStateStyle, + sharedStyle, + rect, + ); }); it('should return correct texture', () => { diff --git a/packages/charts/src/chart_types/xy_chart/renderer/canvas/styles/bar.ts b/packages/charts/src/chart_types/xy_chart/renderer/canvas/styles/bar.ts index 2960d3a293a..ccbd5d1212e 100644 --- a/packages/charts/src/chart_types/xy_chart/renderer/canvas/styles/bar.ts +++ b/packages/charts/src/chart_types/xy_chart/renderer/canvas/styles/bar.ts @@ -9,7 +9,13 @@ import { colorToRgba, overrideOpacity } from '../../../../../common/color_library_wrappers'; import type { Stroke, Fill, Rect } from '../../../../../geoms/types'; import { getColorFromVariant } from '../../../../../utils/common'; -import type { GeometryStateStyle, RectStyle, RectBorderStyle } from '../../../../../utils/themes/theme'; +import { getDimmedColor, hasDimmedColor } from '../../../../../utils/themes/dimmed_colors'; +import type { + GeometryStateStyle, + RectStyle, + RectBorderStyle, + SharedGeometryStateStyle, +} from '../../../../../utils/themes/theme'; import { getTextureStyles } from '../../../utils/texture'; /** @@ -27,14 +33,36 @@ export function buildBarStyle( themeRectStyle: RectStyle, themeRectBorderStyle: RectBorderStyle, geometryStateStyle: GeometryStateStyle, + sharedStyle: SharedGeometryStateStyle, rect: Rect, ): { fill: Fill; stroke: Stroke } { + // Check if dimmed by comparing to the unhighlighted style reference + const isDimmed = geometryStateStyle === sharedStyle.unhighlighted; + const fillVariant = getDimmedColor(isDimmed, themeRectStyle.dimmed, 'fill', themeRectStyle.fill); + const hasDimmedFillColor = hasDimmedColor(isDimmed, themeRectStyle.dimmed, 'fill'); + + // When dimmed with opacity config, use the configured opacity; when dimmed with fill color, use full opacity + const dimmedOpacity = + isDimmed && 'opacity' in themeRectStyle.dimmed + ? themeRectStyle.dimmed.opacity + : hasDimmedFillColor + ? 1 + : geometryStateStyle.opacity; + + const fillBaseColor = getColorFromVariant(baseColor, fillVariant); + + const textureOpacity = + isDimmed && themeRectStyle.dimmed && 'texture' in themeRectStyle.dimmed && themeRectStyle.dimmed.texture + ? themeRectStyle.dimmed.texture.opacity + : geometryStateStyle.opacity; + const texture = themeRectStyle.texture - ? getTextureStyles(ctx, imgCanvas, baseColor, geometryStateStyle.opacity, themeRectStyle.texture) + ? getTextureStyles(ctx, imgCanvas, baseColor, textureOpacity, themeRectStyle.texture) : undefined; + const fillColor = overrideOpacity( - colorToRgba(getColorFromVariant(baseColor, themeRectStyle.fill)), - (opacity) => opacity * themeRectStyle.opacity * geometryStateStyle.opacity, + colorToRgba(fillBaseColor), + (opacity) => opacity * themeRectStyle.opacity * dimmedOpacity, ); const fill: Fill = { color: fillColor, diff --git a/packages/charts/src/chart_types/xy_chart/renderer/canvas/styles/line.ts b/packages/charts/src/chart_types/xy_chart/renderer/canvas/styles/line.ts index b4b233bcb69..d9dfb512d75 100644 --- a/packages/charts/src/chart_types/xy_chart/renderer/canvas/styles/line.ts +++ b/packages/charts/src/chart_types/xy_chart/renderer/canvas/styles/line.ts @@ -10,6 +10,7 @@ import { colorToRgba, overrideOpacity } from '../../../../../common/color_librar import type { Stroke } from '../../../../../geoms/types'; import { getColorFromVariant } from '../../../../../utils/common'; import type { GeometryHighlightState } from '../../../../../utils/geometry'; +import { getDimmedColor } from '../../../../../utils/themes/dimmed_colors'; import type { LineStyle } from '../../../../../utils/themes/theme'; /** @@ -28,7 +29,7 @@ export function buildLineStyles( const isDimmed = highlightState === 'dimmed'; const isFocused = highlightState === 'focused'; - const strokeColor = isDimmed && 'stroke' in themeLineStyle.dimmed ? themeLineStyle.dimmed.stroke : seriesColor; + const strokeColor = getDimmedColor(isDimmed, themeLineStyle.dimmed, 'stroke', seriesColor); const opacity = isDimmed && 'opacity' in themeLineStyle.dimmed ? themeLineStyle.dimmed.opacity * themeLineStyle.opacity diff --git a/packages/charts/src/chart_types/xy_chart/rendering/__snapshots__/rendering.bands.test.ts.snap b/packages/charts/src/chart_types/xy_chart/rendering/__snapshots__/rendering.bands.test.ts.snap index 215e8f2df60..5cf95329c41 100644 --- a/packages/charts/src/chart_types/xy_chart/rendering/__snapshots__/rendering.bands.test.ts.snap +++ b/packages/charts/src/chart_types/xy_chart/rendering/__snapshots__/rendering.bands.test.ts.snap @@ -639,6 +639,12 @@ exports[`Rendering bands - areas Single series band bar chart - ordinal Can rend "padding": 0, }, "rect": { + "dimmed": { + "fill": "#ECF1F9", + "texture": { + "opacity": 0.25, + }, + }, "opacity": 1, }, "rectBorder": { @@ -702,6 +708,12 @@ exports[`Rendering bands - areas Single series band bar chart - ordinal Can rend "padding": 0, }, "rect": { + "dimmed": { + "fill": "#ECF1F9", + "texture": { + "opacity": 0.25, + }, + }, "opacity": 1, }, "rectBorder": { @@ -765,6 +777,12 @@ exports[`Rendering bands - areas Single series band bar chart - ordinal Can rend "padding": 0, }, "rect": { + "dimmed": { + "fill": "#ECF1F9", + "texture": { + "opacity": 0.25, + }, + }, "opacity": 1, }, "rectBorder": { diff --git a/packages/charts/src/chart_types/xy_chart/rendering/__snapshots__/rendering.bars.test.ts.snap b/packages/charts/src/chart_types/xy_chart/rendering/__snapshots__/rendering.bars.test.ts.snap index 55d0ca0da7f..0f8e4e5bdf5 100644 --- a/packages/charts/src/chart_types/xy_chart/rendering/__snapshots__/rendering.bars.test.ts.snap +++ b/packages/charts/src/chart_types/xy_chart/rendering/__snapshots__/rendering.bars.test.ts.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`Rendering bars Multi series bar chart - ordinal can render first spec bars 1`] = ` [ @@ -39,6 +39,12 @@ exports[`Rendering bars Multi series bar chart - ordinal can render first spec b "padding": 0, }, "rect": { + "dimmed": { + "fill": "#ECF1F9", + "texture": { + "opacity": 0.25, + }, + }, "opacity": 1, }, "rectBorder": { @@ -101,6 +107,12 @@ exports[`Rendering bars Multi series bar chart - ordinal can render first spec b "padding": 0, }, "rect": { + "dimmed": { + "fill": "#ECF1F9", + "texture": { + "opacity": 0.25, + }, + }, "opacity": 1, }, "rectBorder": { @@ -168,6 +180,12 @@ exports[`Rendering bars Multi series bar chart - ordinal can render second spec "padding": 0, }, "rect": { + "dimmed": { + "fill": "#ECF1F9", + "texture": { + "opacity": 0.25, + }, + }, "opacity": 1, }, "rectBorder": { @@ -230,6 +248,12 @@ exports[`Rendering bars Multi series bar chart - ordinal can render second spec "padding": 0, }, "rect": { + "dimmed": { + "fill": "#ECF1F9", + "texture": { + "opacity": 0.25, + }, + }, "opacity": 1, }, "rectBorder": { @@ -300,6 +324,12 @@ IndexedGeometryMap { "padding": 0, }, "rect": { + "dimmed": { + "fill": "#ECF1F9", + "texture": { + "opacity": 0.25, + }, + }, "opacity": 1, }, "rectBorder": { @@ -363,6 +393,12 @@ IndexedGeometryMap { "padding": 0, }, "rect": { + "dimmed": { + "fill": "#ECF1F9", + "texture": { + "opacity": 0.25, + }, + }, "opacity": 1, }, "rectBorder": { @@ -428,6 +464,12 @@ IndexedGeometryMap { "padding": 0, }, "rect": { + "dimmed": { + "fill": "#ECF1F9", + "texture": { + "opacity": 0.25, + }, + }, "opacity": 1, }, "rectBorder": { @@ -491,6 +533,12 @@ IndexedGeometryMap { "padding": 0, }, "rect": { + "dimmed": { + "fill": "#ECF1F9", + "texture": { + "opacity": 0.25, + }, + }, "opacity": 1, }, "rectBorder": { @@ -556,6 +604,12 @@ IndexedGeometryMap { "padding": 0, }, "rect": { + "dimmed": { + "fill": "#ECF1F9", + "texture": { + "opacity": 0.25, + }, + }, "opacity": 1, }, "rectBorder": { @@ -619,6 +673,12 @@ IndexedGeometryMap { "padding": 0, }, "rect": { + "dimmed": { + "fill": "#ECF1F9", + "texture": { + "opacity": 0.25, + }, + }, "opacity": 1, }, "rectBorder": { @@ -684,6 +744,12 @@ IndexedGeometryMap { "padding": 0, }, "rect": { + "dimmed": { + "fill": "#ECF1F9", + "texture": { + "opacity": 0.25, + }, + }, "opacity": 1, }, "rectBorder": { @@ -747,6 +813,12 @@ IndexedGeometryMap { "padding": 0, }, "rect": { + "dimmed": { + "fill": "#ECF1F9", + "texture": { + "opacity": 0.25, + }, + }, "opacity": 1, }, "rectBorder": { @@ -812,6 +884,12 @@ IndexedGeometryMap { "padding": 0, }, "rect": { + "dimmed": { + "fill": "#ECF1F9", + "texture": { + "opacity": 0.25, + }, + }, "opacity": 1, }, "rectBorder": { @@ -875,6 +953,12 @@ IndexedGeometryMap { "padding": 0, }, "rect": { + "dimmed": { + "fill": "#ECF1F9", + "texture": { + "opacity": 0.25, + }, + }, "opacity": 1, }, "rectBorder": { @@ -940,6 +1024,12 @@ IndexedGeometryMap { "padding": 0, }, "rect": { + "dimmed": { + "fill": "#ECF1F9", + "texture": { + "opacity": 0.25, + }, + }, "opacity": 1, }, "rectBorder": { @@ -1003,6 +1093,12 @@ IndexedGeometryMap { "padding": 0, }, "rect": { + "dimmed": { + "fill": "#ECF1F9", + "texture": { + "opacity": 0.25, + }, + }, "opacity": 1, }, "rectBorder": { @@ -1068,6 +1164,12 @@ IndexedGeometryMap { "padding": 0, }, "rect": { + "dimmed": { + "fill": "#ECF1F9", + "texture": { + "opacity": 0.25, + }, + }, "opacity": 1, }, "rectBorder": { @@ -1131,6 +1233,12 @@ IndexedGeometryMap { "padding": 0, }, "rect": { + "dimmed": { + "fill": "#ECF1F9", + "texture": { + "opacity": 0.25, + }, + }, "opacity": 1, }, "rectBorder": { @@ -1196,6 +1304,12 @@ IndexedGeometryMap { "padding": 0, }, "rect": { + "dimmed": { + "fill": "#ECF1F9", + "texture": { + "opacity": 0.25, + }, + }, "opacity": 1, }, "rectBorder": { @@ -1259,6 +1373,12 @@ IndexedGeometryMap { "padding": 0, }, "rect": { + "dimmed": { + "fill": "#ECF1F9", + "texture": { + "opacity": 0.25, + }, + }, "opacity": 1, }, "rectBorder": { @@ -1324,6 +1444,12 @@ IndexedGeometryMap { "padding": 0, }, "rect": { + "dimmed": { + "fill": "#ECF1F9", + "texture": { + "opacity": 0.25, + }, + }, "opacity": 1, }, "rectBorder": { @@ -1387,6 +1513,12 @@ IndexedGeometryMap { "padding": 0, }, "rect": { + "dimmed": { + "fill": "#ECF1F9", + "texture": { + "opacity": 0.25, + }, + }, "opacity": 1, }, "rectBorder": { @@ -1452,6 +1584,12 @@ IndexedGeometryMap { "padding": 0, }, "rect": { + "dimmed": { + "fill": "#ECF1F9", + "texture": { + "opacity": 0.25, + }, + }, "opacity": 1, }, "rectBorder": { @@ -1515,6 +1653,12 @@ IndexedGeometryMap { "padding": 0, }, "rect": { + "dimmed": { + "fill": "#ECF1F9", + "texture": { + "opacity": 0.25, + }, + }, "opacity": 1, }, "rectBorder": { @@ -1580,6 +1724,12 @@ IndexedGeometryMap { "padding": 0, }, "rect": { + "dimmed": { + "fill": "#ECF1F9", + "texture": { + "opacity": 0.25, + }, + }, "opacity": 1, }, "rectBorder": { @@ -1643,6 +1793,12 @@ IndexedGeometryMap { "padding": 0, }, "rect": { + "dimmed": { + "fill": "#ECF1F9", + "texture": { + "opacity": 0.25, + }, + }, "opacity": 1, }, "rectBorder": { @@ -1708,6 +1864,12 @@ IndexedGeometryMap { "padding": 0, }, "rect": { + "dimmed": { + "fill": "#ECF1F9", + "texture": { + "opacity": 0.25, + }, + }, "opacity": 1, }, "rectBorder": { @@ -1771,6 +1933,12 @@ IndexedGeometryMap { "padding": 0, }, "rect": { + "dimmed": { + "fill": "#ECF1F9", + "texture": { + "opacity": 0.25, + }, + }, "opacity": 1, }, "rectBorder": { @@ -1836,6 +2004,12 @@ IndexedGeometryMap { "padding": 0, }, "rect": { + "dimmed": { + "fill": "#ECF1F9", + "texture": { + "opacity": 0.25, + }, + }, "opacity": 1, }, "rectBorder": { @@ -1899,6 +2073,12 @@ IndexedGeometryMap { "padding": 0, }, "rect": { + "dimmed": { + "fill": "#ECF1F9", + "texture": { + "opacity": 0.25, + }, + }, "opacity": 1, }, "rectBorder": { @@ -1964,6 +2144,12 @@ IndexedGeometryMap { "padding": 0, }, "rect": { + "dimmed": { + "fill": "#ECF1F9", + "texture": { + "opacity": 0.25, + }, + }, "opacity": 1, }, "rectBorder": { @@ -2027,6 +2213,12 @@ IndexedGeometryMap { "padding": 0, }, "rect": { + "dimmed": { + "fill": "#ECF1F9", + "texture": { + "opacity": 0.25, + }, + }, "opacity": 1, }, "rectBorder": { @@ -2092,6 +2284,12 @@ IndexedGeometryMap { "padding": 0, }, "rect": { + "dimmed": { + "fill": "#ECF1F9", + "texture": { + "opacity": 0.25, + }, + }, "opacity": 1, }, "rectBorder": { @@ -2155,6 +2353,12 @@ IndexedGeometryMap { "padding": 0, }, "rect": { + "dimmed": { + "fill": "#ECF1F9", + "texture": { + "opacity": 0.25, + }, + }, "opacity": 1, }, "rectBorder": { @@ -2220,6 +2424,12 @@ IndexedGeometryMap { "padding": 0, }, "rect": { + "dimmed": { + "fill": "#ECF1F9", + "texture": { + "opacity": 0.25, + }, + }, "opacity": 1, }, "rectBorder": { @@ -2283,6 +2493,12 @@ IndexedGeometryMap { "padding": 0, }, "rect": { + "dimmed": { + "fill": "#ECF1F9", + "texture": { + "opacity": 0.25, + }, + }, "opacity": 1, }, "rectBorder": { @@ -2348,6 +2564,12 @@ IndexedGeometryMap { "padding": 0, }, "rect": { + "dimmed": { + "fill": "#ECF1F9", + "texture": { + "opacity": 0.25, + }, + }, "opacity": 1, }, "rectBorder": { @@ -2411,6 +2633,12 @@ IndexedGeometryMap { "padding": 0, }, "rect": { + "dimmed": { + "fill": "#ECF1F9", + "texture": { + "opacity": 0.25, + }, + }, "opacity": 1, }, "rectBorder": { @@ -2476,6 +2704,12 @@ IndexedGeometryMap { "padding": 0, }, "rect": { + "dimmed": { + "fill": "#ECF1F9", + "texture": { + "opacity": 0.25, + }, + }, "opacity": 1, }, "rectBorder": { @@ -2539,6 +2773,12 @@ IndexedGeometryMap { "padding": 0, }, "rect": { + "dimmed": { + "fill": "#ECF1F9", + "texture": { + "opacity": 0.25, + }, + }, "opacity": 1, }, "rectBorder": { @@ -2617,6 +2857,12 @@ exports[`Rendering bars should render two bars within domain 1`] = ` "padding": 0, }, "rect": { + "dimmed": { + "fill": "#ECF1F9", + "texture": { + "opacity": 0.25, + }, + }, "opacity": 1, }, "rectBorder": { @@ -2679,6 +2925,12 @@ exports[`Rendering bars should render two bars within domain 1`] = ` "padding": 0, }, "rect": { + "dimmed": { + "fill": "#ECF1F9", + "texture": { + "opacity": 0.25, + }, + }, "opacity": 1, }, "rectBorder": { diff --git a/packages/charts/src/chart_types/xy_chart/rendering/rendering.test.ts b/packages/charts/src/chart_types/xy_chart/rendering/rendering.test.ts index 1b4398c291a..a180a6943d7 100644 --- a/packages/charts/src/chart_types/xy_chart/rendering/rendering.test.ts +++ b/packages/charts/src/chart_types/xy_chart/rendering/rendering.test.ts @@ -194,6 +194,7 @@ describe('Rendering utils', () => { const sampleSeriesStyle: BarSeriesStyle = { rect: { opacity: 1, + dimmed: { opacity: 0.25 }, }, rectBorder: { visible: true, diff --git a/packages/charts/src/common/color_library_wrappers.ts b/packages/charts/src/common/color_library_wrappers.ts index 9e264db5fed..e5b207e7ef7 100644 --- a/packages/charts/src/common/color_library_wrappers.ts +++ b/packages/charts/src/common/color_library_wrappers.ts @@ -41,6 +41,22 @@ export function overrideOpacity([r, g, b, o]: RgbaTuple, opacity?: number | Opac return [r, g, b, clamp(Number.isFinite(opacityOverride) ? opacityOverride : o, 0, 1)]; } +/** + * Multiply a color's opacity by a given multiplier. + * @internal + */ +export function multiplyOpacity(rgba: RgbaTuple, opacityMultiplier: number): RgbaTuple { + return overrideOpacity(rgba, (opacity) => opacity * opacityMultiplier); +} + +/** + * Multiply a CSS color's opacity by a given multiplier. + * @internal + */ +export function multiplyColorOpacity(color: Color, opacityMultiplier: number): Color { + return RGBATupleToString(multiplyOpacity(colorToRgba(color), opacityMultiplier)); +} + /** @internal */ export function RGBATupleToString(rgba: RgbTuple): Color { return `rgba(${rgba[0]}, ${rgba[1]}, ${rgba[2]}, ${rgba[3] ?? 1})`; diff --git a/packages/charts/src/index.ts b/packages/charts/src/index.ts index cc7d6ab4980..f0dbc554267 100644 --- a/packages/charts/src/index.ts +++ b/packages/charts/src/index.ts @@ -50,7 +50,7 @@ export { LegendPath, LegendPathElement } from './state/actions/legend'; export { LegendItemValue, LegendValue } from './common/legend'; export { CategoryKey, CategoryLabel } from './common/category'; export { Layer as PartitionLayer, PartitionProps } from './chart_types/partition_chart/specs'; -export { FillLabelConfig as PartitionFillLabel, PartitionStyle } from './utils/themes/partition'; +export { FillLabelConfig as PartitionFillLabel, PartitionDimmedStyle, PartitionStyle } from './utils/themes/partition'; export { PartitionLayout } from './chart_types/partition_chart/layout/types/config_types'; export { diff --git a/packages/charts/src/mocks/theme.ts b/packages/charts/src/mocks/theme.ts index 6bf92c523fe..2e4abead2e9 100644 --- a/packages/charts/src/mocks/theme.ts +++ b/packages/charts/src/mocks/theme.ts @@ -28,7 +28,7 @@ import type { /** @internal */ export class MockStyles { static rect(partial: RecursivePartial = {}): RectStyle { - return mergePartial({ fill: 'blue', opacity: 1 }, partial); + return mergePartial({ fill: 'blue', opacity: 1, dimmed: { opacity: 0.25 } }, partial); } static rectBorder(partial: RecursivePartial = {}): RectBorderStyle { diff --git a/packages/charts/src/utils/themes/amsterdam_dark_theme.ts b/packages/charts/src/utils/themes/amsterdam_dark_theme.ts index c477c48c55e..c7e7d9edd91 100644 --- a/packages/charts/src/utils/themes/amsterdam_dark_theme.ts +++ b/packages/charts/src/utils/themes/amsterdam_dark_theme.ts @@ -116,6 +116,7 @@ export const AMSTERDAM_DARK_THEME: Theme = { barSeriesStyle: { rect: { opacity: 1, + dimmed: { opacity: 0.25 }, }, rectBorder: { visible: false, @@ -350,6 +351,7 @@ export const AMSTERDAM_DARK_THEME: Theme = { }, sectorLineWidth: 1.5, sectorLineStroke: '#1D1E24', + dimmed: { opacity: 0.25 }, }, heatmap: { brushArea: { diff --git a/packages/charts/src/utils/themes/amsterdam_light_theme.ts b/packages/charts/src/utils/themes/amsterdam_light_theme.ts index 7c4f8ebf2e1..e48b6e4620c 100644 --- a/packages/charts/src/utils/themes/amsterdam_light_theme.ts +++ b/packages/charts/src/utils/themes/amsterdam_light_theme.ts @@ -116,6 +116,7 @@ export const AMSTERDAM_LIGHT_THEME: Theme = { barSeriesStyle: { rect: { opacity: 1, + dimmed: { opacity: 0.25 }, }, rectBorder: { visible: false, @@ -351,6 +352,7 @@ export const AMSTERDAM_LIGHT_THEME: Theme = { }, sectorLineWidth: 1.5, sectorLineStroke: '#FFF', + dimmed: { opacity: 0.25 }, }, heatmap: { brushArea: { diff --git a/packages/charts/src/utils/themes/base_colors.ts b/packages/charts/src/utils/themes/base_colors.ts index 12698319ef2..f9fb76c22e9 100644 --- a/packages/charts/src/utils/themes/base_colors.ts +++ b/packages/charts/src/utils/themes/base_colors.ts @@ -29,6 +29,7 @@ export const PRIMITIVE_COLORS = { blueGrey90: '#5A6D8C', blueGrey95: '#516381', blueGrey100: '#485975', + blueGrey110: '#384861', blueGrey120: '#2B394F', blueGrey130: '#1D2A3E', blueGrey140: '#111C2C', @@ -58,6 +59,7 @@ export const SEMANTIC_COLORS = { shade90: PRIMITIVE_COLORS.blueGrey90, shade95: PRIMITIVE_COLORS.blueGrey95, shade100: PRIMITIVE_COLORS.blueGrey100, + shade110: PRIMITIVE_COLORS.blueGrey110, shade120: PRIMITIVE_COLORS.blueGrey120, shade130: PRIMITIVE_COLORS.blueGrey130, shade140: PRIMITIVE_COLORS.blueGrey140, @@ -209,6 +211,9 @@ export const LIGHT_DIMMED_COLORS = { areaStroke: SEMANTIC_ALPHA_COLORS.shade30RGBAlpha50, areaPointStroke: SEMANTIC_ALPHA_COLORS.shade30RGBAlpha15, areaPointFill: SEMANTIC_COLORS.plainLight, + + barFill: SEMANTIC_COLORS.shade15, + partitionFill: SEMANTIC_COLORS.shade15, }; /** @internal */ @@ -221,4 +226,7 @@ export const DARK_DIMMED_COLORS = { areaStroke: SEMANTIC_ALPHA_COLORS.shade60RGBAlpha35, areaPointStroke: SEMANTIC_ALPHA_COLORS.shade60RGBAlpha15, areaPointFill: SEMANTIC_COLORS.shade145, + + barFill: SEMANTIC_COLORS.shade110, + partitionFill: SEMANTIC_COLORS.shade110, }; diff --git a/packages/charts/src/utils/themes/dark_theme.ts b/packages/charts/src/utils/themes/dark_theme.ts index d65331b78a7..f859b1706e1 100644 --- a/packages/charts/src/utils/themes/dark_theme.ts +++ b/packages/charts/src/utils/themes/dark_theme.ts @@ -139,6 +139,10 @@ export const DARK_THEME: Theme = { barSeriesStyle: { rect: { opacity: 1, + dimmed: { + fill: DARK_DIMMED_COLORS.barFill, + texture: { opacity: 0.25 }, + }, }, rectBorder: { visible: false, @@ -373,6 +377,9 @@ export const DARK_THEME: Theme = { }, sectorLineWidth: 1.5, sectorLineStroke: DARK_BACKGROUND_COLORS.backgroundBasePlain, + dimmed: { + fill: DARK_DIMMED_COLORS.partitionFill, + }, }, heatmap: { brushArea: { diff --git a/packages/charts/src/utils/themes/dimmed_colors.ts b/packages/charts/src/utils/themes/dimmed_colors.ts new file mode 100644 index 00000000000..6dbd9f260fa --- /dev/null +++ b/packages/charts/src/utils/themes/dimmed_colors.ts @@ -0,0 +1,114 @@ +/* + * 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 type { Color } from '../../common/colors'; +import type { ColorVariant } from '../common'; + +/** + * A color value that can be used for dimmed styling. + * @internal + */ +type DimmedColor = Color | ColorVariant; + +/** + * Opacity-only dimmed styling configuration. + * When used, elements are dimmed by reducing opacity while keeping their original color. + * @internal + */ +type OpacityDimmedConfig = { opacity: number }; + +/** + * Color override dimmed styling configuration. + * When used, elements are dimmed by replacing their color with specific dimmed colors. + * @internal + */ +type ColorDimmedConfig = { + fill?: DimmedColor; + stroke?: DimmedColor; +}; + +/** + * Configuration for dimmed (unhighlighted) element styling. + * + * The dimmed style can be configured in two ways: + * - **Opacity-only** (`OpacityDimmedConfig`): Reduces element opacity while keeping original color + * - **Color override** (`ColorDimmedConfig`): Uses specific fill/stroke colors for the dimmed state + * @internal + */ +type DimmedStyleConfig = OpacityDimmedConfig | ColorDimmedConfig; + +/** + * Type guard: checks if config uses color overrides (has fill or stroke, no opacity). + * @internal + */ +function isColorDimmedConfig(config: DimmedStyleConfig): config is ColorDimmedConfig { + return !('opacity' in config); +} + +/** + * Checks if a dimmed color is configured for the specified color key. + * + * Use this when you need to know whether a dimmed color was explicitly configured, + * for example to determine opacity behavior (use full opacity when dimmed color exists). + * + * @param isDimmed - Whether the element is in a dimmed/unhighlighted state + * @param dimmedConfig - The dimmed style configuration from theme + * @param colorKey - Which color property to check ('fill' or 'stroke') + * @returns True if a dimmed color is configured for the specified key + * + * @example + * const hasDimmedFill = hasDimmedColor(isDimmed, rectStyle.dimmed, 'fill'); + * const opacity = hasDimmedFill ? 1 : geometryStateStyle.opacity; + * + * @internal + */ +export function hasDimmedColor( + isDimmed: boolean, + dimmedConfig: DimmedStyleConfig, + colorKey: 'fill' | 'stroke', +): boolean { + return isDimmed && isColorDimmedConfig(dimmedConfig) && dimmedConfig[colorKey] !== undefined; +} + +/** + * Resolves the color to use based on highlight state and dimmed configuration. + * + * When an element is dimmed and the theme provides a specific dimmed color, + * returns that color. Otherwise, returns the default color. + * + * @param isDimmed - Whether the element is in a dimmed/unhighlighted state + * @param dimmedConfig - The dimmed style configuration from theme + * @param colorKey - Which color property to extract ('fill' or 'stroke') + * @param defaultColor - Fallback color when not dimmed or no dimmed color configured + * @returns The dimmed color if configured and applicable, otherwise the default color + * + * @example + * // Always get a color (dimmed or series color): + * const stroke = getDimmedColor(isDimmed, lineStyle.dimmed, 'stroke', seriesColor); + * + * @example + * // Get dimmed color only if configured (for optional override): + * const dimmedFill = getDimmedColor(isDimmed, pointStyle.dimmed, 'fill', undefined); + * const fill = dimmedFill ?? originalFill; + * + * @internal + */ +export function getDimmedColor( + isDimmed: boolean, + dimmedConfig: DimmedStyleConfig, + colorKey: 'fill' | 'stroke', + defaultColor: D, +): DimmedColor | D { + if (isDimmed && isColorDimmedConfig(dimmedConfig)) { + const dimmedColor = dimmedConfig[colorKey]; + if (dimmedColor !== undefined) { + return dimmedColor; + } + } + return defaultColor; +} diff --git a/packages/charts/src/utils/themes/legacy_dark_theme.ts b/packages/charts/src/utils/themes/legacy_dark_theme.ts index 40d2da9a8f6..1ce9818637c 100644 --- a/packages/charts/src/utils/themes/legacy_dark_theme.ts +++ b/packages/charts/src/utils/themes/legacy_dark_theme.ts @@ -122,6 +122,7 @@ export const LEGACY_DARK_THEME: Theme = { barSeriesStyle: { rect: { opacity: 1, + dimmed: { opacity: 0.25 }, }, rectBorder: { visible: false, @@ -354,6 +355,7 @@ export const LEGACY_DARK_THEME: Theme = { }, sectorLineWidth: 1, sectorLineStroke: Colors.Black.keyword, + dimmed: { opacity: 0.25 }, }, heatmap: { brushArea: { diff --git a/packages/charts/src/utils/themes/legacy_light_theme.ts b/packages/charts/src/utils/themes/legacy_light_theme.ts index 03ba2115374..3d2ac8221cc 100644 --- a/packages/charts/src/utils/themes/legacy_light_theme.ts +++ b/packages/charts/src/utils/themes/legacy_light_theme.ts @@ -122,6 +122,7 @@ export const LEGACY_LIGHT_THEME: Theme = { barSeriesStyle: { rect: { opacity: 1, + dimmed: { opacity: 0.25 }, }, rectBorder: { visible: false, @@ -354,6 +355,7 @@ export const LEGACY_LIGHT_THEME: Theme = { }, sectorLineWidth: 1, sectorLineStroke: 'white', + dimmed: { opacity: 0.25 }, }, heatmap: { brushArea: { diff --git a/packages/charts/src/utils/themes/light_theme.ts b/packages/charts/src/utils/themes/light_theme.ts index 80311e79df0..2366a8d82fe 100644 --- a/packages/charts/src/utils/themes/light_theme.ts +++ b/packages/charts/src/utils/themes/light_theme.ts @@ -139,6 +139,10 @@ export const LIGHT_THEME: Theme = { barSeriesStyle: { rect: { opacity: 1, + dimmed: { + fill: LIGHT_DIMMED_COLORS.barFill, + texture: { opacity: 0.25 }, + }, }, rectBorder: { visible: false, @@ -373,6 +377,9 @@ export const LIGHT_THEME: Theme = { }, sectorLineWidth: 1.5, sectorLineStroke: LIGHT_BACKGROUND_COLORS.backgroundBasePlain, + dimmed: { + fill: LIGHT_DIMMED_COLORS.partitionFill, + }, }, heatmap: { brushArea: { diff --git a/packages/charts/src/utils/themes/partition.ts b/packages/charts/src/utils/themes/partition.ts index 2124d4aaeae..6680791c713 100644 --- a/packages/charts/src/utils/themes/partition.ts +++ b/packages/charts/src/utils/themes/partition.ts @@ -12,6 +12,14 @@ import type { Font, PartialFont, FontFamily } from '../../common/text_utils'; import type { ColorVariant, StrokeStyle } from '../common'; import type { PerSideDistance } from '../dimensions'; +/** @public */ +export type PartitionDimmedStyle = + | { opacity: number } + | { + /** The fill color to use when partition slices are dimmed. */ + fill: Color | ColorVariant; + }; + interface LabelConfig extends Font { textColor: Color | typeof ColorVariant.Adaptive; valueFont: PartialFont; @@ -82,4 +90,9 @@ export interface PartitionStyle extends FillFontSizeRange { linkLabel: LinkLabelConfig; sectorLineWidth: Pixels; sectorLineStroke: StrokeStyle; + /** + * The style applied to partition slices when they are dimmed relative to other highlighted elements. + * This is typically used to visually de-emphasize slices when hovering over a legend item. + */ + dimmed: PartitionDimmedStyle; } diff --git a/packages/charts/src/utils/themes/theme.ts b/packages/charts/src/utils/themes/theme.ts index 9cce082fa1b..11753664e83 100644 --- a/packages/charts/src/utils/themes/theme.ts +++ b/packages/charts/src/utils/themes/theme.ts @@ -754,6 +754,18 @@ export interface RectStyle { widthRatio?: Ratio; /** applying textures to the bar on the theme/series */ texture?: TexturedStyles; + /** + * The style applied to the rect when it is dimmed relative to other highlighted elements on the chart. + * This is typically used to visually de-emphasize the rect, for example, when another series is highlighted. + */ + dimmed: + | { opacity: number } + | { + /** The fill color to use when the rect is dimmed. */ + fill: Color | ColorVariant; + /** The opacity multiplier for the texture color when the rect is dimmed */ + texture: { opacity: number }; + }; } /** @public */ diff --git a/storybook/stories/icicle/01_unix_icicle.story.tsx b/storybook/stories/icicle/01_unix_icicle.story.tsx index 8fa3aeb43ba..7e5559b73c0 100644 --- a/storybook/stories/icicle/01_unix_icicle.story.tsx +++ b/storybook/stories/icicle/01_unix_icicle.story.tsx @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { boolean } from '@storybook/addon-knobs'; import React from 'react'; import type { Datum } from '@elastic/charts'; @@ -19,6 +20,9 @@ import { viridis18 as palette } from '../utils/utils'; const color = palette.slice().reverse(); export const Example: ChartsStory = (_, { title, description }) => { + const clipText = boolean('Clip text (use linear renderer)', false); + const useOpacityOnlyDimming = boolean('Use opacity-only dimming', false); + return ( { partition: { minFontSize: 6, maxFontSize: 10, + ...(clipText ? { fillLabel: { clipText } } : {}), + ...(useOpacityOnlyDimming ? { dimmed: { opacity: 0.25 } } : {}), }, }} /> diff --git a/storybook/stories/icicle/02_unix_flame.story.tsx b/storybook/stories/icicle/02_unix_flame.story.tsx index 83b9213ff9c..fe70cc88c88 100644 --- a/storybook/stories/icicle/02_unix_flame.story.tsx +++ b/storybook/stories/icicle/02_unix_flame.story.tsx @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { boolean } from '@storybook/addon-knobs'; import React from 'react'; import type { Datum } from '@elastic/charts'; @@ -19,6 +20,9 @@ import { plasma18 as palette } from '../utils/utils'; const color = [...palette].reverse(); export const Example: ChartsStory = (_, { title, description }) => { + const clipText = boolean('Clip text (use linear renderer)', false); + const useOpacityOnlyDimming = boolean('Use opacity-only dimming', false); + return ( { partition: { minFontSize: 6, maxFontSize: 10, + ...(clipText ? { fillLabel: { clipText } } : {}), + ...(useOpacityOnlyDimming ? { dimmed: { opacity: 0.25 } } : {}), }, }} /> diff --git a/storybook/stories/interactions/4_line_area_bar_clicks.story.tsx b/storybook/stories/interactions/4_line_area_bar_clicks.story.tsx index ecf28134046..9bab3695fef 100644 --- a/storybook/stories/interactions/4_line_area_bar_clicks.story.tsx +++ b/storybook/stories/interactions/4_line_area_bar_clicks.story.tsx @@ -43,7 +43,8 @@ export const Example: ChartsStory = (_, { title, description }) => ( Number(d).toFixed(2)} /> ( { x: 3, y: 8 }, ]} /> + + ( { x: 3, y: 6 }, ]} /> + ( { x: 1, y: 5, g: 'b' }, { x: 2, y: 8, g: 'b' }, { x: 3, y: 2, g: 'b' }, + { x: 0, y: 3, g: 'c' }, + { x: 1, y: 6, g: 'c' }, + { x: 2, y: 4, g: 'c' }, + { x: 3, y: 5, g: 'c' }, + { x: 0, y: 5, g: 'd' }, + { x: 1, y: 3, g: 'd' }, + { x: 2, y: 7, g: 'd' }, + { x: 3, y: 4, g: 'd' }, + { x: 0, y: 1, g: 'e' }, + { x: 1, y: 4, g: 'e' }, + { x: 2, y: 2, g: 'e' }, + { x: 3, y: 3, g: 'e' }, ]} /> diff --git a/storybook/stories/stylings/27_dimmed_highlight_style.story.tsx b/storybook/stories/stylings/27_dimmed_highlight_style.story.tsx new file mode 100644 index 00000000000..e22ed83e78f --- /dev/null +++ b/storybook/stories/stylings/27_dimmed_highlight_style.story.tsx @@ -0,0 +1,478 @@ +/* + * 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 { boolean, select } from '@storybook/addon-knobs'; +import React, { useState, useCallback } from 'react'; + +import type { PartialTheme, SeriesIdentifier } from '@elastic/charts'; +import { + AreaSeries, + Axis, + BarSeries, + Chart, + LegendValue, + LineSeries, + Partition, + PartitionLayout, + Position, + ScaleType, + Settings, +} from '@elastic/charts'; +import { SEMANTIC_COLORS } from '@elastic/charts/src/utils/themes/base_colors'; + +import type { ChartsStory } from '../../types'; +import { useBaseTheme } from '../../use_base_theme'; +import { discreteColor, colorBrewerCategoricalPastel12 } from '../utils/utils'; + +// Helper to convert hex to RGB string for rgba() +const hexToRgb = (hex: string): string => { + const result = /^#?([\da-f]{2})([\da-f]{2})([\da-f]{2})$/i.exec(hex); + if (!result) return '0, 0, 0'; + return `${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(result[3], 16)}`; +}; + +// Light mode shades (lighter colors for dimming on light backgrounds) +// Ordered from lightest to darkest +const LIGHT_SHADES: Record = { + [`shade15 (${SEMANTIC_COLORS.shade15}) - default`]: hexToRgb(SEMANTIC_COLORS.shade15), + [`shade20 (${SEMANTIC_COLORS.shade20})`]: hexToRgb(SEMANTIC_COLORS.shade20), + [`shade30 (${SEMANTIC_COLORS.shade30})`]: hexToRgb(SEMANTIC_COLORS.shade30), + [`shade60 (${SEMANTIC_COLORS.shade60})`]: hexToRgb(SEMANTIC_COLORS.shade60), + [`shade70 (${SEMANTIC_COLORS.shade70})`]: hexToRgb(SEMANTIC_COLORS.shade70), + [`shade80 (${SEMANTIC_COLORS.shade80})`]: hexToRgb(SEMANTIC_COLORS.shade80), + [`shade90 (${SEMANTIC_COLORS.shade90})`]: hexToRgb(SEMANTIC_COLORS.shade90), + [`shade95 (${SEMANTIC_COLORS.shade95})`]: hexToRgb(SEMANTIC_COLORS.shade95), + [`shade100 (${SEMANTIC_COLORS.shade100})`]: hexToRgb(SEMANTIC_COLORS.shade100), + [`shade110 (${SEMANTIC_COLORS.shade110})`]: hexToRgb(SEMANTIC_COLORS.shade110), + [`shade120 (${SEMANTIC_COLORS.shade120})`]: hexToRgb(SEMANTIC_COLORS.shade120), + [`shade130 (${SEMANTIC_COLORS.shade130})`]: hexToRgb(SEMANTIC_COLORS.shade130), + [`shade140 (${SEMANTIC_COLORS.shade140})`]: hexToRgb(SEMANTIC_COLORS.shade140), + [`shade145 (${SEMANTIC_COLORS.shade145})`]: hexToRgb(SEMANTIC_COLORS.shade145), +}; + +// Dark mode shades (for dimming on dark backgrounds) +// Ordered from lightest to darkest +const DARK_SHADES: Record = { + [`shade15 (${SEMANTIC_COLORS.shade15})`]: hexToRgb(SEMANTIC_COLORS.shade15), + [`shade20 (${SEMANTIC_COLORS.shade20})`]: hexToRgb(SEMANTIC_COLORS.shade20), + [`shade30 (${SEMANTIC_COLORS.shade30})`]: hexToRgb(SEMANTIC_COLORS.shade30), + [`shade60 (${SEMANTIC_COLORS.shade60})`]: hexToRgb(SEMANTIC_COLORS.shade60), + [`shade70 (${SEMANTIC_COLORS.shade70})`]: hexToRgb(SEMANTIC_COLORS.shade70), + [`shade80 (${SEMANTIC_COLORS.shade80})`]: hexToRgb(SEMANTIC_COLORS.shade80), + [`shade90 (${SEMANTIC_COLORS.shade90})`]: hexToRgb(SEMANTIC_COLORS.shade90), + [`shade95 (${SEMANTIC_COLORS.shade95})`]: hexToRgb(SEMANTIC_COLORS.shade95), + [`shade100 (${SEMANTIC_COLORS.shade100})`]: hexToRgb(SEMANTIC_COLORS.shade100), + [`shade110 (${SEMANTIC_COLORS.shade110}) - default`]: hexToRgb(SEMANTIC_COLORS.shade110), + [`shade120 (${SEMANTIC_COLORS.shade120})`]: hexToRgb(SEMANTIC_COLORS.shade120), + [`shade130 (${SEMANTIC_COLORS.shade130})`]: hexToRgb(SEMANTIC_COLORS.shade130), + [`shade140 (${SEMANTIC_COLORS.shade140})`]: hexToRgb(SEMANTIC_COLORS.shade140), + [`shade145 (${SEMANTIC_COLORS.shade145})`]: hexToRgb(SEMANTIC_COLORS.shade145), +}; + +// Alpha/opacity options (ordered from most transparent to solid) +const ALPHA_OPTIONS: Record = { + '10%': 0.1, + '15%': 0.15, + '20%': 0.2, + '25%': 0.25, + '30%': 0.3, + '35%': 0.35, + '40%': 0.4, + '45%': 0.45, + '50%': 0.5, + '55%': 0.55, + '60%': 0.6, + '65%': 0.65, + '70%': 0.7, + '75%': 0.75, + '80%': 0.8, + '85%': 0.85, + '90%': 0.9, + '95%': 0.95, + '100% (solid) - default': 1.0, +}; + +// Multi-level sunburst data (source -> destination with values) +type SunburstDatum = [string, number, string, number]; +const sunburstData: Array = [ + ['CN', 301, 'IN', 44], + ['CN', 301, 'US', 24], + ['CN', 301, 'ID', 13], + ['CN', 301, 'BR', 8], + ['IN', 245, 'US', 22], + ['IN', 245, 'BR', 11], + ['IN', 245, 'ID', 10], + ['US', 130, 'CN', 33], + ['US', 130, 'IN', 23], + ['US', 130, 'US', 9], + ['US', 130, 'ID', 7], + ['US', 130, 'BR', 5], + ['ID', 55, 'BR', 4], + ['ID', 55, 'US', 3], + ['PK', 43, 'FR', 2], + ['PK', 43, 'PK', 2], +]; + +export const Example: ChartsStory = (_, { description }) => { + const baseTheme = useBaseTheme(); + const isDarkTheme = baseTheme.background.color !== '#FFFFFF'; + + // Track currently hovered series for display + const [hoveredSeries, setHoveredSeries] = useState(null); + + const handleLegendItemOver = useCallback((series: SeriesIdentifier[]) => { + const names = series.map((s) => s.key).join(', '); + setHoveredSeries(names); + }, []); + + const handleLegendItemOut = useCallback(() => { + setHoveredSeries(null); + }, []); + + // Chart visibility toggles + const showPartitionChart = boolean('Show Partition Chart', true); + const showXYChart = boolean('Show XY Chart', true); + + // Toggle between fill-based and opacity-based dimming + const useOpacityOnlyDimming = boolean('Use opacity-only dimming', false); + + const partitionLayout = select( + 'Partition layout', + { Sunburst: PartitionLayout.sunburst, Treemap: PartitionLayout.treemap }, + PartitionLayout.sunburst, + ); + + // Shade selector based on theme + const shadeOptions = isDarkTheme ? DARK_SHADES : LIGHT_SHADES; + const defaultShadeKey = isDarkTheme + ? `shade110 (${SEMANTIC_COLORS.shade110}) - default` + : `shade15 (${SEMANTIC_COLORS.shade15}) - default`; + const selectedShadeRGB = select('Shade color', shadeOptions, shadeOptions[defaultShadeKey] ?? ''); + + // Alpha/opacity selector + const selectedAlphaRaw = select('Alpha (opacity)', ALPHA_OPTIONS, ALPHA_OPTIONS['100% (solid) - default'] ?? 1.0); + const alphaFromString = (value: string): number => { + const trimmed = value.trim(); + if (trimmed.endsWith('%')) { + const pct = Number.parseFloat(trimmed.slice(0, -1)); + return pct / 100; + } + return Number.parseFloat(trimmed); + }; + const selectedAlphaParsed = + typeof selectedAlphaRaw === 'number' ? selectedAlphaRaw : alphaFromString(String(selectedAlphaRaw)); + const selectedAlpha = Number.isFinite(selectedAlphaParsed) ? selectedAlphaParsed : 1.0; + + // Build dimmed color from shade + alpha + const selectedDimmedColor = `rgba(${selectedShadeRGB}, ${selectedAlpha})`; + + // Build theme with dimmed colors for all chart types + // When useOpacityOnlyDimming is true, use { opacity: X } instead of { fill: ... } + const dimmedTheme: PartialTheme = useOpacityOnlyDimming + ? { + barSeriesStyle: { + rect: { + dimmed: { opacity: selectedAlpha }, + }, + }, + lineSeriesStyle: { + line: { + dimmed: { opacity: selectedAlpha }, + }, + point: { + dimmed: { opacity: selectedAlpha }, + }, + }, + areaSeriesStyle: { + area: { + dimmed: { opacity: selectedAlpha }, + }, + line: { + dimmed: { opacity: selectedAlpha }, + }, + point: { + dimmed: { opacity: selectedAlpha }, + }, + }, + partition: { + dimmed: { opacity: selectedAlpha }, + }, + } + : { + barSeriesStyle: { + rect: { + dimmed: { + fill: selectedDimmedColor, + }, + }, + }, + lineSeriesStyle: { + line: { + dimmed: { + stroke: selectedDimmedColor, + }, + }, + point: { + dimmed: { + fill: isDarkTheme ? '#0B1628' : '#FFFFFF', // background color + stroke: selectedDimmedColor, + }, + }, + }, + areaSeriesStyle: { + area: { + dimmed: { + fill: selectedDimmedColor, + }, + }, + line: { + dimmed: { + stroke: selectedDimmedColor, + }, + }, + point: { + dimmed: { + fill: isDarkTheme ? '#0B1628' : '#FFFFFF', + stroke: selectedDimmedColor, + }, + }, + }, + partition: { + dimmed: { + fill: selectedDimmedColor, + }, + }, + }; + + const partitionLayoutLabel = partitionLayout === PartitionLayout.sunburst ? 'Sunburst' : 'Treemap'; + + return ( + <> + {/* Info panel showing current dimmed color and hover state */} +
+
+ + {useOpacityOnlyDimming ? 'Dimmed opacity:' : 'Dimmed color:'} + + {useOpacityOnlyDimming ? ( + {selectedAlpha} + ) : ( + <> +
+ {selectedDimmedColor} + + )} +
+
+ {hoveredSeries ? ( + <> + Hovering: {hoveredSeries} + + ) : ( + 'Hover over a legend item to see dimmed effect' + )} +
+
+ + {/* Partition Chart */} + {showPartitionChart && ( + + + d[3]} + layers={[ + { + groupByRollup: (d: SunburstDatum) => d[0], + nodeLabel: (d) => `dest: ${d}`, + shape: { + fillColor: (key, sortIndex) => discreteColor(colorBrewerCategoricalPastel12, 0.7)(sortIndex), + }, + }, + { + groupByRollup: (d: SunburstDatum) => d[2], + nodeLabel: (d) => `source: ${d}`, + shape: { + fillColor: (key, sortIndex, node) => + discreteColor(colorBrewerCategoricalPastel12, 0.5)(node.parent.sortIndex), + }, + }, + ]} + /> + + )} + + {/* XY Chart */} + {showXYChart && ( + + + + Number(d).toFixed(2)} /> + + + + + + + + + )} + + ); +}; + +Example.parameters = { + markdown: ` +## Dimmed Highlight Style + +This story demonstrates the **dimmed/unhighlighted** styling applied to chart elements when hovering over legend items. + +### How to use +1. Hover over any legend item to see unhighlighted series adopt the dimmed color +2. Use **Shade color** to select the base shade from the EUI Borealis palette +3. Use **Alpha (opacity)** to adjust transparency (10% to 100%) +4. Switch between **light** and **dark** themes to see theme-appropriate shade options +5. Switch **Partition layout** between Sunburst and Treemap + +### Default dim shades +- Light mode: \`shade15\` @ 100% (solid) +- Dark mode: \`shade110\` @ 100% (solid) + +### Affected chart elements +- **Bar charts**: \`barSeriesStyle.rect.dimmed.fill\` +- **Line charts**: \`lineSeriesStyle.line.dimmed.stroke\`, \`lineSeriesStyle.point.dimmed.*\` +- **Area charts**: \`areaSeriesStyle.area.dimmed.fill\`, \`areaSeriesStyle.line.dimmed.stroke\` +- **Partition charts**: \`arcSeriesStyle.arc.dimmed.fill\` + `, +}; diff --git a/storybook/stories/stylings/stylings.stories.tsx b/storybook/stories/stylings/stylings.stories.tsx index f8f03090244..baa5532bc37 100644 --- a/storybook/stories/stylings/stylings.stories.tsx +++ b/storybook/stories/stylings/stylings.stories.tsx @@ -37,3 +37,4 @@ export { Example as withTexture } from './23_with_texture.story'; export { Example as textureMultipleSeries } from './24_texture_multiple_series.story'; export { Example as mixedPointShapes } from './25_mixed_point_shapes.story'; export { Example as highlighterStyle } from './26_highlighter_style.story'; +export { Example as dimmedHighlightStyle } from './27_dimmed_highlight_style.story';