diff --git a/e2e/screenshots/all.test.ts-snapshots/baselines/heatmap-alpha/basic-chrome-linux.png b/e2e/screenshots/all.test.ts-snapshots/baselines/heatmap-alpha/basic-chrome-linux.png index 1d65a4e15c..0e98c90392 100644 Binary files a/e2e/screenshots/all.test.ts-snapshots/baselines/heatmap-alpha/basic-chrome-linux.png and b/e2e/screenshots/all.test.ts-snapshots/baselines/heatmap-alpha/basic-chrome-linux.png differ diff --git a/e2e/screenshots/all.test.ts-snapshots/baselines/heatmap-alpha/categorical-chrome-linux.png b/e2e/screenshots/all.test.ts-snapshots/baselines/heatmap-alpha/categorical-chrome-linux.png index 3c4ad4bb9f..bcba47eddf 100644 Binary files a/e2e/screenshots/all.test.ts-snapshots/baselines/heatmap-alpha/categorical-chrome-linux.png and b/e2e/screenshots/all.test.ts-snapshots/baselines/heatmap-alpha/categorical-chrome-linux.png differ diff --git a/e2e/screenshots/all.test.ts-snapshots/baselines/heatmap-alpha/label-rotation-chrome-linux.png b/e2e/screenshots/all.test.ts-snapshots/baselines/heatmap-alpha/label-rotation-chrome-linux.png index 4501a733b8..0be958dc49 100644 Binary files a/e2e/screenshots/all.test.ts-snapshots/baselines/heatmap-alpha/label-rotation-chrome-linux.png and b/e2e/screenshots/all.test.ts-snapshots/baselines/heatmap-alpha/label-rotation-chrome-linux.png differ diff --git a/e2e/screenshots/all.test.ts-snapshots/baselines/heatmap-alpha/sorting-chrome-linux.png b/e2e/screenshots/all.test.ts-snapshots/baselines/heatmap-alpha/sorting-chrome-linux.png index 191b2ff07a..1249200068 100644 Binary files a/e2e/screenshots/all.test.ts-snapshots/baselines/heatmap-alpha/sorting-chrome-linux.png and b/e2e/screenshots/all.test.ts-snapshots/baselines/heatmap-alpha/sorting-chrome-linux.png differ diff --git a/e2e/screenshots/all.test.ts-snapshots/baselines/heatmap-alpha/theming-chrome-linux.png b/e2e/screenshots/all.test.ts-snapshots/baselines/heatmap-alpha/theming-chrome-linux.png index 841c386c3e..9de88b38d3 100644 Binary files a/e2e/screenshots/all.test.ts-snapshots/baselines/heatmap-alpha/theming-chrome-linux.png and b/e2e/screenshots/all.test.ts-snapshots/baselines/heatmap-alpha/theming-chrome-linux.png differ diff --git a/e2e/screenshots/all.test.ts-snapshots/baselines/heatmap-alpha/time-chrome-linux.png b/e2e/screenshots/all.test.ts-snapshots/baselines/heatmap-alpha/time-chrome-linux.png index 65ecc2808e..efbe2f0de5 100644 Binary files a/e2e/screenshots/all.test.ts-snapshots/baselines/heatmap-alpha/time-chrome-linux.png and b/e2e/screenshots/all.test.ts-snapshots/baselines/heatmap-alpha/time-chrome-linux.png differ diff --git a/e2e/screenshots/all.test.ts-snapshots/baselines/heatmap-alpha/time-snap-chrome-linux.png b/e2e/screenshots/all.test.ts-snapshots/baselines/heatmap-alpha/time-snap-chrome-linux.png index 4e9b1d1070..cac8f425f5 100644 Binary files a/e2e/screenshots/all.test.ts-snapshots/baselines/heatmap-alpha/time-snap-chrome-linux.png and b/e2e/screenshots/all.test.ts-snapshots/baselines/heatmap-alpha/time-snap-chrome-linux.png differ diff --git a/e2e/screenshots/all.test.ts-snapshots/baselines/small-multiples-alpha/heatmap-chrome-linux.png b/e2e/screenshots/all.test.ts-snapshots/baselines/small-multiples-alpha/heatmap-chrome-linux.png new file mode 100644 index 0000000000..d608e12ba9 Binary files /dev/null and b/e2e/screenshots/all.test.ts-snapshots/baselines/small-multiples-alpha/heatmap-chrome-linux.png differ diff --git a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/render-table-with-all-nulls-chrome-linux.png b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/render-table-with-all-nulls-chrome-linux.png index 0677610bcf..8ce089ec1c 100644 Binary files a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/render-table-with-all-nulls-chrome-linux.png and b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/render-table-with-all-nulls-chrome-linux.png differ diff --git a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/rotate-categorical-axis-labels-chrome-linux.png b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/rotate-categorical-axis-labels-chrome-linux.png index 8e0e527871..fdceec9a39 100644 Binary files a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/rotate-categorical-axis-labels-chrome-linux.png and b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/rotate-categorical-axis-labels-chrome-linux.png differ diff --git a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/rotate-time-axis-labels-chrome-linux.png b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/rotate-time-axis-labels-chrome-linux.png index 604c34bb53..05294b8615 100644 Binary files a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/rotate-time-axis-labels-chrome-linux.png and b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/rotate-time-axis-labels-chrome-linux.png differ diff --git a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/should-maximize-the-label-font-size-chrome-linux.png b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/should-maximize-the-label-font-size-chrome-linux.png index cee69e78c4..4c075b19ef 100644 Binary files a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/should-maximize-the-label-font-size-chrome-linux.png and b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/should-maximize-the-label-font-size-chrome-linux.png differ diff --git a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/should-maximize-the-label-with-an-unique-font-size-chrome-linux.png b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/should-maximize-the-label-with-an-unique-font-size-chrome-linux.png index 5a66505788..c648129ace 100644 Binary files a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/should-maximize-the-label-with-an-unique-font-size-chrome-linux.png and b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/should-maximize-the-label-with-an-unique-font-size-chrome-linux.png differ diff --git a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/should-not-have-brush-tool-extend-into-axes-chrome-linux.png b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/should-not-have-brush-tool-extend-into-axes-chrome-linux.png index b9f80fa8ea..a99c98cee6 100644 Binary files a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/should-not-have-brush-tool-extend-into-axes-chrome-linux.png and b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/should-not-have-brush-tool-extend-into-axes-chrome-linux.png differ diff --git a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/should-show-x-and-y-axis-titles-chrome-linux.png b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/should-show-x-and-y-axis-titles-chrome-linux.png index ef8dc57bec..2e2a21a09e 100644 Binary files a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/should-show-x-and-y-axis-titles-chrome-linux.png and b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/should-show-x-and-y-axis-titles-chrome-linux.png differ diff --git a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-brush-panel-with-time-data-chrome-linux.png b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-brush-panel-with-time-data-chrome-linux.png new file mode 100644 index 0000000000..a2ec0d95c8 Binary files /dev/null and b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-brush-panel-with-time-data-chrome-linux.png differ diff --git a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-constrain-brush-to-active-panel-chrome-linux.png b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-constrain-brush-to-active-panel-chrome-linux.png new file mode 100644 index 0000000000..54cda41f93 Binary files /dev/null and b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-constrain-brush-to-active-panel-chrome-linux.png differ diff --git a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-render-0-x-3-trellis-chrome-linux.png b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-render-0-x-3-trellis-chrome-linux.png new file mode 100644 index 0000000000..6f0aa7ae99 Binary files /dev/null and b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-render-0-x-3-trellis-chrome-linux.png differ diff --git a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-render-1-x-3-trellis-chrome-linux.png b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-render-1-x-3-trellis-chrome-linux.png new file mode 100644 index 0000000000..ed72941541 Binary files /dev/null and b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-render-1-x-3-trellis-chrome-linux.png differ diff --git a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-render-2-x-2-trellis-chrome-linux.png b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-render-2-x-2-trellis-chrome-linux.png new file mode 100644 index 0000000000..735bfe5e60 Binary files /dev/null and b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-render-2-x-2-trellis-chrome-linux.png differ diff --git a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-render-2-x-2-trellis-time-data-chrome-linux.png b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-render-2-x-2-trellis-time-data-chrome-linux.png new file mode 100644 index 0000000000..e60b69aae0 Binary files /dev/null and b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-render-2-x-2-trellis-time-data-chrome-linux.png differ diff --git a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-render-2-x-2-trellis-without-any-titles-chrome-linux.png b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-render-2-x-2-trellis-without-any-titles-chrome-linux.png new file mode 100644 index 0000000000..8a74cfb90c Binary files /dev/null and b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-render-2-x-2-trellis-without-any-titles-chrome-linux.png differ diff --git a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-render-2-x-2-trellis-without-any-titles-time-data-chrome-linux.png b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-render-2-x-2-trellis-without-any-titles-time-data-chrome-linux.png new file mode 100644 index 0000000000..6779192fb2 Binary files /dev/null and b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-render-2-x-2-trellis-without-any-titles-time-data-chrome-linux.png differ diff --git a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-render-2-x-2-trellis-without-axes-titles-chrome-linux.png b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-render-2-x-2-trellis-without-axes-titles-chrome-linux.png new file mode 100644 index 0000000000..2f58d95e51 Binary files /dev/null and b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-render-2-x-2-trellis-without-axes-titles-chrome-linux.png differ diff --git a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-render-2-x-2-trellis-without-panel-titles-chrome-linux.png b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-render-2-x-2-trellis-without-panel-titles-chrome-linux.png new file mode 100644 index 0000000000..bd878b8e90 Binary files /dev/null and b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-render-2-x-2-trellis-without-panel-titles-chrome-linux.png differ diff --git a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-render-3-x-0-trellis-chrome-linux.png b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-render-3-x-0-trellis-chrome-linux.png new file mode 100644 index 0000000000..6f0aa7ae99 Binary files /dev/null and b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-render-3-x-0-trellis-chrome-linux.png differ diff --git a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-render-3-x-1-trellis-chrome-linux.png b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-render-3-x-1-trellis-chrome-linux.png new file mode 100644 index 0000000000..2ad98f311b Binary files /dev/null and b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-render-3-x-1-trellis-chrome-linux.png differ diff --git a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-render-3-x-3-trellis-chrome-linux.png b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-render-3-x-3-trellis-chrome-linux.png new file mode 100644 index 0000000000..405b692c58 Binary files /dev/null and b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-render-3-x-3-trellis-chrome-linux.png differ diff --git a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-render-3-x-null-trellis-chrome-linux.png b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-render-3-x-null-trellis-chrome-linux.png new file mode 100644 index 0000000000..85b7de5bfe Binary files /dev/null and b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-render-3-x-null-trellis-chrome-linux.png differ diff --git a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-render-null-x-3-trellis-chrome-linux.png b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-render-null-x-3-trellis-chrome-linux.png new file mode 100644 index 0000000000..7a0acf5a77 Binary files /dev/null and b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-render-null-x-3-trellis-chrome-linux.png differ diff --git a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-render-tooltip-over-correct-panel-chrome-linux.png b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-render-tooltip-over-correct-panel-chrome-linux.png new file mode 100644 index 0000000000..22980b7001 Binary files /dev/null and b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-render-tooltip-over-correct-panel-chrome-linux.png differ diff --git a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-select-single-cell-on-click-with-categorical-data-chrome-linux.png b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-select-single-cell-on-click-with-categorical-data-chrome-linux.png new file mode 100644 index 0000000000..040229d3de Binary files /dev/null and b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-select-single-cell-on-click-with-categorical-data-chrome-linux.png differ diff --git a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-select-single-cell-on-click-with-time-data-chrome-linux.png b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-select-single-cell-on-click-with-time-data-chrome-linux.png new file mode 100644 index 0000000000..d4321ce65c Binary files /dev/null and b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/small-multiples/should-select-single-cell-on-click-with-time-data-chrome-linux.png differ diff --git a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/theme-dark/should-highlight-band-on-legend-hover-dark-chrome-linux.png b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/theme-dark/should-highlight-band-on-legend-hover-dark-chrome-linux.png index c4e31b52d5..25aec8ee3d 100644 Binary files a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/theme-dark/should-highlight-band-on-legend-hover-dark-chrome-linux.png and b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/theme-dark/should-highlight-band-on-legend-hover-dark-chrome-linux.png differ diff --git a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/theme-dark/should-render-basic-heatmap-chrome-linux.png b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/theme-dark/should-render-basic-heatmap-chrome-linux.png index ffe0353d05..68223a1108 100644 Binary files a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/theme-dark/should-render-basic-heatmap-chrome-linux.png and b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/theme-dark/should-render-basic-heatmap-chrome-linux.png differ diff --git a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/theme-dark/should-render-correct-brush-area-chrome-linux.png b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/theme-dark/should-render-correct-brush-area-chrome-linux.png index 4111fa9c4b..6845351f6a 100644 Binary files a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/theme-dark/should-render-correct-brush-area-chrome-linux.png and b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/theme-dark/should-render-correct-brush-area-chrome-linux.png differ diff --git a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/theme-eui-dark/should-highlight-band-on-legend-hover-eui-dark-chrome-linux.png b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/theme-eui-dark/should-highlight-band-on-legend-hover-eui-dark-chrome-linux.png index 2831983de5..7d900375b6 100644 Binary files a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/theme-eui-dark/should-highlight-band-on-legend-hover-eui-dark-chrome-linux.png and b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/theme-eui-dark/should-highlight-band-on-legend-hover-eui-dark-chrome-linux.png differ diff --git a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/theme-eui-dark/should-render-basic-heatmap-chrome-linux.png b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/theme-eui-dark/should-render-basic-heatmap-chrome-linux.png index 0002aab5be..bac1f59aec 100644 Binary files a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/theme-eui-dark/should-render-basic-heatmap-chrome-linux.png and b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/theme-eui-dark/should-render-basic-heatmap-chrome-linux.png differ diff --git a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/theme-eui-dark/should-render-correct-brush-area-chrome-linux.png b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/theme-eui-dark/should-render-correct-brush-area-chrome-linux.png index 879270df3a..b3669d57c0 100644 Binary files a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/theme-eui-dark/should-render-correct-brush-area-chrome-linux.png and b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/theme-eui-dark/should-render-correct-brush-area-chrome-linux.png differ diff --git a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/theme-eui-light/should-highlight-band-on-legend-hover-eui-light-chrome-linux.png b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/theme-eui-light/should-highlight-band-on-legend-hover-eui-light-chrome-linux.png index 7d0abc5cf9..ade10073e1 100644 Binary files a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/theme-eui-light/should-highlight-band-on-legend-hover-eui-light-chrome-linux.png and b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/theme-eui-light/should-highlight-band-on-legend-hover-eui-light-chrome-linux.png differ diff --git a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/theme-eui-light/should-render-basic-heatmap-chrome-linux.png b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/theme-eui-light/should-render-basic-heatmap-chrome-linux.png index 71821affcd..c2760c7a2a 100644 Binary files a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/theme-eui-light/should-render-basic-heatmap-chrome-linux.png and b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/theme-eui-light/should-render-basic-heatmap-chrome-linux.png differ diff --git a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/theme-eui-light/should-render-correct-brush-area-chrome-linux.png b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/theme-eui-light/should-render-correct-brush-area-chrome-linux.png index 1bb22bab98..75f6765d05 100644 Binary files a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/theme-eui-light/should-render-correct-brush-area-chrome-linux.png and b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/theme-eui-light/should-render-correct-brush-area-chrome-linux.png differ diff --git a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/theme-light/should-highlight-band-on-legend-hover-light-chrome-linux.png b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/theme-light/should-highlight-band-on-legend-hover-light-chrome-linux.png index c5e6397bbd..0e456a4f65 100644 Binary files a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/theme-light/should-highlight-band-on-legend-hover-light-chrome-linux.png and b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/theme-light/should-highlight-band-on-legend-hover-light-chrome-linux.png differ diff --git a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/theme-light/should-render-basic-heatmap-chrome-linux.png b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/theme-light/should-render-basic-heatmap-chrome-linux.png index 1d65a4e15c..0e98c90392 100644 Binary files a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/theme-light/should-render-basic-heatmap-chrome-linux.png and b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/theme-light/should-render-basic-heatmap-chrome-linux.png differ diff --git a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/theme-light/should-render-correct-brush-area-chrome-linux.png b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/theme-light/should-render-correct-brush-area-chrome-linux.png index 0edcb4cb69..6938c27007 100644 Binary files a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/theme-light/should-render-correct-brush-area-chrome-linux.png and b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/theme-light/should-render-correct-brush-area-chrome-linux.png differ diff --git a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/time-snap-with-dataset-2-chrome-linux.png b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/time-snap-with-dataset-2-chrome-linux.png index 25e3123f2f..b15574cf94 100644 Binary files a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/time-snap-with-dataset-2-chrome-linux.png and b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/time-snap-with-dataset-2-chrome-linux.png differ diff --git a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/time-snap-with-dataset-3-chrome-linux.png b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/time-snap-with-dataset-3-chrome-linux.png index b08d06c8af..1d19ad06a4 100644 Binary files a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/time-snap-with-dataset-3-chrome-linux.png and b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/time-snap-with-dataset-3-chrome-linux.png differ diff --git a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/time-snap-with-dataset-4-chrome-linux.png b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/time-snap-with-dataset-4-chrome-linux.png index 76a2f52a94..28b5b18ae1 100644 Binary files a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/time-snap-with-dataset-4-chrome-linux.png and b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/time-snap-with-dataset-4-chrome-linux.png differ diff --git a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/time-snap-with-dataset-5-chrome-linux.png b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/time-snap-with-dataset-5-chrome-linux.png index 69a45663a5..faf1ff910d 100644 Binary files a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/time-snap-with-dataset-5-chrome-linux.png and b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/time-snap-with-dataset-5-chrome-linux.png differ diff --git a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/time-snap-with-dataset-6-chrome-linux.png b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/time-snap-with-dataset-6-chrome-linux.png index 8e09d579a2..797a6f8388 100644 Binary files a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/time-snap-with-dataset-6-chrome-linux.png and b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/time-snap-with-dataset-6-chrome-linux.png differ diff --git a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/time-snap-with-dataset-7-chrome-linux.png b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/time-snap-with-dataset-7-chrome-linux.png index d674c768e8..4fa971c4f1 100644 Binary files a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/time-snap-with-dataset-7-chrome-linux.png and b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/time-snap-with-dataset-7-chrome-linux.png differ diff --git a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/time-snap-with-dataset-8-chrome-linux.png b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/time-snap-with-dataset-8-chrome-linux.png index f38b674e0f..68a70d6bb2 100644 Binary files a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/time-snap-with-dataset-8-chrome-linux.png and b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/time-snap-with-dataset-8-chrome-linux.png differ diff --git a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/time-snap-with-dataset-9-chrome-linux.png b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/time-snap-with-dataset-9-chrome-linux.png index b8b41e377a..4c0e596b6f 100644 Binary files a/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/time-snap-with-dataset-9-chrome-linux.png and b/e2e/screenshots/heatmap_stories.test.ts-snapshots/heatmap-stories/time-snap-with-dataset-9-chrome-linux.png differ diff --git a/e2e/screenshots/nick.test.ts-snapshots/nick/testing-hover-with-body-chrome-linux.png b/e2e/screenshots/nick.test.ts-snapshots/nick/testing-hover-with-body-chrome-linux.png deleted file mode 100644 index bd54556941..0000000000 Binary files a/e2e/screenshots/nick.test.ts-snapshots/nick/testing-hover-with-body-chrome-linux.png and /dev/null differ diff --git a/e2e/screenshots/nick.test.ts-snapshots/nick/testing-hover-with-chart-chrome-linux.png b/e2e/screenshots/nick.test.ts-snapshots/nick/testing-hover-with-chart-chrome-linux.png deleted file mode 100644 index b5433a936d..0000000000 Binary files a/e2e/screenshots/nick.test.ts-snapshots/nick/testing-hover-with-chart-chrome-linux.png and /dev/null differ diff --git a/e2e/screenshots/nick.test.ts-snapshots/nick/testing-hover-with-root-chrome-linux.png b/e2e/screenshots/nick.test.ts-snapshots/nick/testing-hover-with-root-chrome-linux.png deleted file mode 100644 index bd54556941..0000000000 Binary files a/e2e/screenshots/nick.test.ts-snapshots/nick/testing-hover-with-root-chrome-linux.png and /dev/null differ diff --git a/e2e/screenshots/tooltip.test.ts-snapshots/tooltip/chart-types/heatmap/pinning-with-selection-chrome-linux.png b/e2e/screenshots/tooltip.test.ts-snapshots/tooltip/chart-types/heatmap/pinning-with-selection-chrome-linux.png index bfce55deba..b58764806e 100644 Binary files a/e2e/screenshots/tooltip.test.ts-snapshots/tooltip/chart-types/heatmap/pinning-with-selection-chrome-linux.png and b/e2e/screenshots/tooltip.test.ts-snapshots/tooltip/chart-types/heatmap/pinning-with-selection-chrome-linux.png differ diff --git a/e2e/tests/heatmap_stories.test.ts b/e2e/tests/heatmap_stories.test.ts index 769f750a7f..e11b512bd8 100644 --- a/e2e/tests/heatmap_stories.test.ts +++ b/e2e/tests/heatmap_stories.test.ts @@ -84,6 +84,95 @@ test.describe('Heatmap stories', () => { ); }); + test.describe('Small multiples', () => { + const titleOptions = { + panel: ' without panel titles', + axes: ' without axes titles', + both: ' without any titles', + }; + + pwEach.test<{ + vSplit: number | null; + hSplit: number | null; + density?: number; + timeData?: boolean; + hiddenTitles?: 'panel' | 'axes' | 'both'; + }>([ + { vSplit: 2, hSplit: 2 }, + { vSplit: 2, hSplit: 2, timeData: true }, + { vSplit: 3, hSplit: 3 }, + { vSplit: 1, hSplit: 3 }, + { vSplit: null, hSplit: 3 }, + { vSplit: 0, hSplit: 3 }, + { vSplit: 3, hSplit: 1 }, + { vSplit: 3, hSplit: 0 }, + { vSplit: 3, hSplit: null }, + { vSplit: 2, hSplit: 2, hiddenTitles: 'axes' }, + { vSplit: 2, hSplit: 2, hiddenTitles: 'panel' }, + { vSplit: 2, hSplit: 2, hiddenTitles: 'both' }, + { vSplit: 2, hSplit: 2, hiddenTitles: 'both', timeData: true }, + ])( + ({ vSplit, hSplit, hiddenTitles, timeData }) => { + const titleText = (hiddenTitles && titleOptions[hiddenTitles]) ?? ''; + const timeDataText = timeData ? ' - time data' : ''; + return `should render ${vSplit} x ${hSplit} trellis${titleText}${timeDataText}`; + }, + async (page, { vSplit, hSplit, density = 30, hiddenTitles, timeData = false }) => { + const showAxesTitles = hiddenTitles !== 'axes' && hiddenTitles !== 'both'; + const showPanelTitles = hiddenTitles !== 'panel' && hiddenTitles !== 'both'; + await common.expectChartAtUrlToMatchScreenshot(page)( + `http://localhost:9001/?path=/story/small-multiples-alpha--heatmap&globals=background:white;theme:light&knob-Hide_left=true&knob-Hide_right=true&knob-Hide_top=true&knob-Horizontal inner pad=0.1&knob-Horizontal inner pad_SmallMultiples Styles=0.05&knob-Horizontal outer pad=0&knob-Horizontal outer pad_SmallMultiples Styles=0&knob-Persist cells selection=true&knob-Show Legend=true&knob-Show axes title_SmallMultiples Styles=${showAxesTitles}&knob-Show axes panel titles_SmallMultiples Styles=${showPanelTitles}&knob-Show grid line_bottom=true&knob-Show grid line_left=true&knob-Show x axis title_SmallMultiples Styles=true&knob-Show y axis title_SmallMultiples Styles=true&knob-Time data=${timeData}&knob-Title_bottom=Hosts - Bottom&knob-Title_left=Metrics - Left&knob-Title_right=Metrics - Right&knob-Title_top=Hosts - Top&knob-Vertical inner pad=0.3&knob-Vertical inner pad_SmallMultiples Styles=0.1&knob-Vertical outer pad=0&knob-Vertical outer pad_SmallMultiples Styles=0&knob-categories_Data=4&knob-cell%20density(%)_Data=${density}&knob-density(%)_Data=100&knob-density_Data=2&knob-group count_Data=9&knob-h - split count_Data=${ + hSplit ?? 0 + }&knob-h - split_Data=${Number.isFinite(hSplit)}&knob-number of groups_Data=4&knob-v - split count_Data=${ + vSplit ?? 0 + }&knob-v - split_Data=${Number.isFinite( + vSplit, + )}&knob-Debug=&knob-Enable debug state=true&knob-cell density(%)_Data=75&knob-xScaleType_Data=linear&knob-Grid stroke_SmallMultiples Styles=1`, + { + waitSelector: (vSplit ?? 1) * (hSplit ?? 1) === 0 ? '.echReactiveChart_noResults' : undefined, + }, + ); + }, + ); + + test('should render tooltip over correct panel', async ({ page }) => { + await common.expectChartWithMouseAtUrlToMatchScreenshot(page)( + 'http://localhost:9001/?path=/story/small-multiples-alpha--heatmap&globals=theme:light&knob-Hide_left=true&knob-Hide_right=true&knob-Hide_top=true&knob-Horizontal inner pad=0.1&knob-Horizontal inner pad_SmallMultiples Styles=0.05&knob-Horizontal outer pad=0&knob-Horizontal outer pad_SmallMultiples Styles=0&knob-Persist cells selection=true&knob-Show Legend=true&knob-Show axes title_SmallMultiples Styles=true&knob-Show axis panel titles=true&knob-Show axis panel titles_SmallMultiples Styles=true&knob-Show grid line_bottom=true&knob-Show grid line_left=true&knob-Show x axis title_SmallMultiples Styles=true&knob-Show y axis title_SmallMultiples Styles=true&knob-Time data=true&knob-Title_bottom=Hosts - Bottom&knob-Title_left=Metrics - Left&knob-Title_right=Metrics - Right&knob-Title_top=Hosts - Top&knob-Vertical inner pad=0.3&knob-Vertical inner pad_SmallMultiples Styles=0.1&knob-Vertical outer pad=0&knob-Vertical outer pad_SmallMultiples Styles=0&knob-categories_Data=4&knob-cell%20density(%)_Data[0]=20&knob-cell%20density(%)_Data[1]=20,60,20,20,50,20,5,25,20,25,50,50,50,50,25,20,20&knob-density(%)_Data=100&knob-density_Data=2&knob-group count_Data=9&knob-h - split count_Data=2&knob-h - split_Data=true&knob-number of groups_Data=4&knob-v - split count_Data=2&knob-v - split_Data=true&knob-Debug=&knob-cell density(%)_Data=100', + { left: 464, top: 212 }, + ); + }); + + test('should constrain brush to active panel', async ({ page }) => { + await common.expectChartWithDragAtUrlToMatchScreenshot(page)( + 'http://localhost:9001/?path=/story/small-multiples-alpha--heatmap&globals=background:white;theme:light&knob-Enable%20debug%20state=true&knob-Grid%20stroke_SmallMultiples%20Styles=1&knob-Horizontal%20inner%20pad_SmallMultiples%20Styles=0.05&knob-Horizontal%20outer%20pad_SmallMultiples%20Styles=0&knob-Persist%20cells%20selection=true&knob-Show%20axes%20panel%20titles_SmallMultiples%20Styles=true&knob-Show%20axes%20title_SmallMultiples%20Styles=true&knob-Vertical%20inner%20pad_SmallMultiples%20Styles=0.1&knob-Vertical%20outer%20pad_SmallMultiples%20Styles=0&knob-categories_Data=4&knob-cell%20density(%)_Data=75&knob-h%20-%20split%20count_Data=2&knob-h%20-%20split_Data=true&knob-v%20-%20split%20count_Data=2&knob-v%20-%20split_Data=true&knob-xScaleType_Data=linear', + { left: 320, top: 100 }, + { left: 630, top: 245 }, + ); + }); + + test('should brush panel with time data', async ({ page }) => { + await common.expectChartWithDragAtUrlToMatchScreenshot(page)( + 'http://localhost:9001/?path=/story/small-multiples-alpha--heatmap&globals=background:white;theme:light&knob-Hide_left=true&knob-Hide_right=true&knob-Hide_top=true&knob-Horizontal inner pad=0.1&knob-Horizontal inner pad_SmallMultiples Styles=0.05&knob-Horizontal outer pad=0&knob-Horizontal outer pad_SmallMultiples Styles=0&knob-Persist cells selection=true&knob-Show Legend=&knob-Show axes title_SmallMultiples Styles=true&knob-Show axis panel titles=true&knob-Show axis panel titles_SmallMultiples Styles=true&knob-Show grid line_bottom=true&knob-Show grid line_left=true&knob-Show x axis title_SmallMultiples Styles=true&knob-Show y axis title_SmallMultiples Styles=true&knob-Time data=true&knob-Title_bottom=Hosts - Bottom&knob-Title_left=Metrics - Left&knob-Title_right=Metrics - Right&knob-Title_top=Hosts - Top&knob-Vertical inner pad=0.3&knob-Vertical inner pad_SmallMultiples Styles=0.1&knob-Vertical outer pad=0&knob-Vertical outer pad_SmallMultiples Styles=0&knob-categories_Data=4&knob-group count_Data=9&knob-h - split count_Data=2&knob-h - split_Data=true&knob-number of groups_Data=4&knob-v - split count_Data=2&knob-v - split_Data=true&knob-Debug=&knob-Enable debug state=true&knob-xScaleType_Data=linear&knob-Show axes panel titles_SmallMultiples Styles=true&knob-Grid stroke_SmallMultiples Styles=1&knob-cell density(%)_Data=50', + { left: 520, top: 210 }, + { left: 710, top: 280 }, + ); + }); + + test('should select single cell on click with time data', async ({ page }) => { + await common.expectChartWithClickAtUrlToMatchScreenshot(page)( + 'http://localhost:9001/?path=/story/small-multiples-alpha--heatmap&globals=background:white;theme:light&knob-Enable debug state=true&knob-Grid stroke_SmallMultiples Styles=1&knob-Hide_left=true&knob-Hide_right=true&knob-Hide_top=true&knob-Horizontal inner pad=0.1&knob-Horizontal inner pad_SmallMultiples Styles=0.05&knob-Horizontal outer pad=0&knob-Horizontal outer pad_SmallMultiples Styles=0&knob-Persist cells selection=true&knob-Show axes panel titles_SmallMultiples Styles=true&knob-Show axes title_SmallMultiples Styles=true&knob-Show axis panel titles=true&knob-Show axis panel titles_SmallMultiples Styles=true&knob-Show grid line_bottom=true&knob-Show grid line_left=true&knob-Show x axis title_SmallMultiples Styles=true&knob-Show y axis title_SmallMultiples Styles=true&knob-Time data=true&knob-Title_bottom=Hosts - Bottom&knob-Title_left=Metrics - Left&knob-Title_right=Metrics - Right&knob-Title_top=Hosts - Top&knob-Vertical inner pad=0.3&knob-Vertical inner pad_SmallMultiples Styles=0.1&knob-Vertical outer pad=0&knob-Vertical outer pad_SmallMultiples Styles=0&knob-categories_Data=4&knob-group count_Data=9&knob-h - split count_Data=2&knob-h - split_Data=true&knob-number of groups_Data=4&knob-v - split count_Data=2&knob-v - split_Data=true&knob-xScaleType_Data=linear&knob-Debug=&knob-Show Legend=&knob-cell density(%)_Data=100', + { left: 340, top: 250 }, + ); + }); + + test('should select single cell on click with categorical data', async ({ page }) => { + await common.expectChartWithClickAtUrlToMatchScreenshot(page)( + 'http://localhost:9001/?path=/story/small-multiples-alpha--heatmap&globals=background:white;theme:light&knob-Enable debug state=true&knob-Grid stroke_SmallMultiples Styles=1&knob-Hide_left=true&knob-Hide_right=true&knob-Hide_top=true&knob-Horizontal inner pad=0.1&knob-Horizontal inner pad_SmallMultiples Styles=0.05&knob-Horizontal outer pad=0&knob-Horizontal outer pad_SmallMultiples Styles=0&knob-Persist cells selection=true&knob-Show axes panel titles_SmallMultiples Styles=true&knob-Show axes title_SmallMultiples Styles=true&knob-Show axis panel titles=true&knob-Show axis panel titles_SmallMultiples Styles=true&knob-Show grid line_bottom=true&knob-Show grid line_left=true&knob-Show x axis title_SmallMultiples Styles=true&knob-Show y axis title_SmallMultiples Styles=true&knob-Time data=false&knob-Title_bottom=Hosts - Bottom&knob-Title_left=Metrics - Left&knob-Title_right=Metrics - Right&knob-Title_top=Hosts - Top&knob-Vertical inner pad=0.3&knob-Vertical inner pad_SmallMultiples Styles=0.1&knob-Vertical outer pad=0&knob-Vertical outer pad_SmallMultiples Styles=0&knob-categories_Data=4&knob-group count_Data=9&knob-h - split count_Data=2&knob-h - split_Data=true&knob-number of groups_Data=4&knob-v - split count_Data=2&knob-v - split_Data=true&knob-xScaleType_Data=linear&knob-Debug=&knob-Show Legend=&knob-cell density(%)_Data=100', + { left: 340, top: 250 }, + ); + }); + }); + eachTheme.describe(({ theme, urlParam }) => { test(`should highlight band on legend hover - ${theme}`, async ({ page }) => { await common.expectChartWithMouseAtUrlToMatchScreenshot(page)( diff --git a/packages/charts/api/charts.api.md b/packages/charts/api/charts.api.md index 3568ee07c7..7ff6b71ec2 100644 --- a/packages/charts/api/charts.api.md +++ b/packages/charts/api/charts.api.md @@ -231,7 +231,7 @@ export interface ArrayNode extends NodeDescriptor { } // @public -export const Axis: FC>; +export const Axis: FC>; // @public (undocumented) export type AxisId = string; @@ -329,7 +329,7 @@ export interface BandFillColorAccessorInput { } // @public -export const BarSeries: (props: SFProps, "chartType" | "specType" | "seriesType", "groupId" | "xScaleType" | "yScaleType" | "hideInLegend" | "enableHistogramMode", "name" | "color" | "sortIndex" | "timeZone" | "barSeriesStyle" | "xNice" | "yNice" | "useDefaultGroupDomain" | "displayValueSettings" | "y0AccessorFormat" | "y1AccessorFormat" | "filterSeriesInTooltip" | "tickFormat" | "y0Accessors" | "splitSeriesAccessors" | "stackAccessors" | "markSizeAccessor" | "stackMode" | "styleAccessor" | "minBarHeight", "id" | "data" | "xAccessor" | "yAccessors">) => null; +export const BarSeries: (props: SFProps, "chartType" | "specType" | "seriesType", "groupId" | "enableHistogramMode" | "xScaleType" | "yScaleType" | "hideInLegend", "name" | "color" | "sortIndex" | "timeZone" | "barSeriesStyle" | "xNice" | "yNice" | "useDefaultGroupDomain" | "displayValueSettings" | "y0AccessorFormat" | "y1AccessorFormat" | "filterSeriesInTooltip" | "tickFormat" | "y0Accessors" | "splitSeriesAccessors" | "stackAccessors" | "markSizeAccessor" | "stackMode" | "styleAccessor" | "minBarHeight", "id" | "data" | "xAccessor" | "yAccessors">) => null; // @public (undocumented) export type BarSeriesProps = ComponentProps; @@ -448,8 +448,6 @@ export type CategoryLabel = string; // @public (undocumented) export interface Cell { - // Warning: (ae-forgotten-export) The symbol "HeatmapCellDatum" needs to be exported by the entry point index.d.ts - // // (undocumented) datum: HeatmapCellDatum; // Warning: (ae-forgotten-export) The symbol "Fill" needs to be exported by the entry point index.d.ts @@ -729,6 +727,10 @@ export class DataGenerator { y: number; g: string; }[]; + generateSMGroupedSeries>(verticalGroups: Array | number, horizontalGroups: Array | number, seriesGenerator: (h: string | number, v: string | number) => T[]): ({ + h: string | number; + v: string | number; + } & T)[]; } // @public (undocumented) @@ -1294,7 +1296,7 @@ export type GroupId = string; export type GroupKeysOrKeyFn = Array | GroupByKeyFn; // @alpha -export const Heatmap: (props: SFProps, "chartType" | "specType", "data" | "valueAccessor" | "valueFormatter" | "timeZone" | "xAccessor" | "yAccessor" | "xScale" | "xSortPredicate" | "ySortPredicate" | "xAxisTitle" | "xAxisLabelName" | "xAxisLabelFormatter" | "yAxisTitle" | "yAxisLabelName" | "yAxisLabelFormatter", "name" | "onBrushEnd" | "highlightedData", "id" | "colorScale">) => null; +export const Heatmap: (props: SFProps, "chartType" | "specType", "data" | "timeZone" | "valueAccessor" | "valueFormatter" | "xAccessor" | "yAccessor" | "xScale" | "xSortPredicate" | "ySortPredicate" | "xAxisTitle" | "xAxisLabelName" | "xAxisLabelFormatter" | "yAxisTitle" | "yAxisLabelName" | "yAxisLabelFormatter", "name" | "highlightedData", "id" | "colorScale">) => null; // @alpha (undocumented) export interface HeatmapBandsColorScale { @@ -1306,15 +1308,38 @@ export interface HeatmapBandsColorScale { } // @public (undocumented) -export type HeatmapBrushEvent = { +export interface HeatmapBrushEvent extends SmallMultiplesDatum { + // (undocumented) cells: Cell[]; + // (undocumented) x: (string | number)[]; + // (undocumented) y: (string | number)[]; -}; +} + +// @public (undocumented) +export interface HeatmapCellDatum extends SmallMultiplesDatum { + // (undocumented) + originalIndex: number; + // (undocumented) + value: number; + // (undocumented) + x: NonNullable; + // (undocumented) + y: NonNullable; +} // @public (undocumented) export type HeatmapElementEvent = [Cell, SeriesIdentifier]; +// @public (undocumented) +export interface HeatmapHighlightedData extends SmallMultiplesDatum { + // (undocumented) + x: Array; + // (undocumented) + y: Array; +} + // Warning: (ae-incompatible-release-tags) The symbol "HeatmapProps" is marked as @public, but its signature references "Heatmap" which is marked as @alpha // // @public (undocumented) @@ -1329,16 +1354,9 @@ export interface HeatmapSpec extends Spec { // (undocumented) data: D[]; // (undocumented) - highlightedData?: { - x: Array; - y: Array; - }; + highlightedData?: HeatmapHighlightedData; // (undocumented) name?: string; - // Warning: (ae-forgotten-export) The symbol "HeatmapBrushEvent_2" needs to be exported by the entry point index.d.ts - // - // (undocumented) - onBrushEnd?: (brushArea: HeatmapBrushEvent_2) => void; // (undocumented) specType: typeof SpecType.Series; // (undocumented) @@ -1406,25 +1424,13 @@ export interface HeatmapStyle { }; // (undocumented) grid: { - cellWidth: { - min: Pixels; - max: Pixels | 'fill'; - }; - cellHeight: { - min: Pixels; - max: Pixels | 'fill'; - }; stroke: { color: string; width: number; }; }; // (undocumented) - maxColumnWidth: Pixels; - // (undocumented) maxLegendHeight?: number; - // (undocumented) - maxRowHeight: Pixels; // Warning: (ae-forgotten-export) The symbol "Font" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1464,7 +1470,7 @@ export interface HighlighterStyle { } // @public -export const HistogramBarSeries: (props: SFProps, "chartType" | "specType" | "seriesType", "groupId" | "xScaleType" | "yScaleType" | "hideInLegend" | "enableHistogramMode", "name" | "color" | "sortIndex" | "timeZone" | "barSeriesStyle" | "xNice" | "yNice" | "useDefaultGroupDomain" | "displayValueSettings" | "y0AccessorFormat" | "y1AccessorFormat" | "filterSeriesInTooltip" | "tickFormat" | "y0Accessors" | "splitSeriesAccessors" | "markSizeAccessor" | "stackMode" | "styleAccessor" | "minBarHeight", "id" | "data" | "xAccessor" | "yAccessors">) => null; +export const HistogramBarSeries: (props: SFProps, "chartType" | "specType" | "seriesType", "groupId" | "enableHistogramMode" | "xScaleType" | "yScaleType" | "hideInLegend", "name" | "color" | "sortIndex" | "timeZone" | "barSeriesStyle" | "xNice" | "yNice" | "useDefaultGroupDomain" | "displayValueSettings" | "y0AccessorFormat" | "y1AccessorFormat" | "filterSeriesInTooltip" | "tickFormat" | "y0Accessors" | "splitSeriesAccessors" | "markSizeAccessor" | "stackMode" | "styleAccessor" | "minBarHeight", "id" | "data" | "xAccessor" | "yAccessors">) => null; // @public (undocumented) export type HistogramBarSeriesProps = ComponentProps; @@ -2481,7 +2487,7 @@ export const Settings: (props: SFProps; +export const settingsBuildProps: BuildProps; // @public (undocumented) export type SettingsProps = ComponentProps; @@ -2582,6 +2588,14 @@ export interface SimplePadding { // @alpha export const SmallMultiples: FC>; +// @public (undocumented) +export interface SmallMultiplesDatum { + // (undocumented) + smHorizontalAccessorValue?: NonNullable; + // (undocumented) + smVerticalAccessorValue?: NonNullable; +} + // Warning: (ae-incompatible-release-tags) The symbol "SmallMultiplesProps" is marked as @public, but its signature references "SmallMultiples" which is marked as @alpha // // @public (undocumented) @@ -3243,14 +3257,10 @@ export interface XYBrushEvent { export type XYChartElementEvent = [GeometryValue, XYChartSeriesIdentifier]; // @public (undocumented) -export interface XYChartSeriesIdentifier extends SeriesIdentifier { +export interface XYChartSeriesIdentifier extends SeriesIdentifier, SmallMultiplesDatum { // (undocumented) seriesKeys: (string | number)[]; // (undocumented) - smHorizontalAccessorValue?: string | number; - // (undocumented) - smVerticalAccessorValue?: string | number; - // (undocumented) splitAccessors: Map; // (undocumented) xAccessor: Accessor; diff --git a/packages/charts/src/chart_types/flame_chart/internal_chart_state.ts b/packages/charts/src/chart_types/flame_chart/internal_chart_state.ts index 32fb37a9e0..96dbd4e451 100644 --- a/packages/charts/src/chart_types/flame_chart/internal_chart_state.ts +++ b/packages/charts/src/chart_types/flame_chart/internal_chart_state.ts @@ -37,4 +37,10 @@ export class FlameState implements InternalChartState { getMainProjectionArea = () => ({ width: 0, height: 0, top: 0, left: 0 }); getBrushArea = () => null; getDebugState = () => ({}); + getSmallMultiplesDomains() { + return { + smHDomain: [], + smVDomain: [], + }; + } } diff --git a/packages/charts/src/chart_types/goal_chart/state/chart_state.tsx b/packages/charts/src/chart_types/goal_chart/state/chart_state.tsx index 034e903922..d128fb0620 100644 --- a/packages/charts/src/chart_types/goal_chart/state/chart_state.tsx +++ b/packages/charts/src/chart_types/goal_chart/state/chart_state.tsx @@ -141,4 +141,11 @@ export class GoalState implements InternalChartState { getDebugState(): DebugState { return {}; } + + getSmallMultiplesDomains() { + return { + smHDomain: [], + smVDomain: [], + }; + } } diff --git a/packages/charts/src/chart_types/heatmap/layout/types/viewmodel_types.ts b/packages/charts/src/chart_types/heatmap/layout/types/viewmodel_types.ts index 0d198c4322..e29939705b 100644 --- a/packages/charts/src/chart_types/heatmap/layout/types/viewmodel_types.ts +++ b/packages/charts/src/chart_types/heatmap/layout/types/viewmodel_types.ts @@ -7,8 +7,9 @@ */ import { ChartType } from '../../..'; -import { Color, Colors } from '../../../../common/colors'; +import { Color } from '../../../../common/colors'; import { Pixels } from '../../../../common/geometry'; +import { PerPanelMap } from '../../../../common/panel_utils'; import { Box, Font, TextAlign } from '../../../../common/text_utils'; import { Fill, Line, Rect, Stroke } from '../../../../geoms/types'; import { HeatmapBrushEvent } from '../../../../specs/settings'; @@ -47,7 +48,16 @@ export interface TextBox extends Box { } /** @internal */ -export interface HeatmapViewModel { +export type HeatmapTitleConfig = Font & + Visible & { + fontSize: number; + text: string; + origin: Point; + rotation: 0 | -90; + }; + +/** @internal */ +export interface HeatmapViewModel extends PerPanelMap { gridOrigin: { x: number; y: number; @@ -61,16 +71,7 @@ export interface HeatmapViewModel { cellFontSize: (c: Cell) => Pixels; xValues: Array; yValues: Array; - pageSize: number; - titles: Array< - Font & - Visible & { - fontSize: number; - text: string; - origin: Point; - rotation: 0 | -90; - } - >; + titles: HeatmapTitleConfig[]; } /** @internal */ @@ -82,10 +83,10 @@ export function isPickedCells(v: unknown): v is Cell[] { export type PickFunction = (x: Pixels, y: Pixels) => Cell[] | TextBox; /** @internal */ -export type PickDragFunction = (points: [Point, Point]) => HeatmapBrushEvent; +export type PickDragFunction = (points: [start: Point, end: Point]) => HeatmapBrushEvent; /** @internal */ -export type PickDragShapeFunction = (points: [Point, Point]) => Rect | null; +export type PickDragShapeFunction = (points: [start: Point, end: Point]) => Rect | null; /** * From x and y coordinates in the data domain space to a canvas projected rectangle @@ -97,6 +98,8 @@ export type PickDragShapeFunction = (points: [Point, Point]) => Rect | null; export type PickHighlightedArea = ( x: Array>, y: Array>, + smHorizontalAccessorValue?: string | number, + smVerticalAccessorValue?: string | number, ) => Rect | null; /** @internal */ @@ -111,7 +114,7 @@ export type DragShape = ReturnType; /** @internal */ export type ShapeViewModel = { theme: HeatmapStyle; - heatmapViewModel: HeatmapViewModel; + heatmapViewModels: HeatmapViewModel[]; pickQuads: PickFunction; pickDragArea: PickDragFunction; pickDragShape: PickDragShapeFunction; @@ -120,29 +123,10 @@ export type ShapeViewModel = { pickCursorBand: PickCursorBand; }; -/** @internal */ -export const nullHeatmapViewModel: HeatmapViewModel = { - gridOrigin: { - x: 0, - y: 0, - }, - gridLines: { - x: [], - y: [], - stroke: { width: 0, color: Colors.Transparent.rgba }, - }, - cells: [], - xValues: [], - yValues: [], - pageSize: 0, - cellFontSize: () => 0, - titles: [], -}; - /** @internal */ export const nullShapeViewModel = (): ShapeViewModel => ({ theme: LIGHT_THEME.heatmap, - heatmapViewModel: nullHeatmapViewModel, + heatmapViewModels: [], pickQuads: () => [], pickDragArea: () => ({ cells: [], x: [], y: [], chartType: ChartType.Heatmap }), pickDragShape: () => ({ x: 0, y: 0, width: 0, height: 0 }), diff --git a/packages/charts/src/chart_types/heatmap/layout/viewmodel/scenegraph.ts b/packages/charts/src/chart_types/heatmap/layout/viewmodel/scenegraph.ts index 2b4a31c2d5..406edbbfab 100644 --- a/packages/charts/src/chart_types/heatmap/layout/viewmodel/scenegraph.ts +++ b/packages/charts/src/chart_types/heatmap/layout/viewmodel/scenegraph.ts @@ -6,24 +6,41 @@ * Side Public License, v 1. */ +import { SmallMultipleScales, SmallMultiplesGroupBy } from '../../../../common/panel_utils'; import { withTextMeasure } from '../../../../utils/bbox/canvas_text_bbox_calculator'; import { Theme } from '../../../../utils/themes/theme'; +import { ChartDimensions } from '../../../xy_chart/utils/dimensions'; import { ShapeViewModel } from '../../layout/types/viewmodel_types'; import { shapeViewModel } from '../../layout/viewmodel/viewmodel'; import { HeatmapSpec } from '../../specs'; -import { ChartElementSizes, HeatmapTable } from '../../state/selectors/compute_chart_dimensions'; +import { ChartElementSizes } from '../../state/selectors/compute_chart_element_sizes'; import { ColorScale } from '../../state/selectors/get_color_scale'; +import { HeatmapTable } from '../../state/selectors/get_heatmap_table'; /** @internal */ -export function render( +export function computeScenegraph( spec: HeatmapSpec, + chartDimensions: ChartDimensions, elementSizes: ChartElementSizes, + smScales: SmallMultipleScales, + groupBySpec: SmallMultiplesGroupBy, heatmapTable: HeatmapTable, colorScale: ColorScale, bandsToHide: Array<[number, number]>, theme: Theme, ): ShapeViewModel { return withTextMeasure((measureText) => { - return shapeViewModel(measureText, spec, theme, elementSizes, heatmapTable, colorScale, bandsToHide); + return shapeViewModel( + measureText, + spec, + theme, + chartDimensions, + elementSizes, + heatmapTable, + colorScale, + smScales, + groupBySpec, + bandsToHide, + ); }); } diff --git a/packages/charts/src/chart_types/heatmap/layout/viewmodel/viewmodel.ts b/packages/charts/src/chart_types/heatmap/layout/viewmodel/viewmodel.ts index c05692bcc7..0d04b1e833 100644 --- a/packages/charts/src/chart_types/heatmap/layout/viewmodel/viewmodel.ts +++ b/packages/charts/src/chart_types/heatmap/layout/viewmodel/viewmodel.ts @@ -6,29 +6,41 @@ * Side Public License, v 1. */ -import { bisectLeft } from 'd3-array'; import { ScaleBand, scaleBand, scaleQuantize } from 'd3-scale'; import { BaseDatum } from './../../../xy_chart/utils/specs'; import { colorToRgba } from '../../../../common/color_library_wrappers'; import { fillTextColor } from '../../../../common/fill_text_color'; import { Pixels } from '../../../../common/geometry'; +import { + getPanelSize, + getPanelTitle, + getPerPanelMap, + hasSMDomain, + isPointerOverPanelFn, + SmallMultipleScales, + SmallMultiplesDatum, + SmallMultiplesGroupBy, +} from '../../../../common/panel_utils'; import { Box, Font, maximiseFontSize } from '../../../../common/text_utils'; import { ScaleType } from '../../../../scales/constants'; import { LinearScale, OrdinalScale, RasterTimeScale } from '../../../../specs'; import { TextMeasure } from '../../../../utils/bbox/canvas_text_bbox_calculator'; import { addIntervalToTime, roundDateToESInterval } from '../../../../utils/chrono/elasticsearch'; -import { clamp, Datum, isFiniteNumber } from '../../../../utils/common'; +import { clamp, Datum, isFiniteNumber, isNil } from '../../../../utils/common'; import { innerPad, pad } from '../../../../utils/dimensions'; import { Logger } from '../../../../utils/logger'; import { HeatmapStyle, Theme, Visible } from '../../../../utils/themes/theme'; import { PrimitiveValue } from '../../../partition_chart/layout/utils/group_by_rollup'; +import { ChartDimensions } from '../../../xy_chart/utils/dimensions'; import { HeatmapSpec } from '../../specs'; -import { ChartElementSizes, HeatmapTable } from '../../state/selectors/compute_chart_dimensions'; +import { ChartElementSizes } from '../../state/selectors/compute_chart_element_sizes'; import { ColorScale } from '../../state/selectors/get_color_scale'; +import { HeatmapTable } from '../../state/selectors/get_heatmap_table'; import { Cell, GridCell, + HeatmapTitleConfig, PickCursorBand, PickDragFunction, PickDragShapeFunction, @@ -38,13 +50,16 @@ import { } from '../types/viewmodel_types'; /** @public */ -export interface HeatmapCellDatum { +export interface HeatmapCellDatum extends SmallMultiplesDatum { x: NonNullable; y: NonNullable; value: number; originalIndex: number; } +type CellMap = Map; +type PanelCellMap = Map; + function getValuesInRange( values: NonNullable[], startValue: NonNullable, @@ -55,19 +70,27 @@ function getValuesInRange( return values.slice(startIndex, endIndex); } +/** @internal */ +export function clampWithOffset(value: number, lowerBound: number, upperBound: number, offset: number): number { + return clamp(value, lowerBound + offset, upperBound + offset) - offset; +} + /** @internal */ export function shapeViewModel( textMeasure: TextMeasure, spec: HeatmapSpec, - { heatmap: heatmapTheme, axes: { axisTitle }, background }: Theme, + { heatmap: heatmapTheme, axes: { axisTitle, axisPanelTitle }, background }: Theme, + { chartDimensions }: ChartDimensions, elementSizes: ChartElementSizes, heatmapTable: HeatmapTable, colorScale: ColorScale, + smScales: SmallMultipleScales, + groupBySpec: SmallMultiplesGroupBy, bandsToHide: Array<[number, number]>, ): ShapeViewModel { - const gridStrokeWidth = heatmapTheme.grid.stroke.width ?? 1; - const { table, yValues, xValues } = heatmapTable; + const gridStrokeWidth = heatmapTheme.grid.stroke.width; + const isPointerOverPanel = isPointerOverPanelFn(smScales, chartDimensions, gridStrokeWidth); // measure the text width of all rows values to get the grid area width const boxedYValues = yValues.map }>((value) => ({ @@ -77,31 +100,26 @@ export function shapeViewModel( ...heatmapTheme.yAxisLabel, })); + const panelSize = getPanelSize(smScales); + // compute the scale for the rows positions - const yScale = scaleBand>().domain(yValues).range([0, elementSizes.fullHeatmapHeight]); + const yScale = scaleBand>().domain(yValues).range([0, panelSize.height]); - const yInvertedScale = scaleQuantize>() - .domain([0, elementSizes.fullHeatmapHeight]) - .range(yValues); + const yInvertedScale = scaleQuantize>().domain([0, panelSize.height]).range(yValues); // compute the scale for the columns positions - const xScale = scaleBand>().domain(xValues).range([0, elementSizes.grid.width]); - - const xInvertedScale = scaleQuantize>() - .domain([0, elementSizes.grid.width]) - .range(xValues); + const xScale = scaleBand>().domain(xValues).range([0, panelSize.width]); + const xInvertedScale = scaleQuantize>().domain([0, panelSize.width]).range(xValues); - // compute the cell width (can be smaller then the available size depending on config + // compute the cell width, can be smaller then the available size depending on config const cellWidth = heatmapTheme.cell.maxWidth !== 'fill' && xScale.bandwidth() > heatmapTheme.cell.maxWidth ? heatmapTheme.cell.maxWidth : xScale.bandwidth(); - // compute the cell height (we already computed the max size for that) + // compute the cell height, we already computed the max size for that const cellHeight = yScale.bandwidth(); - const currentGridHeight = elementSizes.grid.height; - // compute the position of each column label const textXValues = getXTicks(spec, heatmapTheme.xAxisLabel, xScale, heatmapTable.xValues); @@ -129,8 +147,10 @@ export function shapeViewModel( ); } + let tableMinFontSize = Infinity; + // compute each available cell position, color and value - const cellMap = table.reduce>((acc, d) => { + const panelCellMap = table.reduce((acc, d) => { const x = xScale(String(d.x)); const y = yScale(String(d.y)); const yIndex = yValues.indexOf(d.y); @@ -139,6 +159,7 @@ export function shapeViewModel( return acc; } const cellBackgroundColor = colorScale(d.value); + const panelKey = getPanelKey(d.smHorizontalAccessorValue, d.smVerticalAccessorValue); const cellKey = getCellKey(d.x, d.y); const formattedValue = spec.valueFormatter(d.value); @@ -153,8 +174,13 @@ export function shapeViewModel( cellWidthInner - 6, cellHeightInner - 6, ); + tableMinFontSize = Math.min(tableMinFontSize, fontSize); + + const cellMap = acc.get(panelKey) ?? new Map(); + + if (!acc.has(panelKey)) acc.set(panelKey, cellMap); - acc[cellKey] = { + cellMap.set(cellKey, { x: (heatmapTheme.cell.maxWidth !== 'fill' ? x + xScale.bandwidth() / 2 - heatmapTheme.cell.maxWidth / 2 : x) + gridStrokeWidth / 2, @@ -175,9 +201,40 @@ export function shapeViewModel( formatted: formattedValue, fontSize, textColor: fillTextColor(background.fallbackColor, cellBackgroundColor, background.color), - }; + }); return acc; - }, {}); + }, new Map()); + + const getScaledSMValue = (value: number | string, scale: 'horizontal' | 'vertical') => { + return hasSMDomain(smScales[scale]) ? smScales[scale].scale(value) : 0; + }; + + const getPanelPointCoordinate = (value: Pixels, scale: 'horizontal' | 'vertical') => { + const category = smScales[scale].invert(value) ?? ''; + const panelOffset = getScaledSMValue(category, scale); + const invertedScale = scale === 'horizontal' ? xInvertedScale : yInvertedScale; + + return { + category, + panelOffset, + panelPixelValue: value - panelOffset, + panelValue: invertedScale(value - panelOffset), + }; + }; + + const getPanelPointCoordinates = (x: Pixels, y: Pixels) => { + const { category: v, panelValue: panelY, panelOffset: panelOffsetY } = getPanelPointCoordinate(y, 'vertical'); + const { category: h, panelValue: panelX, panelOffset: panelOffsetX } = getPanelPointCoordinate(x, 'horizontal'); + + return { + x: panelX, + y: panelY, + v, + h, + panelOffsetY, + panelOffsetX, + }; + }; /** * Returns the corresponding x & y values of grid cell from the x & y positions @@ -185,11 +242,11 @@ export function shapeViewModel( * @param y */ const pickGridCell = (x: Pixels, y: Pixels): GridCell | undefined => { - if (x < elementSizes.grid.left || y < elementSizes.grid.top) return undefined; - if (x > elementSizes.grid.width + elementSizes.grid.left || y > elementSizes.grid.top + elementSizes.grid.height) + if (x < chartDimensions.left || y < chartDimensions.top) return undefined; + if (x > chartDimensions.width + chartDimensions.left || y > chartDimensions.top + chartDimensions.height) return undefined; - const xValue = xInvertedScale(x - elementSizes.grid.left); + const xValue = xInvertedScale(x - chartDimensions.left); const yValue = yInvertedScale(y); if (xValue === undefined || yValue === undefined) return undefined; @@ -205,9 +262,9 @@ export function shapeViewModel( const pickQuads = (x: Pixels, y: Pixels): Array | TextBox => { if ( x > 0 && - x < elementSizes.grid.left && - y > elementSizes.grid.top && - y < elementSizes.grid.top + elementSizes.grid.height + x < chartDimensions.left && + y > chartDimensions.top && + y < chartDimensions.top + chartDimensions.height ) { // look up for a Y axis elements const yLabelKey = yInvertedScale(y); @@ -217,22 +274,21 @@ export function shapeViewModel( } } - if (x < elementSizes.grid.left || y < elementSizes.grid.top) { + if (!isPointerOverPanel({ x, y })) { return []; } - if (x > elementSizes.grid.width + elementSizes.grid.left || y > elementSizes.grid.top + elementSizes.grid.height) { - return []; - } - const xValue = xInvertedScale(x - elementSizes.grid.left); - const yValue = yInvertedScale(y); + + const { x: xValue, y: yValue, h, v } = getPanelPointCoordinates(x - chartDimensions.left, y); + if (xValue === undefined || yValue === undefined) { return []; } + + const panelKey = getPanelKey(h, v); const cellKey = getCellKey(xValue, yValue); - const cell = cellMap[cellKey]; - if (cell) { - return [cell]; - } + const cell = panelCellMap.get(panelKey)?.get(cellKey); + + if (cell) return [cell]; return []; }; @@ -242,14 +298,27 @@ export function shapeViewModel( const pickDragArea: PickDragFunction = (bound) => { const [start, end] = bound; - const { left, top, width } = elementSizes.grid; + const { left, top } = chartDimensions; const topLeft = [Math.min(start.x, end.x) - left, Math.min(start.y, end.y) - top]; const bottomRight = [Math.max(start.x, end.x) - left, Math.max(start.y, end.y) - top]; - const startX = xInvertedScale(clamp(topLeft[0], 0, width)); - const endX = xInvertedScale(clamp(bottomRight[0], 0, width)); - const startY = yInvertedScale(clamp(topLeft[1], 0, currentGridHeight - 1)); - const endY = yInvertedScale(clamp(bottomRight[1], 0, currentGridHeight - 1)); + // Find panel based on start pointer + const { category: smHorizontalAccessorValue, panelOffset: hOffset } = getPanelPointCoordinate( + start.x, + 'horizontal', + ); + const { category: smVerticalAccessorValue, panelOffset: vOffset } = getPanelPointCoordinate(start.y, 'vertical'); + + // confine selection to start panel + const panelStartX = clampWithOffset(topLeft[0], 0, panelSize.width, hOffset); + const panelStartY = clampWithOffset(topLeft[1], 0, panelSize.height, vOffset); + const panelEndX = clampWithOffset(bottomRight[0], 0, panelSize.width, hOffset); + const panelEndY = clampWithOffset(bottomRight[1], 0, panelSize.height, vOffset); + + const startX = xInvertedScale(panelStartX); + const startY = yInvertedScale(panelStartY); + const endX = xInvertedScale(panelEndX); + const endY = yInvertedScale(panelEndY); const allXValuesInRange: Array> = getValuesInRange(xValues, startX, endX); const allYValuesInRange: Array> = getValuesInRange(yValues, startY, endY); @@ -261,8 +330,10 @@ export function shapeViewModel( allXValuesInRange.forEach((x) => { allYValuesInRange.forEach((y) => { + const panelKey = getPanelKey(smHorizontalAccessorValue, smVerticalAccessorValue); const cellKey = getCellKey(x, y); - cells.push(cellMap[cellKey]); + const cellValue = panelCellMap.get(panelKey)?.get(cellKey); + if (cellValue) cells.push(cellValue); }); }); @@ -270,6 +341,8 @@ export function shapeViewModel( cells: cells.filter(Boolean), x: invertedXValues, y: allYValuesInRange, + smHorizontalAccessorValue, + smVerticalAccessorValue, }; }; @@ -281,14 +354,13 @@ export function shapeViewModel( const pickHighlightedArea: PickHighlightedArea = ( x: Array>, y: Array>, + smHorizontalAccessorValue?: string | number, + smVerticalAccessorValue?: string | number, ) => { const startValue = x[0]; const endValue = x[x.length - 1]; - - const leftIndex = - typeof startValue === 'number' ? bisectLeft(xValues as number[], startValue) : xValues.indexOf(startValue); - const rightIndex = - typeof endValue === 'number' ? bisectLeft(xValues as number[], endValue) : xValues.indexOf(endValue) + 1; + const leftIndex = xValues.indexOf(startValue); + const rightIndex = xValues.indexOf(endValue) + (isRasterTimeScale(spec.xScale) && x.length > 1 ? 0 : 1); const isRightOutOfRange = rightIndex > xValues.length - 1 || rightIndex < 0; const isLeftOutOfRange = leftIndex > xValues.length - 1 || leftIndex < 0; @@ -300,7 +372,12 @@ export function shapeViewModel( return null; } - const xStart = elementSizes.grid.left + startFromScale; + const panelXOffset = isNil(smHorizontalAccessorValue) + ? 0 + : getScaledSMValue(smHorizontalAccessorValue, 'horizontal'); + const panelYOffset = isNil(smVerticalAccessorValue) ? 0 : getScaledSMValue(smVerticalAccessorValue, 'vertical'); + + const xStart = chartDimensions.left + startFromScale + panelXOffset; // extend the range in case the right boundary has been selected const width = endFromScale - startFromScale + (isRightOutOfRange || isLeftOutOfRange ? cellWidth : 0); @@ -311,7 +388,7 @@ export function shapeViewModel( .reduce( (acc, current, i) => { if (i === 0) { - acc.y = yScale(current) || 0; + acc.y = (yScale(current) || 0) + panelYOffset; } acc.totalHeight += cellHeight; return acc; @@ -330,8 +407,8 @@ export function shapeViewModel( * Resolves coordinates and metrics of the selected rect area. */ const pickDragShape: PickDragShapeFunction = (bound) => { - const area = pickDragArea(bound); - return pickHighlightedArea(area.x, area.y); + const { x, y, smHorizontalAccessorValue, smVerticalAccessorValue } = pickDragArea(bound); + return pickHighlightedArea(x, y, smHorizontalAccessorValue, smVerticalAccessorValue); }; const pickCursorBand: PickCursorBand = (x) => { @@ -346,9 +423,9 @@ export function shapeViewModel( ? undefined : { width: cellWidth, - x: elementSizes.grid.left + (xScale(xValues[index]) ?? NaN), - y: elementSizes.grid.top, - height: elementSizes.grid.height, + x: chartDimensions.left + (xScale(xValues[index]) ?? NaN), + y: chartDimensions.top, + height: chartDimensions.height, }; }; @@ -356,23 +433,21 @@ export function shapeViewModel( const xLines = Array.from({ length: xValues.length + 1 }, (d, i) => { const xAxisExtension = i % elementSizes.xAxisTickCadence === 0 ? 5 : 0; return { - x1: elementSizes.grid.left + i * cellWidth, - x2: elementSizes.grid.left + i * cellWidth, - y1: elementSizes.grid.top, - y2: currentGridHeight + xAxisExtension, + x1: i * cellWidth, + x2: i * cellWidth, + y1: 0, + y2: panelSize.height + xAxisExtension, }; }); // horizontal lines - const yLines = Array.from({ length: elementSizes.visibleNumberOfRows + 1 }, (d, i) => ({ - x1: elementSizes.grid.left, - x2: elementSizes.grid.left + elementSizes.grid.width, - y1: elementSizes.grid.top + i * cellHeight, - y2: elementSizes.grid.top + i * cellHeight, + const yLines = Array.from({ length: yValues.length + 1 }, (d, i) => ({ + x1: 0, + x2: panelSize.width, + y1: i * cellHeight, + y2: i * cellHeight, })); - const cells = Object.values(cellMap); - const tableMinFontSize = cells.reduce((acc, { fontSize }) => Math.min(acc, fontSize), Infinity); // TODO introduce missing styles into axes.axisTitle const axisTitleFont: Visible & Font & { fontSize: Pixels } = { visible: axisTitle.visible, @@ -384,52 +459,116 @@ export function shapeViewModel( fontSize: axisTitle.fontSize, }; + const axisPanelTitleFont: Visible & Font & { fontSize: Pixels } = { + visible: axisPanelTitle.visible, + fontFamily: axisPanelTitle.fontFamily, + fontStyle: axisPanelTitle.fontStyle ?? 'normal', + fontVariant: 'normal', + fontWeight: 'bold', + textColor: axisPanelTitle.fill, + fontSize: axisPanelTitle.fontSize, + }; + return { theme: heatmapTheme, - heatmapViewModel: { - gridOrigin: { - x: elementSizes.grid.left, - y: elementSizes.grid.top, - }, - gridLines: { - x: xLines, - y: yLines, - stroke: { - color: colorToRgba(heatmapTheme.grid.stroke.color), - width: gridStrokeWidth, - }, - }, - pageSize: elementSizes.visibleNumberOfRows, - cells, - cellFontSize: (cell: Cell) => (heatmapTheme.cell.label.useGlobalMinFontSize ? tableMinFontSize : cell.fontSize), - xValues: textXValues, - yValues: textYValues, - titles: [ - { + heatmapViewModels: getPerPanelMap(smScales, (anchor, h, v) => { + const primaryColumn = smScales.vertical.domain[0] === v; + const primaryRow = smScales.horizontal.domain[0] === h; + const lastColumn = smScales.vertical.domain[smScales.vertical.domain.length - 1] === v; + + const titles: HeatmapTitleConfig[] = []; + // TODO this should be filtered by the pageSize AND the pageNumber + const cells = [...(panelCellMap.get(getPanelKey(h, v))?.values() ?? [])]; + + if (primaryColumn && primaryRow) { + if (spec.xAxisTitle) { + const axisPanelTitleHeight = + groupBySpec.horizontal && axisPanelTitle.visible + ? axisPanelTitle.fontSize + innerPad(axisPanelTitle.padding) / 2 + : 0; + + titles.push({ + origin: { + x: chartDimensions.width / 2, + y: + chartDimensions.top + + chartDimensions.height + + elementSizes.xAxis.height + + axisPanelTitleHeight + + innerPad(axisTitle.padding) / 2 + + axisTitle.fontSize / 2, + }, + ...axisTitleFont, + text: spec.xAxisTitle, + rotation: 0, + }); + } + + if (spec.yAxisTitle) { + titles.push({ + origin: { + x: -chartDimensions.left + axisTitle.fontSize / 2, + y: chartDimensions.top + chartDimensions.height / 2, + }, + ...axisTitleFont, + text: spec.yAxisTitle, + rotation: -90, + }); + } + } + + if (primaryColumn && groupBySpec.horizontal) { + titles.push({ origin: { - x: elementSizes.grid.left + elementSizes.grid.width / 2, + x: panelSize.width / 2, y: - elementSizes.grid.top + - elementSizes.grid.height + + chartDimensions.top + + chartDimensions.height + elementSizes.xAxis.height + - innerPad(axisTitle.padding) + - axisTitle.fontSize / 2, + innerPad(axisPanelTitle.padding) + + axisPanelTitle.fontSize / 2, }, - ...axisTitleFont, - text: spec.xAxisTitle, + ...axisPanelTitleFont, + text: getPanelTitle(false, v, h, groupBySpec), rotation: 0, - }, - { + }); + } + + if (primaryRow && groupBySpec.vertical) { + const axisTitleWidth = axisTitle.visible ? axisTitle.fontSize + innerPad(axisTitle.padding) : 0; + titles.push({ origin: { - x: elementSizes.yAxis.left - innerPad(axisTitle.padding) - axisTitle.fontSize / 2, - y: elementSizes.grid.top + elementSizes.grid.height / 2, + x: -chartDimensions.left + axisTitleWidth + axisPanelTitle.fontSize / 2, + y: chartDimensions.top + panelSize.height / 2, }, - ...axisTitleFont, - text: spec.yAxisTitle, + ...axisPanelTitleFont, + text: getPanelTitle(true, v, h, groupBySpec), rotation: -90, + }); + } + + return { + anchor, + panelSize, + gridOrigin: { + x: anchor.x + chartDimensions.left, + y: anchor.y + chartDimensions.top, }, - ], - }, + gridLines: { + x: xLines, + y: yLines, + stroke: { + color: colorToRgba(heatmapTheme.grid.stroke.color), + width: gridStrokeWidth, + }, + }, + cells, + cellFontSize: (cell: Cell) => (heatmapTheme.cell.label.useGlobalMinFontSize ? tableMinFontSize : cell.fontSize), + xValues: lastColumn ? textXValues : [], + yValues: primaryRow ? textYValues : [], + titles, + }; + }), pickGridCell, pickQuads, pickDragArea, @@ -443,6 +582,10 @@ function getCellKey(x: NonNullable, y: NonNullable = '', v: NonNullable = '') { + return [String(h), String(v)].join('&_&'); +} + /** @internal */ export function isValueInRanges(value: number, ranges: Array<[number, number]>) { return ranges.some(([min, max]) => min <= value && value < max); diff --git a/packages/charts/src/chart_types/heatmap/renderer/canvas/canvas_renderers.ts b/packages/charts/src/chart_types/heatmap/renderer/canvas/canvas_renderers.ts index 08bb492482..247224aa82 100644 --- a/packages/charts/src/chart_types/heatmap/renderer/canvas/canvas_renderers.ts +++ b/packages/charts/src/chart_types/heatmap/renderer/canvas/canvas_renderers.ts @@ -6,29 +6,27 @@ * Side Public License, v 1. */ +import { ReactiveChartStateProps } from './connected_component'; import { getColorBandStyle, getGeometryStateStyle } from './utils'; -import { Color } from '../../../../common/colors'; import { clearCanvas, renderLayers, withContext } from '../../../../renderers/canvas'; import { radToDeg } from '../../../../utils/common'; import { horizontalPad } from '../../../../utils/dimensions'; -import { SharedGeometryStateStyle } from '../../../../utils/themes/theme'; import { renderMultiLine } from '../../../xy_chart/renderer/canvas/primitives/line'; import { renderRect } from '../../../xy_chart/renderer/canvas/primitives/rect'; import { renderText, TextFont, wrapLines } from '../../../xy_chart/renderer/canvas/primitives/text'; -import { ShapeViewModel } from '../../layout/types/viewmodel_types'; -import { ChartElementSizes } from '../../state/selectors/compute_chart_dimensions'; /** @internal */ -export function renderCanvas2d( - ctx: CanvasRenderingContext2D, - dpr: number, - { theme, heatmapViewModel }: ShapeViewModel, - sharedGeometryStyle: SharedGeometryStateStyle, - background: Color, - elementSizes: ChartElementSizes, - debug: boolean, - highlightedLegendBands: Array<[start: number, end: number]>, -) { +export function renderHeatmapCanvas2d(ctx: CanvasRenderingContext2D, dpr: number, props: ReactiveChartStateProps) { + const { theme } = props.geometries; + const { heatmapViewModels } = props.geometries; + const { + theme: { sharedStyle: sharedGeometryStyle }, + background, + elementSizes, + highlightedLegendBands, + } = props; + if (heatmapViewModels.length === 0) return; + withContext(ctx, () => { // set some defaults for the overall rendering @@ -49,151 +47,145 @@ export function renderCanvas2d( // - in any case, it's possible to refactor for a -y = North convention if that's deemed preferable // ctx.scale(1, -1); - // TODO this should be filtered by the pageSize AND the pageNumber - const filteredCells = heatmapViewModel.cells.filter((cell) => cell.yIndex < heatmapViewModel.pageSize); - const filteredYValues = heatmapViewModel.yValues.filter((value, yIndex) => yIndex < heatmapViewModel.pageSize); - renderLayers(ctx, [ () => clearCanvas(ctx, background), - () => - debug && - withContext(ctx, () => { - ctx.strokeStyle = 'black'; - ctx.strokeRect( - elementSizes.grid.left, - elementSizes.grid.top, - elementSizes.grid.width, - elementSizes.grid.height, - ); - ctx.strokeStyle = 'red'; - ctx.strokeRect( - elementSizes.xAxis.left, - elementSizes.xAxis.top, - elementSizes.xAxis.width, - elementSizes.xAxis.height, - ); - - ctx.strokeStyle = 'violet'; - ctx.strokeRect( - elementSizes.yAxis.left, - elementSizes.yAxis.top, - elementSizes.yAxis.width, - elementSizes.yAxis.height, - ); - }), () => { // Grid - withContext(ctx, () => { - renderMultiLine(ctx, heatmapViewModel.gridLines.x, heatmapViewModel.gridLines.stroke); - renderMultiLine(ctx, heatmapViewModel.gridLines.y, heatmapViewModel.gridLines.stroke); + heatmapViewModels.forEach(({ gridOrigin: { x, y }, gridLines }) => { + withContext(ctx, () => { + ctx.translate(x, y); + renderMultiLine(ctx, gridLines.x, gridLines.stroke); + renderMultiLine(ctx, gridLines.y, gridLines.stroke); + }); }); }, () => // Cells - withContext(ctx, () => { - const { x, y } = heatmapViewModel.gridOrigin; - ctx.translate(x, y); - filteredCells.forEach((cell) => { - if (cell.visible) { - const geometryStateStyle = getGeometryStateStyle(cell, sharedGeometryStyle, highlightedLegendBands); - const style = getColorBandStyle(cell, geometryStateStyle); - renderRect(ctx, cell, style.fill, style.stroke); - } + heatmapViewModels.forEach(({ gridOrigin: { x, y }, cells }) => { + withContext(ctx, () => { + ctx.translate(x, y); + cells.forEach((cell) => { + if (cell.visible) { + const geometryStateStyle = getGeometryStateStyle(cell, sharedGeometryStyle, highlightedLegendBands); + const style = getColorBandStyle(cell, geometryStateStyle); + renderRect(ctx, cell, style.fill, style.stroke); + } + }); }); }), - () => - theme.cell.label.visible && - withContext(ctx, () => { - // Text on cells - const { x, y } = heatmapViewModel.gridOrigin; - ctx.translate(x, y); - filteredCells.forEach((cell) => { - const fontSize = heatmapViewModel.cellFontSize(cell); - if (cell.visible && Number.isFinite(fontSize)) - renderText(ctx, { x: cell.x + cell.width / 2, y: cell.y + cell.height / 2 }, cell.formatted, { - ...theme.cell.label, - fontSize, - align: 'center', - baseline: 'middle', - textColor: cell.textColor, - }); - }); - }), + // Text on cells + () => { + if (!theme.cell.label.visible) return; - () => - // render text on Y axis - theme.yAxisLabel.visible && - withContext(ctx, () => { - // the text is right aligned so the canvas needs to be aligned to the right of the Y axis box - ctx.translate(elementSizes.yAxis.left + elementSizes.yAxis.width, elementSizes.yAxis.top); - const font: TextFont = { ...theme.yAxisLabel, baseline: 'middle' /* fixed */, align: 'right' /* fixed */ }; - const { padding } = theme.yAxisLabel; - const horizontalPadding = horizontalPad(padding); - filteredYValues.forEach(({ x, y, text }) => { - const textLines = wrapLines( - ctx, - text, - font, - theme.yAxisLabel.fontSize, - Math.max(elementSizes.yAxis.width - horizontalPadding, 0), - theme.yAxisLabel.fontSize, - { shouldAddEllipsis: true, wrapAtWord: false }, - ).lines; - // TODO improve the `wrapLines` code to handle results with short width - renderText(ctx, { x, y }, textLines.length > 0 ? textLines[0] : '…', font); + heatmapViewModels.forEach(({ cellFontSize, gridOrigin: { x, y }, cells }) => { + withContext(ctx, () => { + ctx.translate(x, y); + cells.forEach((cell) => { + const fontSize = cellFontSize(cell); + if (cell.visible && Number.isFinite(fontSize)) + renderText(ctx, { x: cell.x + cell.width / 2, y: cell.y + cell.height / 2 }, cell.formatted, { + ...theme.cell.label, + fontSize, + align: 'center', + baseline: 'middle', + textColor: cell.textColor, + }); + }); }); - }), + }); + }, - () => - // render text on X axis - theme.xAxisLabel.visible && - withContext(ctx, () => { - ctx.translate(elementSizes.xAxis.left, elementSizes.xAxis.top); - heatmapViewModel.xValues - .filter((_, i) => i % elementSizes.xAxisTickCadence === 0) - .forEach(({ x, y, text, align }) => { + // render text on Y axis + () => { + if (!theme.yAxisLabel.visible) return; + + heatmapViewModels.forEach(({ yValues, gridOrigin: { x, y } }) => { + withContext(ctx, () => { + ctx.translate(x, y); + const font: TextFont = { + ...theme.yAxisLabel, + baseline: 'middle' /* fixed */, + align: 'right' /* fixed */, + }; + const { padding } = theme.yAxisLabel; + const horizontalPadding = horizontalPad(padding); + yValues.forEach(({ x, y, text }) => { const textLines = wrapLines( ctx, text, - theme.xAxisLabel, - theme.xAxisLabel.fontSize, - // TODO wrap into multilines - Infinity, - 16, + font, + theme.yAxisLabel.fontSize, + Math.max(elementSizes.yAxis.width - horizontalPadding, 0), + theme.yAxisLabel.fontSize, { shouldAddEllipsis: true, wrapAtWord: false }, ).lines; - renderText( - ctx, - { x, y }, - textLines.length > 0 ? textLines[0] : '…', - { ...theme.xAxisLabel, baseline: 'middle', align }, - // negative rotation due to the canvas rotation direction - radToDeg(-elementSizes.xLabelRotation), - ); + // TODO improve the `wrapLines` code to handle results with short width + renderText(ctx, { x, y }, textLines.length > 0 ? textLines[0] : '…', font); }); - }), + }); + }); + }, + + // render text on X axis + () => { + if (!theme.xAxisLabel.visible) return; + heatmapViewModels.forEach(({ xValues, gridOrigin: { x, y } }) => { + withContext(ctx, () => { + ctx.translate(x, y + elementSizes.xAxis.top); + xValues + .filter((_, i) => i % elementSizes.xAxisTickCadence === 0) + .forEach(({ x, y, text, align }) => { + const textLines = wrapLines( + ctx, + text, + theme.xAxisLabel, + theme.xAxisLabel.fontSize, + // TODO wrap into multilines + Infinity, + 16, + { shouldAddEllipsis: true, wrapAtWord: false }, + ).lines; + renderText( + ctx, + { x, y }, + textLines.length > 0 ? textLines[0] : '…', + { ...theme.xAxisLabel, baseline: 'middle', align }, + // negative rotation due to the canvas rotation direction + radToDeg(-elementSizes.xLabelRotation), + ); + }); + }); + }); + }, + + // render axes and panel titles () => - withContext(ctx, () => { - heatmapViewModel.titles - .filter((t) => t.visible && t.text !== '') - .forEach((title) => { - renderText( - ctx, - title.origin, - title.text, - { - ...title, - baseline: 'middle', - align: 'center', - }, - title.rotation, - ); + heatmapViewModels + .filter(({ titles }) => titles.length > 0) + .forEach(({ titles, gridOrigin: { x, y } }) => { + withContext(ctx, () => { + ctx.translate(x, y); + titles + .filter((t) => t.visible && t.text !== '') + .forEach((title) => { + renderText( + ctx, + title.origin, + title.text, + { + ...title, + baseline: 'middle', + align: 'center', + }, + title.rotation, + ); + }); }); - }), + }), ]); }); } diff --git a/packages/charts/src/chart_types/heatmap/renderer/canvas/connected_component.tsx b/packages/charts/src/chart_types/heatmap/renderer/canvas/connected_component.tsx index 009a42307e..7b87c5114a 100644 --- a/packages/charts/src/chart_types/heatmap/renderer/canvas/connected_component.tsx +++ b/packages/charts/src/chart_types/heatmap/renderer/canvas/connected_component.tsx @@ -10,7 +10,7 @@ import React, { RefObject } from 'react'; import { connect } from 'react-redux'; import { bindActionCreators, Dispatch } from 'redux'; -import { renderCanvas2d } from './canvas_renderers'; +import { renderHeatmapCanvas2d } from './canvas_renderers'; import { Color, Colors } from '../../../../common/colors'; import { ScreenReaderSummary } from '../../../../components/accessibility'; import { onChartRendered } from '../../../../state/actions/chart'; @@ -24,15 +24,17 @@ import { getChartThemeSelector } from '../../../../state/selectors/get_chart_the import { getInternalIsInitializedSelector, InitStatus } from '../../../../state/selectors/get_internal_is_intialized'; import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_spec'; import { Dimensions } from '../../../../utils/dimensions'; +import { deepEqual } from '../../../../utils/fast_deep_equal'; import { LIGHT_THEME } from '../../../../utils/themes/light_theme'; import { Theme } from '../../../../utils/themes/theme'; import { nullShapeViewModel, ShapeViewModel } from '../../layout/types/viewmodel_types'; -import { ChartElementSizes, computeChartElementSizesSelector } from '../../state/selectors/compute_chart_dimensions'; -import { getHeatmapGeometries } from '../../state/selectors/geometries'; +import { ChartElementSizes, computeChartElementSizesSelector } from '../../state/selectors/compute_chart_element_sizes'; import { getHeatmapContainerSizeSelector } from '../../state/selectors/get_heatmap_container_size'; import { getHighlightedLegendBandsSelector } from '../../state/selectors/get_highlighted_legend_bands'; +import { getPerPanelHeatmapGeometries } from '../../state/selectors/get_per_panel_heatmap_geometries'; -interface ReactiveChartStateProps { +/** @internal */ +export interface ReactiveChartStateProps { initialized: boolean; geometries: ShapeViewModel; chartContainerDimensions: Dimensions; @@ -80,6 +82,10 @@ class Component extends React.Component { } } + shouldComponentUpdate(nextProps: ReactiveChartStateProps) { + return !deepEqual(this.props, nextProps); + } + componentDidUpdate() { if (!this.ctx) { this.tryCanvasContext(); @@ -97,19 +103,7 @@ class Component extends React.Component { private drawCanvas() { if (this.ctx) { - renderCanvas2d( - this.ctx, - this.devicePixelRatio, - { - ...this.props.geometries, - theme: this.props.geometries.theme, - }, - this.props.theme.sharedStyle, - this.props.background, - this.props.elementSizes, - this.props.debug, - this.props.highlightedLegendBands, - ); + renderHeatmapCanvas2d(this.ctx, this.devicePixelRatio, this.props); } } @@ -167,12 +161,8 @@ const DEFAULT_PROPS: ReactiveChartStateProps = { a11ySettings: DEFAULT_A11Y_SETTINGS, background: Colors.Transparent.keyword, elementSizes: { - grid: { width: 0, height: 0, left: 0, top: 0 }, xAxis: { width: 0, height: 0, left: 0, top: 0 }, yAxis: { width: 0, height: 0, left: 0, top: 0 }, - fullHeatmapHeight: 0, - rowHeight: 0, - visibleNumberOfRows: 0, xAxisTickCadence: 1, xLabelRotation: 0, }, @@ -185,7 +175,7 @@ const mapStateToProps = (state: GlobalChartState): ReactiveChartStateProps => { } return { initialized: true, - geometries: getHeatmapGeometries(state), + geometries: getPerPanelHeatmapGeometries(state), chartContainerDimensions: getHeatmapContainerSizeSelector(state), highlightedLegendBands: getHighlightedLegendBandsSelector(state), theme: getChartThemeSelector(state), diff --git a/packages/charts/src/chart_types/heatmap/renderer/dom/highlighter_brush.tsx b/packages/charts/src/chart_types/heatmap/renderer/dom/highlighter_brush.tsx index 9c2ff1673b..a7938b1cb1 100644 --- a/packages/charts/src/chart_types/heatmap/renderer/dom/highlighter_brush.tsx +++ b/packages/charts/src/chart_types/heatmap/renderer/dom/highlighter_brush.tsx @@ -12,26 +12,26 @@ import { DEFAULT_PROPS, HighlighterCellsComponent, HighlighterCellsProps } from import { GlobalChartState } from '../../../../state/chart_state'; import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; import { getInternalIsInitializedSelector, InitStatus } from '../../../../state/selectors/get_internal_is_intialized'; -import { computeChartElementSizesSelector } from '../../state/selectors/compute_chart_dimensions'; -import { getHeatmapGeometries } from '../../state/selectors/geometries'; +import { computeChartDimensionsSelector } from '../../state/selectors/compute_chart_dimensions'; import { getBrushedHighlightedShapesSelector } from '../../state/selectors/get_brushed_highlighted_shapes'; import { getHighlightedAreaSelector } from '../../state/selectors/get_highlighted_area'; +import { getPerPanelHeatmapGeometries } from '../../state/selectors/get_per_panel_heatmap_geometries'; const brushMapStateToProps = (state: GlobalChartState): HighlighterCellsProps => { if (getInternalIsInitializedSelector(state) !== InitStatus.Initialized) { return DEFAULT_PROPS; } - const { chartId } = state; - - const geoms = getHeatmapGeometries(state); - const canvasDimension = computeChartElementSizesSelector(state).grid; - let dragShape = getBrushedHighlightedShapesSelector(state); const highlightedArea = getHighlightedAreaSelector(state); + if (highlightedArea) { dragShape = highlightedArea; } + + const { chartId } = state; + const geoms = getPerPanelHeatmapGeometries(state); + const canvasDimension = computeChartDimensionsSelector(state).chartDimensions; const { brushMask, brushArea } = getChartThemeSelector(state).heatmap; return { diff --git a/packages/charts/src/chart_types/heatmap/specs/heatmap.ts b/packages/charts/src/chart_types/heatmap/specs/heatmap.ts index 4cb8c05f09..94daa1c941 100644 --- a/packages/charts/src/chart_types/heatmap/specs/heatmap.ts +++ b/packages/charts/src/chart_types/heatmap/specs/heatmap.ts @@ -11,6 +11,7 @@ import { ComponentProps } from 'react'; import { X_SCALE_DEFAULT } from './scale_defaults'; import { ChartType } from '../..'; import { Color } from '../../../common/colors'; +import { SmallMultiplesDatum } from '../../../common/panel_utils'; import { Predicate } from '../../../common/predicate'; import { ScaleType } from '../../../scales/constants'; import { BaseDatum, Spec } from '../../../specs'; @@ -45,10 +46,8 @@ export interface HeatmapBandsColorScale { } /** @public */ -export type HeatmapBrushEvent = { +export type HeatmapBrushEvent = HeatmapHighlightedData & { cells: Cell[]; - x: (string | number)[]; - y: (string | number)[]; }; /** @public */ export interface TimeScale { @@ -70,6 +69,14 @@ export interface OrdinalScale { type: typeof ScaleType.Ordinal; } +/** + * @public + */ +export interface HeatmapHighlightedData extends SmallMultiplesDatum { + x: Array; + y: Array; +} + /** @alpha */ export interface HeatmapSpec extends Spec { specType: typeof SpecType.Series; @@ -83,10 +90,9 @@ export interface HeatmapSpec extends Spec { xSortPredicate: Predicate; ySortPredicate: Predicate; xScale: RasterTimeScale | OrdinalScale | LinearScale; - highlightedData?: { x: Array; y: Array }; + highlightedData?: HeatmapHighlightedData; name?: string; timeZone: string; - onBrushEnd?: (brushArea: HeatmapBrushEvent) => void; xAxisTitle: string; xAxisLabelName: string; xAxisLabelFormatter: LabelAccessor; diff --git a/packages/charts/src/chart_types/heatmap/state/chart_state.tsx b/packages/charts/src/chart_types/heatmap/state/chart_state.tsx index 4c0f91a3dd..440da620f2 100644 --- a/packages/charts/src/chart_types/heatmap/state/chart_state.tsx +++ b/packages/charts/src/chart_types/heatmap/state/chart_state.tsx @@ -8,11 +8,12 @@ import React, { RefObject } from 'react'; -import { computeChartElementSizesSelector } from './selectors/compute_chart_dimensions'; +import { computeChartDimensionsSelector } from './selectors/compute_chart_dimensions'; import { computeLegendSelector } from './selectors/compute_legend'; import { getBrushAreaSelector } from './selectors/get_brush_area'; import { getPointerCursorSelector } from './selectors/get_cursor_pointer'; import { getDebugStateSelector } from './selectors/get_debug_state'; +import { getHeatmapTableSelector } from './selectors/get_heatmap_table'; import { getLegendItemsLabelsSelector } from './selectors/get_legend_items_labels'; import { getTooltipAnchorSelector } from './selectors/get_tooltip_anchor'; import { getSpecOrNull } from './selectors/heatmap_spec'; @@ -27,6 +28,7 @@ import { createOnElementOverCaller } from './selectors/on_element_over_caller'; import { createOnPointerUpdateCaller } from './selectors/on_pointer_update_caller'; import { getTooltipInfoSelector } from './selectors/tooltip'; import { ChartType } from '../..'; +import { SmallMultiplesSeriesDomains } from '../../../common/panel_utils'; import { BrushTool } from '../../../components/brush/brush'; import { Tooltip } from '../../../components/tooltip/tooltip'; import { InternalChartState, GlobalChartState, BackwardRef } from '../../../state/chart_state'; @@ -114,7 +116,7 @@ export class HeatmapState implements InternalChartState { } getMainProjectionArea(globalState: GlobalChartState): Dimensions { - return computeChartElementSizesSelector(globalState).grid; + return computeChartDimensionsSelector(globalState).chartDimensions; } getBrushArea(globalState: GlobalChartState): Dimensions | null { @@ -129,6 +131,10 @@ export class HeatmapState implements InternalChartState { return 'Heatmap chart'; } + getSmallMultiplesDomains(globalState: GlobalChartState): SmallMultiplesSeriesDomains { + return getHeatmapTableSelector(globalState); + } + eventCallbacks(globalState: GlobalChartState) { this.onElementOverCaller(globalState); this.onElementOutCaller(globalState); diff --git a/packages/charts/src/chart_types/heatmap/state/selectors/compute_axes_sizes.ts b/packages/charts/src/chart_types/heatmap/state/selectors/compute_axes_sizes.ts new file mode 100644 index 0000000000..819a849e96 --- /dev/null +++ b/packages/charts/src/chart_types/heatmap/state/selectors/compute_axes_sizes.ts @@ -0,0 +1,115 @@ +/* + * 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 { getHeatmapSpecSelector } from './get_heatmap_spec'; +import { getHeatmapTableSelector } from './get_heatmap_table'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { createCustomCachedSelector } from '../../../../state/create_selector'; +import { getScale } from '../../../../state/selectors/compute_small_multiple_scales'; +import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; +import { getInternalSmallMultiplesDomains } from '../../../../state/selectors/get_internal_sm_domains'; +import { getLegendSizeSelector } from '../../../../state/selectors/get_legend_size'; +import { getSmallMultiplesSpec } from '../../../../state/selectors/get_small_multiples_spec'; +import { withTextMeasure } from '../../../../utils/bbox/canvas_text_bbox_calculator'; +import { isHorizontalLegend } from '../../../../utils/legend'; +import { isRasterTimeScale } from '../../layout/viewmodel/viewmodel'; +import { getTextSizeDimension, getYAxisHorizontalUsedSpace, getXAxisSize } from '../utils/axis'; + +const getParentDimension = (state: GlobalChartState) => state.parentDimensions; + +/** + * Returns grid and axes sizes and positions. + * @internal + */ +export const computeAxesSizesSelector = createCustomCachedSelector( + [ + getParentDimension, + getLegendSizeSelector, + getHeatmapTableSelector, + getChartThemeSelector, + getHeatmapSpecSelector, + getSmallMultiplesSpec, + getInternalSmallMultiplesDomains, + ], + ( + container, + legendSize, + { yValues, xValues }, + { heatmap, axes: { axisTitle: axisTitleStyle, axisPanelTitle: axisPanelTitleStyle } }, + { xAxisTitle, yAxisTitle, xAxisLabelFormatter, yAxisLabelFormatter, xScale }, + smSpec, + { smHDomain }, + ) => { + // TODO find a cleaner way without circular dependencies + const panelWidth = getScale(smHDomain, container.width, smSpec?.style?.horizontalPanelPadding).bandwidth; + + return withTextMeasure((textMeasure) => { + const isLegendHorizontal = isHorizontalLegend(legendSize.position); + const legendWidth = !isLegendHorizontal ? legendSize.width + legendSize.margin * 2 : 0; + const legendHeight = isLegendHorizontal + ? heatmap.maxLegendHeight ?? legendSize.height + legendSize.margin * 2 + : 0; + + const yAxisTitleHorizontalSize = getTextSizeDimension(yAxisTitle, axisTitleStyle, textMeasure, 'height'); + const yAxisPanelTitleHorizontalSize = getTextSizeDimension( + yAxisTitle, + axisPanelTitleStyle, + textMeasure, + 'height', + !smSpec?.splitVertically, + ); + const yAxis = { + width: getYAxisHorizontalUsedSpace(yValues, heatmap.yAxisLabel, yAxisLabelFormatter, textMeasure), + }; + + const xAxisTitleVerticalSize = getTextSizeDimension(xAxisTitle, axisTitleStyle, textMeasure, 'height'); + const xAxisPanelTitleVerticalSize = getTextSizeDimension( + xAxisTitle, + axisPanelTitleStyle, + textMeasure, + 'height', + !smSpec?.splitHorizontally, + ); + const xAxis = getXAxisSize( + !isRasterTimeScale(xScale), + heatmap.xAxisLabel, + xAxisLabelFormatter, + xValues, + textMeasure, + panelWidth - legendWidth - heatmap.grid.stroke.width / 2, // we should consider also the grid width + [ + yAxisTitleHorizontalSize + yAxisPanelTitleHorizontalSize + yAxis.width, + 0, // this can be used if we have a right Y axis + ], + ); + + // TODO simplify this width calculation + const chartWidth = getXAxisSize( + !isRasterTimeScale(xScale), + heatmap.xAxisLabel, + xAxisLabelFormatter, + xValues, + textMeasure, + container.width - legendWidth - heatmap.grid.stroke.width / 2, // we should consider also the grid width + [ + yAxisTitleHorizontalSize + yAxisPanelTitleHorizontalSize + yAxis.width, + 0, // this can be used if we have a right Y axis + ], + ).width; + + return { + yAxis, + xAxis, + legendHeight, + xAxisTitleVerticalSize, + xAxisPanelTitleVerticalSize, + chartWidth, + }; + }); + }, +); diff --git a/packages/charts/src/chart_types/heatmap/state/selectors/compute_chart_dimensions.ts b/packages/charts/src/chart_types/heatmap/state/selectors/compute_chart_dimensions.ts index 4611ffb6ed..eaaf972617 100644 --- a/packages/charts/src/chart_types/heatmap/state/selectors/compute_chart_dimensions.ts +++ b/packages/charts/src/chart_types/heatmap/state/selectors/compute_chart_dimensions.ts @@ -6,420 +6,39 @@ * Side Public License, v 1. */ -import { scaleBand } from 'd3-scale'; - -import { getHeatmapSpecSelector } from './get_heatmap_spec'; -import { getHeatmapTableSelector } from './get_heatmap_table'; -import { Radian } from '../../../../common/geometry'; -import { extent } from '../../../../common/math'; -import { rotate2, sub2, Vec2 } from '../../../../common/vectors'; -import { screenspaceMarkerScaleCompressor } from '../../../../solvers/screenspace_marker_scale_compressor'; +import { computeAxesSizesSelector } from './compute_axes_sizes'; import { GlobalChartState } from '../../../../state/chart_state'; import { createCustomCachedSelector } from '../../../../state/create_selector'; import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; -import { getLegendSizeSelector } from '../../../../state/selectors/get_legend_size'; -import { TextMeasure, withTextMeasure } from '../../../../utils/bbox/canvas_text_bbox_calculator'; -import { degToRad, isFiniteNumber } from '../../../../utils/common'; -import { Dimensions, horizontalPad, innerPad, outerPad, pad, Size } from '../../../../utils/dimensions'; -import { isHorizontalLegend } from '../../../../utils/legend'; -import { AxisStyle, HeatmapStyle } from '../../../../utils/themes/theme'; -import { PrimitiveValue } from '../../../partition_chart/layout/utils/group_by_rollup'; -import { HeatmapCellDatum, isRasterTimeScale } from '../../layout/viewmodel/viewmodel'; -import { HeatmapSpec } from '../../specs/heatmap'; - -/** @internal */ -export interface HeatmapTable { - table: Array; - yValues: Array; - xValues: Array; - xNumericExtent: [number, number]; - extent: [number, number]; -} +import { ChartDimensions } from '../../../xy_chart/utils/dimensions'; const getParentDimension = (state: GlobalChartState) => state.parentDimensions; -/** @internal */ -export type ChartElementSizes = { - yAxis: Dimensions; - xAxis: Dimensions; - grid: Dimensions; - fullHeatmapHeight: number; - rowHeight: number; - visibleNumberOfRows: number; - xAxisTickCadence: number; - xLabelRotation: number; -}; - /** - * Returns grid and axes sizes and positions. + * Returns chart dimensions axes sizes and positions. * @internal */ -export const computeChartElementSizesSelector = createCustomCachedSelector( - [getParentDimension, getLegendSizeSelector, getHeatmapTableSelector, getChartThemeSelector, getHeatmapSpecSelector], - ( - container, - legendSize, - { yValues, xValues }, - { heatmap, axes: { axisTitle: axisTitleStyle } }, - { xAxisTitle, yAxisTitle, xAxisLabelFormatter, yAxisLabelFormatter, xScale }, - ): ChartElementSizes => { - return withTextMeasure((textMeasure) => { - const isLegendHorizontal = isHorizontalLegend(legendSize.position); - const legendWidth = !isLegendHorizontal ? legendSize.width + legendSize.margin * 2 : 0; - const legendHeight = isLegendHorizontal - ? heatmap.maxLegendHeight ?? legendSize.height + legendSize.margin * 2 - : 0; - - const yAxisTitleHorizontalSize = getTextSizeDimension(yAxisTitle, axisTitleStyle, textMeasure, 'height'); - const yAxisWidth = getYAxisHorizontalUsedSpace(yValues, heatmap.yAxisLabel, yAxisLabelFormatter, textMeasure); - - const xAxisTitleVerticalSize = getTextSizeDimension(xAxisTitle, axisTitleStyle, textMeasure, 'height'); - const xAxisSize = getXAxisSize( - !isRasterTimeScale(xScale), - heatmap.xAxisLabel, - xAxisLabelFormatter, - xValues, - textMeasure, - container.width - legendWidth - heatmap.grid.stroke.width / 2, // we should consider also the grid width - [ - yAxisTitleHorizontalSize + yAxisWidth, - 0, // this can be used if we have a right Y axis - ], - ); - - const availableHeightForGrid = - container.height - xAxisTitleVerticalSize - xAxisSize.height - legendHeight - heatmap.grid.stroke.width / 2; - - const rowHeight = getGridCellHeight(yValues.length, heatmap.grid, availableHeightForGrid); - const fullHeatmapHeight = rowHeight * yValues.length; - - const visibleNumberOfRows = - rowHeight > 0 && fullHeatmapHeight > availableHeightForGrid - ? Math.floor(availableHeightForGrid / rowHeight) - : yValues.length; - - const grid: Dimensions = { - width: xAxisSize.width, - height: visibleNumberOfRows * rowHeight - heatmap.grid.stroke.width / 2, - left: container.left + xAxisSize.left, - top: container.top + heatmap.grid.stroke.width / 2, - }; - - const yAxis: Dimensions = { - width: yAxisWidth, - height: grid.height, - top: grid.top, - left: grid.left - yAxisWidth, - }; - - const xAxis: Dimensions = { - width: grid.width, - height: xAxisSize.height, - top: grid.top + grid.height, - left: grid.left, - }; - - return { - grid, - yAxis, - xAxis, - visibleNumberOfRows, - fullHeatmapHeight, - rowHeight, - xAxisTickCadence: xAxisSize.tickCadence, - xLabelRotation: xAxisSize.minRotation, - }; - }); - }, -); - -function getYAxisHorizontalUsedSpace( - yValues: HeatmapTable['yValues'], - style: HeatmapStyle['yAxisLabel'], - formatter: HeatmapSpec['yAxisLabelFormatter'], - textMeasure: TextMeasure, -): number { - if (!style.visible) { - return 0; - } - if (typeof style.width === 'number' && isFiniteNumber(style.width)) { - return style.width; - } - // account for the space required to show the longest Y axis label - const longestLabelWidth = yValues.reduce((acc, value) => { - const { width } = textMeasure(formatter(value), style, style.fontSize); - return Math.max(width + horizontalPad(style.padding), acc); - }, 0); - - return style.width === 'auto' ? longestLabelWidth : Math.min(longestLabelWidth, style.width.max); -} - -function getTextSizeDimension( - text: string, - style: AxisStyle['axisTitle'], - textMeasure: TextMeasure, - param: 'height' | 'width', -): number { - if (!style.visible || text === '') { - return 0; - } - const textPadding = innerPad(style.padding) + outerPad(style.padding); - if (param === 'height') { - return style.fontSize + textPadding; - } - - const textBox = textMeasure( - text, - { - fontFamily: style.fontFamily, - fontVariant: 'normal', - fontWeight: 'bold', - fontStyle: style.fontStyle ?? 'normal', - }, - style.fontSize, - ); - return textBox.width + textPadding; -} +export const computeChartDimensionsSelector = createCustomCachedSelector( + [getParentDimension, computeAxesSizesSelector, getChartThemeSelector], + (parentDimensions, axesSizes, { heatmap, chartPaddings }): ChartDimensions => { + const chartHeight = + parentDimensions.height - + axesSizes.xAxisTitleVerticalSize - + axesSizes.xAxisPanelTitleVerticalSize - + axesSizes.xAxis.height - + axesSizes.legendHeight - + heatmap.grid.stroke.width / 2; + + const { chartWidth } = axesSizes; -function getGridCellHeight(rows: number, grid: HeatmapStyle['grid'], height: number): number { - if (rows === 0) { - return height; // TODO check if this can be just 0 - } - const stretchedHeight = height / rows; - - if (stretchedHeight < grid.cellHeight.min) { - return grid.cellHeight.min; - } - if (grid.cellHeight.max !== 'fill' && stretchedHeight > grid.cellHeight.max) { - return grid.cellHeight.max; - } - - return stretchedHeight; -} - -function getXAxisSize( - isCategoricalScale: boolean, - style: HeatmapStyle['xAxisLabel'], - formatter: HeatmapSpec['xAxisLabelFormatter'], - labels: (string | number)[], - textMeasure: TextMeasure, - containerWidth: number, - surroundingSpace: [number, number], -): Size & { right: number; left: number; tickCadence: number; minRotation: Radian } { - if (!style.visible) { return { - height: 0, - width: Math.max(containerWidth - surroundingSpace[0] - surroundingSpace[1], 0), - left: surroundingSpace[0], - right: surroundingSpace[1], - tickCadence: NaN, - minRotation: 0, - }; - } - const isRotated = style.rotation !== 0; - const normalizedScale = scaleBand>().domain(labels).range([0, 1]); - - const alignment = isRotated ? 'right' : isCategoricalScale ? 'center' : 'left'; - const alignmentOffset = isCategoricalScale ? normalizedScale.bandwidth() / 2 : 0; - const scale = (d: NonNullable) => (normalizedScale(d) ?? 0) + alignmentOffset; - - // use positive angle from 0 to 90 only - const rotationRad = degToRad(style.rotation); - - const measuredLabels = labels.map((label) => ({ - ...textMeasure(formatter(label), style, style.fontSize), - label, - })); - - // don't filter ticks if categorical scale or with rotated labels - if (isCategoricalScale || isRotated) { - const maxLabelBBox = measuredLabels.reduce( - (acc, curr) => { - return { - height: Math.max(acc.height, curr.height), - width: Math.max(acc.width, curr.width), - }; + leftMargin: NaN, // not used + chartDimensions: { + top: parentDimensions.top + heatmap.grid.stroke.width / 2 + chartPaddings.top, + left: parentDimensions.left + axesSizes.xAxis.left + chartPaddings.left, + width: Math.max(0, chartWidth - chartPaddings.left - chartPaddings.right), + height: Math.max(0, chartHeight - chartPaddings.top - chartPaddings.bottom), }, - { height: 0, width: 0 }, - ); - const compressedScale = computeCompressedScale( - style, - scale, - measuredLabels, - containerWidth, - surroundingSpace, - alignment, - rotationRad, - ); - const scaleStep = compressedScale.width / labels.length; - // this optimal rotation is computed on a suboptimal compressed scale, it can be further enhanced with a monotonic hill climber - const optimalRotation = - scaleStep > maxLabelBBox.width ? 0 : Math.asin(Math.min(maxLabelBBox.height / scaleStep, 1)); - // if the current requested rotation is not at least bigger then the optimal one, recalculate the compression - // using the optimal one forcing the rotation to be without overlaps - const { width, height, left, right, minRotation } = { - ...(rotationRad !== 0 && optimalRotation > rotationRad - ? computeCompressedScale( - style, - scale, - measuredLabels, - containerWidth, - surroundingSpace, - alignment, - optimalRotation, - ) - : compressedScale), - minRotation: isRotated ? Math.max(optimalRotation, rotationRad) : 0, - }; - - const validCompression = isFiniteNumber(width); - return { - height: validCompression ? height : 0, - width: validCompression ? width : Math.max(containerWidth - surroundingSpace[0] - surroundingSpace[1], 0), - left: validCompression ? left : surroundingSpace[0], - right: validCompression ? right : surroundingSpace[1], - tickCadence: validCompression ? 1 : NaN, - minRotation, - }; - } - - // TODO refactor and move to monotonic hill climber and no mutations - // reduce the tick cadence on time scale to avoid overlaps and overflows - let tickCadence = 1; - let dimension = computeCompressedScale( - style, - scale, - measuredLabels, - containerWidth, - surroundingSpace, - alignment, - rotationRad, - ); - - for (let i = 1; i < measuredLabels.length; i++) { - if ((!dimension.overlaps && !dimension.overflow.right) || !isFiniteNumber(dimension.width)) { - break; - } - dimension = computeCompressedScale( - style, - scale, - measuredLabels.filter((_, index) => index % (i + 1) === 0), - containerWidth, - surroundingSpace, - alignment, - rotationRad, - ); - tickCadence++; - } - - // hide the axis because there is no space for labels - if (!isFiniteNumber(dimension.width)) { - return { - // hide the whole axis - height: 0, - width: Math.max(containerWidth - surroundingSpace[0] - surroundingSpace[1], 0), - left: surroundingSpace[0], - right: surroundingSpace[1], - // hide all ticks - tickCadence: NaN, - minRotation: rotationRad, }; - } - - return { - ...dimension, - tickCadence, - minRotation: rotationRad, - }; -} - -function computeCompressedScale( - style: HeatmapStyle['xAxisLabel'], - scale: (d: NonNullable) => number, - labels: Array }>, - containerWidth: number, - surroundingSpace: [number, number], - alignment: 'left' | 'right' | 'center', - rotation: Radian, -): Size & { left: number; right: number; overlaps: boolean; overflow: { left: boolean; right: boolean } } { - const { itemsPerSideSize, domainPositions, hMax } = labels.reduce<{ - wMax: number; - hMax: number; - itemsPerSideSize: [number, number][]; - domainPositions: number[]; - }>( - (acc, { width, height, label }) => { - // rotate the label box coordinates - const labelRect: Vec2[] = [ - [0, 0], - [width, 0], - [width, height], - [0, height], - ]; - - const rotationOrigin: Vec2 = - alignment === 'right' ? [width, height / 2] : alignment === 'left' ? [0, height / 2] : [width / 2, height / 2]; - - const rotatedVectors = labelRect.map((vector) => rotate2(rotation, sub2(vector, rotationOrigin))); - - // find the rotated bounding box - const x = extent(rotatedVectors.map((v) => v[0])); - const y = extent(rotatedVectors.map((v) => v[1])); - acc.wMax = Math.max(acc.wMax, Math.abs(x[1] - x[0])); - acc.hMax = Math.max(acc.hMax, Math.abs(y[1] - y[0])); - - // describe the item width as the left and right vector size from the rotation origin - acc.itemsPerSideSize.push([Math.abs(x[0]), Math.abs(x[1])]); - - // use a categorical scale with labels aligned to the center to compute the domain position - const domainPosition = scale(label); - acc.domainPositions.push(domainPosition); - return acc; - }, - { wMax: -Infinity, hMax: -Infinity, itemsPerSideSize: [], domainPositions: [] }, - ); - - // account for the left and right space (Y axes, overflows etc) - const globalDomainPositions = [0, ...domainPositions, 1]; - const globalItemWidth: [number, number][] = [[surroundingSpace[0], 0], ...itemsPerSideSize, [0, surroundingSpace[1]]]; - - const { scaleMultiplier, bounds } = screenspaceMarkerScaleCompressor( - globalDomainPositions, - globalItemWidth, - containerWidth, - ); - - // check label overlaps using the computed compressed scale - const overlaps = itemsPerSideSize.some(([, rightSide], i) => { - if (i >= itemsPerSideSize.length - 2) { - return false; - } - const currentItemRightSide = domainPositions[i] * scaleMultiplier + rightSide + pad(style.padding, 'right'); - const nextItemLeftSize = - domainPositions[i + 1] * scaleMultiplier - itemsPerSideSize[i + 1][0] - pad(style.padding, 'left'); - return currentItemRightSide > nextItemLeftSize; - }); - - const leftMargin = isFiniteNumber(bounds[0]) - ? globalItemWidth[bounds[0]][0] - scaleMultiplier * globalDomainPositions[bounds[0]] - : 0; - const rightMargin = isFiniteNumber(bounds[1]) ? globalItemWidth[bounds[1]][1] : 0; - - return { - // the horizontal space - width: scaleMultiplier, - right: rightMargin, - left: leftMargin, - // the height represent the height of the max rotated bbox plus the padding and the vertical position of the rotation origin - height: hMax + pad(style.padding, 'top') + style.fontSize / 2, - overlaps, - overflow: { - // true if a label exist protrude to the left making the scale shrink from the left - // the current check is based on the way we construct globalItemWidth and globalDomainPositions - left: bounds[0] !== 0, - // true if a label exist protrude to the right making the scale shrink from the right - // the current check is based on the way we construct globalItemWidth and globalDomainPositions - right: bounds[1] !== globalDomainPositions.length - 1, - }, - }; -} + }, +); diff --git a/packages/charts/src/chart_types/heatmap/state/selectors/compute_chart_element_sizes.ts b/packages/charts/src/chart_types/heatmap/state/selectors/compute_chart_element_sizes.ts new file mode 100644 index 0000000000..91632648de --- /dev/null +++ b/packages/charts/src/chart_types/heatmap/state/selectors/compute_chart_element_sizes.ts @@ -0,0 +1,61 @@ +/* + * 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 { computeAxesSizesSelector } from './compute_axes_sizes'; +import { computeChartDimensionsSelector } from './compute_chart_dimensions'; +import { getPanelSize } from '../../../../common/panel_utils'; +import { createCustomCachedSelector } from '../../../../state/create_selector'; +import { computeSmallMultipleScalesSelector } from '../../../../state/selectors/compute_small_multiple_scales'; +import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; +import { Dimensions } from '../../../../utils/dimensions'; + +/** @internal */ +export type ChartElementSizes = { + yAxis: Dimensions; + xAxis: Dimensions; + xAxisTickCadence: number; + xLabelRotation: number; +}; + +/** + * Returns grid and axes sizes and positions. + * @internal + */ +export const computeChartElementSizesSelector = createCustomCachedSelector( + [computeChartDimensionsSelector, computeAxesSizesSelector, getChartThemeSelector, computeSmallMultipleScalesSelector], + ({ chartDimensions }, axesSizes, { heatmap }, smScales): ChartElementSizes => { + const panelHeight = getPanelSize(smScales).height; + const grid: Dimensions = { + width: axesSizes.xAxis.width, + height: panelHeight, + left: chartDimensions.left + axesSizes.xAxis.left, + top: chartDimensions.top + heatmap.grid.stroke.width / 2, + }; + + const yAxis: Dimensions = { + width: axesSizes.yAxis.width, + height: grid.height, + top: grid.top, + left: grid.left - axesSizes.yAxis.width, + }; + + const xAxis: Dimensions = { + width: grid.width, + height: axesSizes.xAxis.height, + top: grid.top + grid.height, + left: grid.left, + }; + + return { + yAxis, + xAxis, + xAxisTickCadence: axesSizes.xAxis.tickCadence, + xLabelRotation: axesSizes.xAxis.minRotation, + }; + }, +); diff --git a/packages/charts/src/chart_types/heatmap/state/selectors/get_brush_area.ts b/packages/charts/src/chart_types/heatmap/state/selectors/get_brush_area.ts index 7737f1cfad..20a204f869 100644 --- a/packages/charts/src/chart_types/heatmap/state/selectors/get_brush_area.ts +++ b/packages/charts/src/chart_types/heatmap/state/selectors/get_brush_area.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { computeChartElementSizesSelector } from './compute_chart_dimensions'; +import { computeChartDimensionsSelector } from './compute_chart_dimensions'; import { getBrushedHighlightedShapesSelector } from './get_brushed_highlighted_shapes'; import { BrushAxis } from '../../../../specs'; import { GlobalChartState } from '../../../../state/chart_state'; @@ -26,33 +26,33 @@ export const getBrushAreaSelector = createCustomCachedSelector( getMouseDownPosition, getActivePointerPosition, getSettingsSpecSelector, - computeChartElementSizesSelector, + computeChartDimensionsSelector, getBrushedHighlightedShapesSelector, ], - (isDragging, mouseDownPosition, end, { brushAxis }, dims, dragShape): Dimensions | null => { + (isDragging, mouseDownPosition, end, { brushAxis }, { chartDimensions }, dragShape): Dimensions | null => { if (!isDragging || !mouseDownPosition || !dragShape) { return null; } const start = { - x: mouseDownPosition.position.x - dims.grid.left, + x: mouseDownPosition.position.x - chartDimensions.left, y: mouseDownPosition.position.y, }; - const clampedEndY = clamp(end.y, 0, dims.grid.height); + const clampedEndY = clamp(end.y, 0, chartDimensions.height); switch (brushAxis) { case BrushAxis.Both: return { top: start.y, left: start.x, - width: end.x - start.x - dims.grid.left, + width: end.x - start.x - chartDimensions.left, height: clampedEndY - start.y, }; default: return { top: start.y, left: start.x, - width: end.x - start.x - dims.grid.left, + width: end.x - start.x - chartDimensions.left, height: clampedEndY - start.y, }; } diff --git a/packages/charts/src/chart_types/heatmap/state/selectors/get_brushed_highlighted_shapes.test.ts b/packages/charts/src/chart_types/heatmap/state/selectors/get_brushed_highlighted_shapes.test.ts index 146957f363..3c515fa501 100644 --- a/packages/charts/src/chart_types/heatmap/state/selectors/get_brushed_highlighted_shapes.test.ts +++ b/packages/charts/src/chart_types/heatmap/state/selectors/get_brushed_highlighted_shapes.test.ts @@ -27,14 +27,6 @@ describe('Categorical heatmap brush', () => { MockGlobalSpec.settingsNoMargins({ theme: { heatmap: { - grid: { - cellHeight: { - max: 'fill', - }, - cellWidth: { - max: 'fill', - }, - }, xAxisLabel: { visible: false, }, @@ -90,14 +82,6 @@ describe('Temporal heatmap brush', () => { MockGlobalSpec.settingsNoMargins({ theme: { heatmap: { - grid: { - cellHeight: { - max: 'fill', - }, - cellWidth: { - max: 'fill', - }, - }, xAxisLabel: { visible: false, }, diff --git a/packages/charts/src/chart_types/heatmap/state/selectors/get_brushed_highlighted_shapes.ts b/packages/charts/src/chart_types/heatmap/state/selectors/get_brushed_highlighted_shapes.ts index fdc649ffb8..d9b4207bfd 100644 --- a/packages/charts/src/chart_types/heatmap/state/selectors/get_brushed_highlighted_shapes.ts +++ b/packages/charts/src/chart_types/heatmap/state/selectors/get_brushed_highlighted_shapes.ts @@ -7,7 +7,7 @@ */ import { getActivePointerPosition } from './../../../../state/selectors/get_active_pointer_position'; -import { getHeatmapGeometries } from './geometries'; +import { getPerPanelHeatmapGeometries } from './get_per_panel_heatmap_geometries'; import { GlobalChartState } from '../../../../state/chart_state'; import { createCustomCachedSelector } from '../../../../state/create_selector'; import { DragShape } from '../../layout/types/viewmodel_types'; @@ -18,7 +18,7 @@ function getCurrentPointerStates(state: GlobalChartState) { /** @internal */ export const getBrushedHighlightedShapesSelector = createCustomCachedSelector( - [getHeatmapGeometries, getCurrentPointerStates, getActivePointerPosition], + [getPerPanelHeatmapGeometries, getCurrentPointerStates, getActivePointerPosition], (geoms, pointerStates, activePosition): DragShape => { if (!pointerStates.dragging || !pointerStates.down) { return null; diff --git a/packages/charts/src/chart_types/heatmap/state/selectors/get_cursor_band.ts b/packages/charts/src/chart_types/heatmap/state/selectors/get_cursor_band.ts index 5632c81fe7..f4a621a6eb 100644 --- a/packages/charts/src/chart_types/heatmap/state/selectors/get_cursor_band.ts +++ b/packages/charts/src/chart_types/heatmap/state/selectors/get_cursor_band.ts @@ -6,54 +6,56 @@ * Side Public License, v 1. */ -import { getHeatmapGeometries } from './geometries'; +import { getPerPanelHeatmapGeometries } from './get_per_panel_heatmap_geometries'; +import { getTooltipAnchorSelector } from './get_tooltip_anchor'; import { getPickedShapes, hasPicketVisibleCells } from './picked_shapes'; import { Rect } from '../../../../geoms/types'; -import { isPointerOverEvent, PointerEvent } from '../../../../specs'; +import { isPointerOverEvent } from '../../../../specs'; import { GlobalChartState } from '../../../../state/chart_state'; import { createCustomCachedSelector } from '../../../../state/create_selector'; -import { getActivePointerPosition } from '../../../../state/selectors/get_active_pointer_position'; +import { getInternalIsBrushingSelector } from '../../../../state/selectors/get_internal_is_brushing'; import { isNil } from '../../../../utils/common'; -import { Point } from '../../../../utils/point'; -import { Cell, ShapeViewModel, TextBox } from '../../layout/types/viewmodel_types'; const getExternalPointerEventStateSelector = (state: GlobalChartState) => state.externalEvents.pointer; /** @internal */ export const getCursorBandPositionSelector = createCustomCachedSelector( - [getHeatmapGeometries, getExternalPointerEventStateSelector, getActivePointerPosition, getPickedShapes], - getCursorBand, -); + [ + getPerPanelHeatmapGeometries, + getExternalPointerEventStateSelector, + getPickedShapes, + getTooltipAnchorSelector, + getInternalIsBrushingSelector, + ], + ( + geoms, + externalPointerEvent, + pickedShapes, + tooltipShape, + isBrushing, + ): (Rect & { fromExternalEvent: boolean }) | undefined => { + // block cursor when brusing + if (isBrushing) return; -function getCursorBand( - geoms: ShapeViewModel, - externalPointerEvent: PointerEvent | null, - currentPointerPosition: Point, - pickedShapes: Cell[] | TextBox, -): (Rect & { fromExternalEvent: boolean }) | undefined { - // external pointer events takes precedence over the current mouse pointer - if (isPointerOverEvent(externalPointerEvent)) { - const { x } = externalPointerEvent; - if (!isNil(x)) { - const band = geoms.pickCursorBand(x); - if (band) { - return { - ...band, - fromExternalEvent: true, - }; + // external pointer events takes precedence over the current mouse pointer + if (isPointerOverEvent(externalPointerEvent)) { + const { x } = externalPointerEvent; + if (!isNil(x)) { + const band = geoms.pickCursorBand(x); + if (band) { + return { + ...band, + fromExternalEvent: true, + }; + } } } - } - if (hasPicketVisibleCells(pickedShapes)) { - const point = currentPointerPosition; - const end = { x: point.x + Number.EPSILON, y: point.y + Number.EPSILON }; - const band = geoms.pickDragShape([point, end]); - if (band) { + if (hasPicketVisibleCells(pickedShapes)) { return { - ...band, + ...tooltipShape, fromExternalEvent: false, }; } - } -} + }, +); diff --git a/packages/charts/src/chart_types/heatmap/state/selectors/get_debug_state.ts b/packages/charts/src/chart_types/heatmap/state/selectors/get_debug_state.ts index b3cc3fca40..668b65456b 100644 --- a/packages/charts/src/chart_types/heatmap/state/selectors/get_debug_state.ts +++ b/packages/charts/src/chart_types/heatmap/state/selectors/get_debug_state.ts @@ -6,15 +6,15 @@ * Side Public License, v 1. */ -import { getChartThemeSelector } from './../../../../state/selectors/get_chart_theme'; -import { computeChartElementSizesSelector } from './compute_chart_dimensions'; +import { computeChartElementSizesSelector } from './compute_chart_element_sizes'; import { computeLegendSelector } from './compute_legend'; -import { getHeatmapGeometries } from './geometries'; import { getHeatmapSpecSelector } from './get_heatmap_spec'; import { getHighlightedAreaSelector, getHighlightedDataSelector } from './get_highlighted_area'; +import { getPerPanelHeatmapGeometries } from './get_per_panel_heatmap_geometries'; import { RGBATupleToString } from '../../../../common/color_library_wrappers'; import { LegendItem } from '../../../../common/legend'; import { createCustomCachedSelector } from '../../../../state/create_selector'; +import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; import { DebugState, DebugStateLegend } from '../../../../state/types'; import { Position } from '../../../../utils/common'; @@ -24,7 +24,7 @@ import { Position } from '../../../../utils/common'; */ export const getDebugStateSelector = createCustomCachedSelector( [ - getHeatmapGeometries, + getPerPanelHeatmapGeometries, computeLegendSelector, getHighlightedAreaSelector, getHighlightedDataSelector, @@ -41,7 +41,9 @@ export const getDebugStateSelector = createCustomCachedSelector( { xAxisTickCadence }, { xAxisTitle, yAxisTitle }, ): DebugState => { - const xAxisValues = geoms.heatmapViewModel.xValues.filter((_, i) => i % xAxisTickCadence === 0); + const heatmapViewModel = geoms.heatmapViewModels[0]; + const xAxisValues = heatmapViewModel?.xValues.filter((_, i) => i % xAxisTickCadence === 0) ?? []; + return { // Common debug state legend: getLegendState(legend), @@ -53,7 +55,7 @@ export const getDebugStateSelector = createCustomCachedSelector( labels: xAxisValues.map(({ text }) => text), values: xAxisValues.map(({ value }) => value), // vertical lines - gridlines: geoms.heatmapViewModel.gridLines.x.map((line) => ({ x: line.x1, y: line.y2 })), + gridlines: (heatmapViewModel?.gridLines?.x ?? []).map((line) => ({ x: line.x1, y: line.y2 })), ...(xAxisTitle ? { title: xAxisTitle } : {}), }, ], @@ -61,24 +63,27 @@ export const getDebugStateSelector = createCustomCachedSelector( { id: 'y', position: Position.Bottom, - labels: geoms.heatmapViewModel.yValues.map(({ text }) => text), - values: geoms.heatmapViewModel.yValues.map(({ value }) => value), + labels: (heatmapViewModel?.yValues ?? []).map(({ text }) => text), + values: (heatmapViewModel?.yValues ?? []).map(({ value }) => value), // horizontal lines - gridlines: geoms.heatmapViewModel.gridLines.y.map((line) => ({ x: line.x2, y: line.y1 })), + gridlines: (heatmapViewModel?.gridLines?.y ?? []).map((line) => ({ x: line.x2, y: line.y1 })), ...(yAxisTitle ? { title: yAxisTitle } : {}), }, ], }, // Heatmap debug state heatmap: { - cells: geoms.heatmapViewModel.cells.map((cell) => ({ - x: cell.x, - y: cell.y, - fill: RGBATupleToString(cell.fill.color), - formatted: cell.formatted, - value: cell.value, - valueShown: heatmap.cell.label.visible && Number.isFinite(geoms.heatmapViewModel.cellFontSize(cell)), - })), + cells: geoms.heatmapViewModels.flatMap((vm) => + vm.cells.map((cell) => ({ + x: cell.x, + y: cell.y, + datum: cell.datum, + fill: RGBATupleToString(cell.fill.color), + formatted: cell.formatted, + value: cell.value, + valueShown: heatmap.cell.label.visible && Number.isFinite(vm.cellFontSize(cell)), + })), + ), selection: { area: pickedArea, data: highlightedData, diff --git a/packages/charts/src/chart_types/heatmap/state/selectors/get_heatmap_table.ts b/packages/charts/src/chart_types/heatmap/state/selectors/get_heatmap_table.ts index e0b3a440bc..c886271531 100644 --- a/packages/charts/src/chart_types/heatmap/state/selectors/get_heatmap_table.ts +++ b/packages/charts/src/chart_types/heatmap/state/selectors/get_heatmap_table.ts @@ -8,26 +8,39 @@ import { extent } from 'd3-array'; -import { HeatmapTable } from './compute_chart_dimensions'; import { getHeatmapSpecSelector } from './get_heatmap_spec'; -import { getPredicateFn } from '../../../../common/predicate'; +import { SmallMultiplesSeriesDomains } from '../../../../common/panel_utils'; +import { getPredicateFn, Predicate } from '../../../../common/predicate'; import { ScaleType } from '../../../../scales/constants'; import { createCustomCachedSelector } from '../../../../state/create_selector'; import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_spec'; +import { getSmallMultiplesIndexOrderSelector } from '../../../../state/selectors/get_small_multiples_index_order'; import { getAccessorValue } from '../../../../utils/accessor'; import { addIntervalToTime, timeRange } from '../../../../utils/chrono/elasticsearch'; -import { isFiniteNumber, isNonNullablePrimitiveValue } from '../../../../utils/common'; +import { isFiniteNumber, isNil, isNonNullablePrimitiveValue } from '../../../../utils/common'; +import { HeatmapCellDatum } from '../../layout/viewmodel/viewmodel'; + +/** + * @internal + */ +export interface HeatmapTable extends SmallMultiplesSeriesDomains { + table: Array; + yValues: Array; + xValues: Array; + xNumericExtent: [number, number]; + extent: [number, number]; +} /** * Extracts axis and cell values from the input data. * @internal */ export const getHeatmapTableSelector = createCustomCachedSelector( - [getHeatmapSpecSelector, getSettingsSpecSelector], - ( - { data, valueAccessor, xAccessor, yAccessor, xSortPredicate, ySortPredicate, xScale, timeZone }, - { xDomain }, - ): HeatmapTable => { + [getHeatmapSpecSelector, getSettingsSpecSelector, getSmallMultiplesIndexOrderSelector], + (spec, { xDomain }, smallMultiples): HeatmapTable => { + const { data, valueAccessor, xAccessor, yAccessor, xSortPredicate, ySortPredicate, xScale, timeZone } = spec; + const smVValues = new Set(); + const smHValues = new Set(); const resultData = data.reduce( (acc, curr, index) => { const x = getAccessorValue(curr, xAccessor); @@ -40,11 +53,21 @@ export const getHeatmapTableSelector = createCustomCachedSelector( // add a cell and update extent only if the value is finite if (isFiniteNumber(value)) { acc.extent = [Math.min(acc.extent[0], value), Math.max(acc.extent[1], value)]; + + // extract small multiples aggregation values + const smH = smallMultiples?.horizontal?.by?.(spec, curr); + const smV = smallMultiples?.vertical?.by?.(spec, curr); + + if (!isNil(smH)) smHValues.add(smH); + if (!isNil(smV)) smVValues.add(smV); + acc.table.push({ x, y, value, originalIndex: index, + smVerticalAccessorValue: smV, + smHorizontalAccessorValue: smH, }); } // the x and y values are used for the scale domain, and we want to keep track of every element, even non-finite values @@ -61,6 +84,8 @@ export const getHeatmapTableSelector = createCustomCachedSelector( table: [], xValues: [], yValues: [], + smHDomain: [], + smVDomain: [], extent: [+Infinity, -Infinity], xNumericExtent: [+Infinity, -Infinity], }, @@ -82,6 +107,17 @@ export const getHeatmapTableSelector = createCustomCachedSelector( // sort Y values by their predicates resultData.yValues.sort(getPredicateFn(ySortPredicate)); - return resultData; + // sort small multiples values + const horizontalPredicate = smallMultiples?.horizontal?.sort ?? Predicate.DataIndex; + const smHDomain = [...smHValues].sort(getPredicateFn(horizontalPredicate)); + + const verticalPredicate = smallMultiples?.vertical?.sort ?? Predicate.DataIndex; + const smVDomain = [...smVValues].sort(getPredicateFn(verticalPredicate)); + + return { + ...resultData, + smHDomain, + smVDomain, + }; }, ); diff --git a/packages/charts/src/chart_types/heatmap/state/selectors/get_highlighted_area.ts b/packages/charts/src/chart_types/heatmap/state/selectors/get_highlighted_area.ts index 32e383dddb..d75501ad18 100644 --- a/packages/charts/src/chart_types/heatmap/state/selectors/get_highlighted_area.ts +++ b/packages/charts/src/chart_types/heatmap/state/selectors/get_highlighted_area.ts @@ -6,8 +6,8 @@ * Side Public License, v 1. */ -import { getHeatmapGeometries } from './geometries'; import { getHeatmapSpecSelector } from './get_heatmap_spec'; +import { getPerPanelHeatmapGeometries } from './get_per_panel_heatmap_geometries'; import { isBrushingSelector } from './is_brushing'; import { createCustomCachedSelector } from '../../../../state/create_selector'; @@ -29,11 +29,12 @@ export const getHighlightedDataSelector = createCustomCachedSelector( * @internal */ export const getHighlightedAreaSelector = createCustomCachedSelector( - [getHeatmapGeometries, getHeatmapSpecSelector, isBrushingSelector], + [getPerPanelHeatmapGeometries, getHeatmapSpecSelector, isBrushingSelector], (geoms, spec, isBrushing) => { if (!spec.highlightedData || isBrushing) { return null; } - return geoms.pickHighlightedArea(spec.highlightedData.x, spec.highlightedData.y); + const { x, y, smHorizontalAccessorValue, smVerticalAccessorValue } = spec.highlightedData; + return geoms.pickHighlightedArea(x, y, smHorizontalAccessorValue, smVerticalAccessorValue); }, ); diff --git a/packages/charts/src/chart_types/heatmap/state/selectors/geometries.ts b/packages/charts/src/chart_types/heatmap/state/selectors/get_per_panel_heatmap_geometries.ts similarity index 62% rename from packages/charts/src/chart_types/heatmap/state/selectors/geometries.ts rename to packages/charts/src/chart_types/heatmap/state/selectors/get_per_panel_heatmap_geometries.ts index aff024f32e..5b5dcb08ff 100644 --- a/packages/charts/src/chart_types/heatmap/state/selectors/geometries.ts +++ b/packages/charts/src/chart_types/heatmap/state/selectors/get_per_panel_heatmap_geometries.ts @@ -6,31 +6,48 @@ * Side Public License, v 1. */ -import { computeChartElementSizesSelector } from './compute_chart_dimensions'; +import { computeChartDimensionsSelector } from './compute_chart_dimensions'; +import { computeChartElementSizesSelector } from './compute_chart_element_sizes'; import { getColorScale } from './get_color_scale'; import { getHeatmapSpecSelector } from './get_heatmap_spec'; import { getHeatmapTableSelector } from './get_heatmap_table'; import { isEmptySelector } from './is_empty'; import { GlobalChartState } from '../../../../state/chart_state'; import { createCustomCachedSelector } from '../../../../state/create_selector'; +import { computeSmallMultipleScalesSelector } from '../../../../state/selectors/compute_small_multiple_scales'; import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; +import { getSmallMultiplesIndexOrderSelector } from '../../../../state/selectors/get_small_multiples_index_order'; import { nullShapeViewModel, ShapeViewModel } from '../../layout/types/viewmodel_types'; -import { render } from '../../layout/viewmodel/scenegraph'; +import { computeScenegraph } from '../../layout/viewmodel/scenegraph'; const getDeselectedSeriesSelector = (state: GlobalChartState) => state.interactions.deselectedDataSeries; /** @internal */ -export const getHeatmapGeometries = createCustomCachedSelector( +export const getPerPanelHeatmapGeometries = createCustomCachedSelector( [ getHeatmapSpecSelector, + computeChartDimensionsSelector, computeChartElementSizesSelector, getHeatmapTableSelector, getColorScale, getDeselectedSeriesSelector, getChartThemeSelector, isEmptySelector, + computeSmallMultipleScalesSelector, + getSmallMultiplesIndexOrderSelector, ], - (heatmapSpec, dims, heatmapTable, { bands, scale: colorScale }, deselectedSeries, theme, empty): ShapeViewModel => { + ( + heatmapSpec, + chartDimensions, + elementSizes, + heatmapTable, + { bands, scale: colorScale }, + deselectedSeries, + theme, + empty, + smScales, + groupBySpec, + ): ShapeViewModel => { // instead of using the specId, each legend item is associated with an unique band label const disabledBandLabels = new Set(deselectedSeries.map(({ specId }) => specId)); const bandsToHide: Array<[number, number]> = bands @@ -38,7 +55,17 @@ export const getHeatmapGeometries = createCustomCachedSelector( .map(({ start, end }) => [start, end]); return heatmapSpec && !empty - ? render(heatmapSpec, dims, heatmapTable, colorScale, bandsToHide, theme) + ? computeScenegraph( + heatmapSpec, + chartDimensions, + elementSizes, + smScales, + groupBySpec, + heatmapTable, + colorScale, + bandsToHide, + theme, + ) : nullShapeViewModel(); }, ); diff --git a/packages/charts/src/chart_types/heatmap/state/selectors/get_picked_cells.test.ts b/packages/charts/src/chart_types/heatmap/state/selectors/get_picked_cells.test.ts index a165d79405..8a86bdb85d 100644 --- a/packages/charts/src/chart_types/heatmap/state/selectors/get_picked_cells.test.ts +++ b/packages/charts/src/chart_types/heatmap/state/selectors/get_picked_cells.test.ts @@ -29,14 +29,6 @@ describe('Heatmap picked cells', () => { theme: { heatmap: { brushTool: { visible: true }, - grid: { - cellHeight: { - max: 'fill', - }, - cellWidth: { - max: 'fill', - }, - }, xAxisLabel: { visible: true, }, diff --git a/packages/charts/src/chart_types/heatmap/state/selectors/get_picked_cells.ts b/packages/charts/src/chart_types/heatmap/state/selectors/get_picked_cells.ts index e161909294..735a982189 100644 --- a/packages/charts/src/chart_types/heatmap/state/selectors/get_picked_cells.ts +++ b/packages/charts/src/chart_types/heatmap/state/selectors/get_picked_cells.ts @@ -6,31 +6,33 @@ * Side Public License, v 1. */ -import { computeChartElementSizesSelector } from './compute_chart_dimensions'; -import { getHeatmapGeometries } from './geometries'; +import { computeChartDimensionsSelector } from './compute_chart_dimensions'; +import { getPerPanelHeatmapGeometries } from './get_per_panel_heatmap_geometries'; import { createCustomCachedSelector } from '../../../../state/create_selector'; import { getLastDragSelector } from '../../../../state/selectors/get_last_drag'; import { PickDragFunction } from '../../layout/types/viewmodel_types'; /** @internal */ export const getPickedCells = createCustomCachedSelector( - [getHeatmapGeometries, getLastDragSelector, computeChartElementSizesSelector], - (geoms, dragState, dims): ReturnType | null => { + [getPerPanelHeatmapGeometries, getLastDragSelector, computeChartDimensionsSelector], + (geoms, dragState, { chartDimensions }): ReturnType | null => { if (!dragState) { return null; } // the pointer is not on the cells but over the y- axis and does not cross the y-axis - if (dragState.start.position.x < dims.grid.left && dragState.end.position.x < dims.grid.left) { - const fittedDragStateStart = { x: dims.grid.left, y: dragState.start.position.y }; + if (dragState.start.position.x < chartDimensions.left && dragState.end.position.x < chartDimensions.left) { + const fittedDragStateStart = { x: chartDimensions.left, y: dragState.start.position.y }; const { y, cells } = geoms.pickDragArea([fittedDragStateStart, dragState.end.position]); + // TODO: fix with small multiples return { x: [], y, cells }; } // the pointer is not on the cells by over the x-axis and does not cross the x-axis - if (dragState.start.position.y > dims.grid.height && dragState.end.position.y > dims.grid.height) { - const fittedDragStateStart = { x: dragState.start.position.x, y: dims.grid.height }; + if (dragState.start.position.y > chartDimensions.height && dragState.end.position.y > chartDimensions.height) { + const fittedDragStateStart = { x: dragState.start.position.x, y: chartDimensions.height }; const { x, cells } = geoms.pickDragArea([fittedDragStateStart, dragState.end.position]); + // TODO: fix with small multiples return { x, y: [], cells }; } return geoms.pickDragArea([dragState.start.position, dragState.end.position]); diff --git a/packages/charts/src/chart_types/heatmap/state/selectors/get_tooltip_anchor.ts b/packages/charts/src/chart_types/heatmap/state/selectors/get_tooltip_anchor.ts index 8566f9e7ce..cb1dfa4116 100644 --- a/packages/charts/src/chart_types/heatmap/state/selectors/get_tooltip_anchor.ts +++ b/packages/charts/src/chart_types/heatmap/state/selectors/get_tooltip_anchor.ts @@ -6,25 +6,49 @@ * Side Public License, v 1. */ -import { computeChartElementSizesSelector } from './compute_chart_dimensions'; +import { computeChartDimensionsSelector } from './compute_chart_dimensions'; import { getPickedShapes } from './picked_shapes'; import { AnchorPosition } from '../../../../components/portal/types'; import { createCustomCachedSelector } from '../../../../state/create_selector'; +import { computeSmallMultipleScalesSelector } from '../../../../state/selectors/compute_small_multiple_scales'; import { getActivePointerPosition } from '../../../../state/selectors/get_active_pointer_position'; +import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; /** @internal */ export const getTooltipAnchorSelector = createCustomCachedSelector( - [getPickedShapes, computeChartElementSizesSelector, getActivePointerPosition], - (shapes, { grid }, position): AnchorPosition => { + [ + getPickedShapes, + computeChartDimensionsSelector, + getActivePointerPosition, + computeSmallMultipleScalesSelector, + getChartThemeSelector, + ], + (shapes, { chartDimensions }, position, smScales, { heatmap }): AnchorPosition => { if (Array.isArray(shapes) && shapes.length > 0) { - const firstShape = shapes[0]; + const [ + { + x, + y, + width, + height, + datum: { smHorizontalAccessorValue = '', smVerticalAccessorValue = '' }, + }, + ] = shapes; + + const scaledPanelXOffset = smScales.horizontal.scale(smHorizontalAccessorValue); + const scaledPanelYOffset = smScales.vertical.scale(smVerticalAccessorValue); + + const panelXOffset = isNaN(scaledPanelXOffset) ? 0 : scaledPanelXOffset; + const panelYOffset = isNaN(scaledPanelYOffset) ? 0 : scaledPanelYOffset; + return { - x: firstShape.x + grid.left, - width: firstShape.width, - y: firstShape.y - grid.top, - height: firstShape.height, + x: x + chartDimensions.left + panelXOffset, + width, + y: y - chartDimensions.top + panelYOffset + heatmap.grid.stroke.width, + height, }; } + return { x: position.x, width: 0, diff --git a/packages/charts/src/chart_types/heatmap/state/selectors/picked_shapes.ts b/packages/charts/src/chart_types/heatmap/state/selectors/picked_shapes.ts index 1705aa6611..e331907bdc 100644 --- a/packages/charts/src/chart_types/heatmap/state/selectors/picked_shapes.ts +++ b/packages/charts/src/chart_types/heatmap/state/selectors/picked_shapes.ts @@ -6,28 +6,25 @@ * Side Public License, v 1. */ -import { computeChartElementSizesSelector } from './compute_chart_dimensions'; -import { getHeatmapGeometries } from './geometries'; +import { computeChartElementSizesSelector } from './compute_chart_element_sizes'; +import { getPerPanelHeatmapGeometries } from './get_per_panel_heatmap_geometries'; import { createCustomCachedSelector } from '../../../../state/create_selector'; import { getActivePointerPosition } from '../../../../state/selectors/get_active_pointer_position'; import { Cell, GridCell, TextBox } from '../../layout/types/viewmodel_types'; /** @internal */ export const getPickedShapes = createCustomCachedSelector( - [getHeatmapGeometries, getActivePointerPosition, computeChartElementSizesSelector], - (geoms, pointerPosition, dims): Cell[] | TextBox => { + [getPerPanelHeatmapGeometries, getActivePointerPosition], + (geoms, pointerPosition): Cell[] | TextBox => { const picker = geoms.pickQuads; const { x, y } = pointerPosition; - const pickedData = picker(x, y); - return Array.isArray(pickedData) - ? pickedData.filter(({ y }) => y < dims.rowHeight * dims.visibleNumberOfRows) - : pickedData; + return picker(x, y); }, ); /** @internal */ export const getPickedGridCell = createCustomCachedSelector( - [getHeatmapGeometries, getActivePointerPosition, computeChartElementSizesSelector], + [getPerPanelHeatmapGeometries, getActivePointerPosition, computeChartElementSizesSelector], (geoms, pointerPosition): GridCell | undefined => { return geoms.pickGridCell(pointerPosition.x, pointerPosition.y); }, diff --git a/packages/charts/src/chart_types/heatmap/state/utils/axis.ts b/packages/charts/src/chart_types/heatmap/state/utils/axis.ts new file mode 100644 index 0000000000..6936bf5920 --- /dev/null +++ b/packages/charts/src/chart_types/heatmap/state/utils/axis.ts @@ -0,0 +1,300 @@ +/* + * 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 { scaleBand } from 'd3-scale'; + +import { Radian } from '../../../../common/geometry'; +import { extent } from '../../../../common/math'; +import { Vec2, rotate2, sub2 } from '../../../../common/vectors'; +import { screenspaceMarkerScaleCompressor } from '../../../../solvers/screenspace_marker_scale_compressor'; +import { TextMeasure } from '../../../../utils/bbox/canvas_text_bbox_calculator'; +import { isFiniteNumber, degToRad } from '../../../../utils/common'; +import { horizontalPad, innerPad, outerPad, pad, Size } from '../../../../utils/dimensions'; +import { HeatmapStyle, AxisStyle } from '../../../../utils/themes/theme'; +import { PrimitiveValue } from '../../../partition_chart/layout/utils/group_by_rollup'; +import { HeatmapSpec } from '../../specs'; +import { HeatmapTable } from '../selectors/get_heatmap_table'; + +/** @internal */ +export function getYAxisHorizontalUsedSpace( + yValues: HeatmapTable['yValues'], + style: HeatmapStyle['yAxisLabel'], + formatter: HeatmapSpec['yAxisLabelFormatter'], + textMeasure: TextMeasure, +): number { + if (!style.visible) { + return 0; + } + if (typeof style.width === 'number' && isFiniteNumber(style.width)) { + return style.width; + } + // account for the space required to show the longest Y axis label + const longestLabelWidth = yValues.reduce((acc, value) => { + const { width } = textMeasure(formatter(value), style, style.fontSize); + return Math.max(width + horizontalPad(style.padding), acc); + }, 0); + + return style.width === 'auto' ? longestLabelWidth : Math.min(longestLabelWidth, style.width.max); +} + +/** @internal */ +export function getTextSizeDimension( + text: string, + style: AxisStyle['axisTitle'], + textMeasure: TextMeasure, + param: 'height' | 'width', + hidden = false, +): number { + if (!style.visible || text === '' || hidden) { + return 0; + } + const textPadding = innerPad(style.padding) + outerPad(style.padding); + if (param === 'height') { + return style.fontSize + textPadding; + } + + const textBox = textMeasure( + text, + { + fontFamily: style.fontFamily, + fontVariant: 'normal', + fontWeight: 'bold', + fontStyle: style.fontStyle ?? 'normal', + }, + style.fontSize, + ); + return textBox.width + textPadding; +} + +/** @internal */ +export function getXAxisSize( + isCategoricalScale: boolean, + style: HeatmapStyle['xAxisLabel'], + formatter: HeatmapSpec['xAxisLabelFormatter'], + labels: (string | number)[], + textMeasure: TextMeasure, + containerWidth: number, + surroundingSpace: [left: number, right: number], +): Size & { right: number; left: number; tickCadence: number; minRotation: Radian } { + if (!style.visible) { + return { + height: 0, + width: Math.max(containerWidth - surroundingSpace[0] - surroundingSpace[1], 0), + left: surroundingSpace[0], + right: surroundingSpace[1], + tickCadence: NaN, + minRotation: 0, + }; + } + const isRotated = style.rotation !== 0; + const normalizedScale = scaleBand>().domain(labels).range([0, 1]); + + const alignment = isRotated ? 'right' : isCategoricalScale ? 'center' : 'left'; + const alignmentOffset = isCategoricalScale ? normalizedScale.bandwidth() / 2 : 0; + const scale = (d: NonNullable) => (normalizedScale(d) ?? 0) + alignmentOffset; + + // use positive angle from 0 to 90 only + const rotationRad = degToRad(style.rotation); + + const measuredLabels = labels.map((label) => ({ + ...textMeasure(formatter(label), style, style.fontSize), + label, + })); + + // don't filter ticks if categorical scale or with rotated labels + if (isCategoricalScale || isRotated) { + const maxLabelBBox = measuredLabels.reduce( + (acc, curr) => { + return { + height: Math.max(acc.height, curr.height), + width: Math.max(acc.width, curr.width), + }; + }, + { height: 0, width: 0 }, + ); + const compressedScale = computeCompressedScale( + style, + scale, + measuredLabels, + containerWidth, + surroundingSpace, + alignment, + rotationRad, + ); + const scaleStep = compressedScale.width / labels.length; + // this optimal rotation is computed on a suboptimal compressed scale, it can be further enhanced with a monotonic hill climber + const optimalRotation = + scaleStep > maxLabelBBox.width ? 0 : Math.asin(Math.min(maxLabelBBox.height / scaleStep, 1)); + // if the current requested rotation is not at least bigger then the optimal one, recalculate the compression + // using the optimal one forcing the rotation to be without overlaps + const { width, height, left, right, minRotation } = { + ...(rotationRad !== 0 && optimalRotation > rotationRad + ? computeCompressedScale( + style, + scale, + measuredLabels, + containerWidth, + surroundingSpace, + alignment, + optimalRotation, + ) + : compressedScale), + minRotation: isRotated ? Math.max(optimalRotation, rotationRad) : 0, + }; + + const validCompression = isFiniteNumber(width); + return { + height: validCompression ? height : 0, + width: validCompression ? width : Math.max(containerWidth - surroundingSpace[0] - surroundingSpace[1], 0), + left: validCompression ? left : surroundingSpace[0], + right: validCompression ? right : surroundingSpace[1], + tickCadence: validCompression ? 1 : NaN, + minRotation, + }; + } + + // TODO refactor and move to monotonic hill climber and no mutations + // reduce the tick cadence on time scale to avoid overlaps and overflows + let tickCadence = 1; + let dimension = computeCompressedScale( + style, + scale, + measuredLabels, + containerWidth, + surroundingSpace, + alignment, + rotationRad, + ); + + for (let i = 1; i < measuredLabels.length; i++) { + if ((!dimension.overlaps && !dimension.overflow.right) || !isFiniteNumber(dimension.width)) { + break; + } + dimension = computeCompressedScale( + style, + scale, + measuredLabels.filter((_, index) => index % (i + 1) === 0), + containerWidth, + surroundingSpace, + alignment, + rotationRad, + ); + tickCadence++; + } + + // hide the axis because there is no space for labels + if (!isFiniteNumber(dimension.width)) { + return { + // hide the whole axis + height: 0, + width: Math.max(containerWidth - surroundingSpace[0] - surroundingSpace[1], 0), + left: surroundingSpace[0], + right: surroundingSpace[1], + // hide all ticks + tickCadence: NaN, + minRotation: rotationRad, + }; + } + + return { + ...dimension, + tickCadence, + minRotation: rotationRad, + }; +} + +function computeCompressedScale( + style: HeatmapStyle['xAxisLabel'], + scale: (d: NonNullable) => number, + labels: Array }>, + containerWidth: number, + surroundingSpace: [number, number], + alignment: 'left' | 'right' | 'center', + rotation: Radian, +): Size & { left: number; right: number; overlaps: boolean; overflow: { left: boolean; right: boolean } } { + const { itemsPerSideSize, domainPositions, hMax } = labels.reduce<{ + wMax: number; + hMax: number; + itemsPerSideSize: [number, number][]; + domainPositions: number[]; + }>( + (acc, { width, height, label }) => { + // rotate the label box coordinates + const labelRect: Vec2[] = [ + [0, 0], + [width, 0], + [width, height], + [0, height], + ]; + + const rotationOrigin: Vec2 = + alignment === 'right' ? [width, height / 2] : alignment === 'left' ? [0, height / 2] : [width / 2, height / 2]; + + const rotatedVectors = labelRect.map((vector) => rotate2(rotation, sub2(vector, rotationOrigin))); + + // find the rotated bounding box + const x = extent(rotatedVectors.map((v) => v[0])); + const y = extent(rotatedVectors.map((v) => v[1])); + acc.wMax = Math.max(acc.wMax, Math.abs(x[1] - x[0])); + acc.hMax = Math.max(acc.hMax, Math.abs(y[1] - y[0])); + + // describe the item width as the left and right vector size from the rotation origin + acc.itemsPerSideSize.push([Math.abs(x[0]), Math.abs(x[1])]); + + // use a categorical scale with labels aligned to the center to compute the domain position + const domainPosition = scale(label); + acc.domainPositions.push(domainPosition); + return acc; + }, + { wMax: -Infinity, hMax: -Infinity, itemsPerSideSize: [], domainPositions: [] }, + ); + + // account for the left and right space (Y axes, overflows etc) + const globalDomainPositions = [0, ...domainPositions, 1]; + const globalItemWidth: [number, number][] = [[surroundingSpace[0], 0], ...itemsPerSideSize, [0, surroundingSpace[1]]]; + + const { scaleMultiplier, bounds } = screenspaceMarkerScaleCompressor( + globalDomainPositions, + globalItemWidth, + containerWidth, + ); + + // check label overlaps using the computed compressed scale + const overlaps = itemsPerSideSize.some(([, rightSide], i) => { + if (i >= itemsPerSideSize.length - 2) { + return false; + } + const currentItemRightSide = domainPositions[i] * scaleMultiplier + rightSide + pad(style.padding, 'right'); + const nextItemLeftSize = + domainPositions[i + 1] * scaleMultiplier - itemsPerSideSize[i + 1][0] - pad(style.padding, 'left'); + return currentItemRightSide > nextItemLeftSize; + }); + + const leftMargin = isFiniteNumber(bounds[0]) + ? globalItemWidth[bounds[0]][0] - scaleMultiplier * globalDomainPositions[bounds[0]] + : 0; + const rightMargin = isFiniteNumber(bounds[1]) ? globalItemWidth[bounds[1]][1] : 0; + + return { + // the horizontal space + width: scaleMultiplier, + right: rightMargin, + left: leftMargin, + // the height represent the height of the max rotated bbox plus the padding and the vertical position of the rotation origin + height: hMax + pad(style.padding, 'top') + style.fontSize / 2, + overlaps, + overflow: { + // true if a label exist protrude to the left making the scale shrink from the left + // the current check is based on the way we construct globalItemWidth and globalDomainPositions + left: bounds[0] !== 0, + // true if a label exist protrude to the right making the scale shrink from the right + // the current check is based on the way we construct globalItemWidth and globalDomainPositions + right: bounds[1] !== globalDomainPositions.length - 1, + }, + }; +} diff --git a/packages/charts/src/chart_types/metric/state/chart_state.tsx b/packages/charts/src/chart_types/metric/state/chart_state.tsx index ee23c7a63c..2fd3cc0f7e 100644 --- a/packages/charts/src/chart_types/metric/state/chart_state.tsx +++ b/packages/charts/src/chart_types/metric/state/chart_state.tsx @@ -47,4 +47,10 @@ export class MetricState implements InternalChartState { getMainProjectionArea = () => ({ width: 0, height: 0, top: 0, left: 0 }); getBrushArea = () => null; getDebugState = () => ({}); + getSmallMultiplesDomains() { + return { + smHDomain: [], + smVDomain: [], + }; + } } diff --git a/packages/charts/src/chart_types/partition_chart/state/chart_state.tsx b/packages/charts/src/chart_types/partition_chart/state/chart_state.tsx index 5b187c7d5f..b500e018f5 100644 --- a/packages/charts/src/chart_types/partition_chart/state/chart_state.tsx +++ b/packages/charts/src/chart_types/partition_chart/state/chart_state.tsx @@ -135,4 +135,11 @@ export class PartitionState implements InternalChartState { getChartTypeDescription(state: GlobalChartState): string { return getChartTypeDescriptionSelector(state); } + + getSmallMultiplesDomains() { + return { + smHDomain: [], + smVDomain: [], + }; + } } diff --git a/packages/charts/src/chart_types/timeslip/internal_chart_state.ts b/packages/charts/src/chart_types/timeslip/internal_chart_state.ts index b827b8f7b0..15d36560eb 100644 --- a/packages/charts/src/chart_types/timeslip/internal_chart_state.ts +++ b/packages/charts/src/chart_types/timeslip/internal_chart_state.ts @@ -43,4 +43,10 @@ export class TimeslipState implements InternalChartState { getMainProjectionArea = () => ({ width: 0, height: 0, top: 0, left: 0 }); getBrushArea = () => null; getDebugState = () => ({}); + getSmallMultiplesDomains() { + return { + smHDomain: [], + smVDomain: [], + }; + } } diff --git a/packages/charts/src/chart_types/wordcloud/state/chart_state.tsx b/packages/charts/src/chart_types/wordcloud/state/chart_state.tsx index af79ab4990..3281bb6dac 100644 --- a/packages/charts/src/chart_types/wordcloud/state/chart_state.tsx +++ b/packages/charts/src/chart_types/wordcloud/state/chart_state.tsx @@ -113,4 +113,11 @@ export class WordcloudState implements InternalChartState { getDebugState(): DebugState { return {}; } + + getSmallMultiplesDomains() { + return { + smHDomain: [], + smVDomain: [], + }; + } } diff --git a/packages/charts/src/chart_types/xy_chart/annotations/line/dimensions.ts b/packages/charts/src/chart_types/xy_chart/annotations/line/dimensions.ts index 2b6e6f7914..f68a6e88b9 100644 --- a/packages/charts/src/chart_types/xy_chart/annotations/line/dimensions.ts +++ b/packages/charts/src/chart_types/xy_chart/annotations/line/dimensions.ts @@ -8,6 +8,7 @@ import { AnnotationLineProps } from './types'; import { Colors } from '../../../../common/colors'; +import { SmallMultipleScales, getPanelSize } from '../../../../common/panel_utils'; import { Line } from '../../../../geoms/types'; import { ScaleBand, ScaleContinuous } from '../../../../scales'; import { isBandScale, isContinuousScale } from '../../../../scales/types'; @@ -15,10 +16,8 @@ import { Position, Rotation } from '../../../../utils/common'; import { Dimensions, Size } from '../../../../utils/dimensions'; import { GroupId } from '../../../../utils/ids'; import { mergeWithDefaultAnnotationLine } from '../../../../utils/themes/merge_utils'; -import { SmallMultipleScales } from '../../state/selectors/compute_small_multiple_scales'; import { isHorizontalRotation, isVerticalRotation } from '../../state/utils/common'; import { computeXScaleOffset } from '../../state/utils/utils'; -import { getPanelSize } from '../../utils/panel'; import { AnnotationDomainType, LineAnnotationDatum, LineAnnotationSpec } from '../../utils/specs'; function computeYDomainLineAnnotationDimensions( diff --git a/packages/charts/src/chart_types/xy_chart/annotations/rect/dimensions.ts b/packages/charts/src/chart_types/xy_chart/annotations/rect/dimensions.ts index da6b4e56dc..509dbe788e 100644 --- a/packages/charts/src/chart_types/xy_chart/annotations/rect/dimensions.ts +++ b/packages/charts/src/chart_types/xy_chart/annotations/rect/dimensions.ts @@ -7,6 +7,7 @@ */ import { AnnotationRectProps } from './types'; +import { getPanelSize, SmallMultipleScales } from '../../../../common/panel_utils'; import { ScaleBand, ScaleContinuous } from '../../../../scales'; import { isBandScale, isContinuousScale } from '../../../../scales/types'; import { isDefined, isNil, Position, Rotation } from '../../../../utils/common'; @@ -14,10 +15,8 @@ import { AxisId, GroupId } from '../../../../utils/ids'; import { Point } from '../../../../utils/point'; import { AxisStyle } from '../../../../utils/themes/theme'; import { PrimitiveValue } from '../../../partition_chart/layout/utils/group_by_rollup'; -import { SmallMultipleScales } from '../../state/selectors/compute_small_multiple_scales'; import { isHorizontalRotation, isVerticalRotation } from '../../state/utils/common'; import { getAxesSpecForSpecId } from '../../state/utils/spec'; -import { getPanelSize } from '../../utils/panel'; import { AxisSpec, RectAnnotationDatum, RectAnnotationSpec } from '../../utils/specs'; import { Bounds } from '../types'; diff --git a/packages/charts/src/chart_types/xy_chart/annotations/utils.ts b/packages/charts/src/chart_types/xy_chart/annotations/utils.ts index fa235ea031..89fb737eac 100644 --- a/packages/charts/src/chart_types/xy_chart/annotations/utils.ts +++ b/packages/charts/src/chart_types/xy_chart/annotations/utils.ts @@ -9,13 +9,13 @@ import { computeLineAnnotationDimensions } from './line/dimensions'; import { computeRectAnnotationDimensions } from './rect/dimensions'; import { AnnotationDimensions } from './types'; +import { SmallMultipleScales } from '../../../common/panel_utils'; import { SettingsSpec } from '../../../specs'; import { Rotation, Position } from '../../../utils/common'; import { Dimensions } from '../../../utils/dimensions'; import { AnnotationId, AxisId, GroupId } from '../../../utils/ids'; import { Point } from '../../../utils/point'; import { AxisStyle } from '../../../utils/themes/theme'; -import { SmallMultipleScales } from '../state/selectors/compute_small_multiple_scales'; import { getAxesSpecForSpecId } from '../state/utils/spec'; import { ComputedGeometries } from '../state/utils/types'; import { AnnotationDomainType, AnnotationSpec, AxisSpec, isLineAnnotation } from '../utils/specs'; diff --git a/packages/charts/src/chart_types/xy_chart/renderer/canvas/panels/panels.ts b/packages/charts/src/chart_types/xy_chart/renderer/canvas/panels/panels.ts index 70ed03099a..9ddb8034c1 100644 --- a/packages/charts/src/chart_types/xy_chart/renderer/canvas/panels/panels.ts +++ b/packages/charts/src/chart_types/xy_chart/renderer/canvas/panels/panels.ts @@ -9,10 +9,10 @@ import { renderTitle } from './title'; import { Colors } from '../../../../../common/colors'; import { withContext } from '../../../../../renderers/canvas'; +import { PanelGeoms } from '../../../../../state/selectors/compute_panels'; import { Position } from '../../../../../utils/common'; import { AxisId } from '../../../../../utils/ids'; import { Point } from '../../../../../utils/point'; -import { PanelGeoms } from '../../../state/selectors/compute_panels'; import { getSpecsById } from '../../../state/utils/spec'; import { AxisSpec } from '../../../utils/specs'; import { AxesProps, AxisProps, renderAxis } from '../axes'; diff --git a/packages/charts/src/chart_types/xy_chart/renderer/canvas/renderers.ts b/packages/charts/src/chart_types/xy_chart/renderer/canvas/renderers.ts index 6766bf2b98..0844752484 100644 --- a/packages/charts/src/chart_types/xy_chart/renderer/canvas/renderers.ts +++ b/packages/charts/src/chart_types/xy_chart/renderer/canvas/renderers.ts @@ -52,7 +52,9 @@ export function renderXYChartCanvas2d( panelGeoms, hoveredAnnotationIds, } = props; + const transform = { x: renderingArea.left + chartTransform.x, y: renderingArea.top + chartTransform.y }; + renderLayers(ctx, [ () => clearCanvas(ctx, 'transparent'), diff --git a/packages/charts/src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx b/packages/charts/src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx index ad750bb0c0..ef5fe3c92e 100644 --- a/packages/charts/src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx +++ b/packages/charts/src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx @@ -17,6 +17,7 @@ import { LegendItem } from '../../../../common/legend'; import { ScreenReaderSummary } from '../../../../components/accessibility'; import { onChartRendered } from '../../../../state/actions/chart'; import { GlobalChartState } from '../../../../state/chart_state'; +import { computePanelsSelectors, PanelGeoms } from '../../../../state/selectors/compute_panels'; import { A11ySettings, DEFAULT_A11Y_SETTINGS, @@ -37,7 +38,6 @@ import { AnnotationDimensions } from '../../annotations/types'; import { computeAnnotationDimensionsSelector } from '../../state/selectors/compute_annotations'; import { computeChartDimensionsSelector } from '../../state/selectors/compute_chart_dimensions'; import { computeChartTransformSelector } from '../../state/selectors/compute_chart_transform'; -import { computePanelsSelectors, PanelGeoms } from '../../state/selectors/compute_panels'; import { computePerPanelAxesGeomsSelector, PerPanelAxisGeoms, @@ -228,7 +228,6 @@ const DEFAULT_PROPS: ReactiveChartStateProps = { y: 0, rotate: 0, }, - axesSpecs: [], perPanelAxisGeoms: [], perPanelGridLines: [], diff --git a/packages/charts/src/chart_types/xy_chart/state/chart_state.tsx b/packages/charts/src/chart_types/xy_chart/state/chart_state.tsx index f8ae321f6b..8d4087ba72 100644 --- a/packages/charts/src/chart_types/xy_chart/state/chart_state.tsx +++ b/packages/charts/src/chart_types/xy_chart/state/chart_state.tsx @@ -10,6 +10,7 @@ import React, { RefObject } from 'react'; import { computeChartDimensionsSelector } from './selectors/compute_chart_dimensions'; import { computeLegendSelector } from './selectors/compute_legend'; +import { computeSeriesDomainsSelector } from './selectors/compute_series_domains'; import { getBrushAreaSelector } from './selectors/get_brush_area'; import { getChartTypeDescriptionSelector } from './selectors/get_chart_type_description'; import { getPointerCursorSelector } from './selectors/get_cursor_pointer'; @@ -145,6 +146,10 @@ export class XYAxisChartState implements InternalChartState { return getTooltipAnchorPositionSelector(globalState); } + getSmallMultiplesDomains(globalState: GlobalChartState) { + return computeSeriesDomainsSelector(globalState); + } + eventCallbacks(globalState: GlobalChartState) { this.onElementOverCaller(globalState); this.onElementOutCaller(globalState); diff --git a/packages/charts/src/chart_types/xy_chart/state/selectors/compute_annotations.ts b/packages/charts/src/chart_types/xy_chart/state/selectors/compute_annotations.ts index 29521ed48a..a980abe276 100644 --- a/packages/charts/src/chart_types/xy_chart/state/selectors/compute_annotations.ts +++ b/packages/charts/src/chart_types/xy_chart/state/selectors/compute_annotations.ts @@ -7,11 +7,11 @@ */ import { computeSeriesGeometriesSelector } from './compute_series_geometries'; -import { computeSmallMultipleScalesSelector } from './compute_small_multiple_scales'; import { getAxesStylesSelector } from './get_axis_styles'; import { getAxisSpecsSelector, getAnnotationSpecsSelector } from './get_specs'; import { isHistogramModeEnabledSelector } from './is_histogram_mode_enabled'; import { createCustomCachedSelector } from '../../../../state/create_selector'; +import { computeSmallMultipleScalesSelector } from '../../../../state/selectors/compute_small_multiple_scales'; import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_spec'; import { AxisId } from '../../../../utils/ids'; diff --git a/packages/charts/src/chart_types/xy_chart/state/selectors/compute_axes_geometries.ts b/packages/charts/src/chart_types/xy_chart/state/selectors/compute_axes_geometries.ts index 4a6aca27e0..b1b5a0cc89 100644 --- a/packages/charts/src/chart_types/xy_chart/state/selectors/compute_axes_geometries.ts +++ b/packages/charts/src/chart_types/xy_chart/state/selectors/compute_axes_geometries.ts @@ -7,13 +7,11 @@ */ import { computeChartDimensionsSelector } from './compute_chart_dimensions'; -import { computeSmallMultipleScalesSelector } from './compute_small_multiple_scales'; -import { countBarsInClusterSelector } from './count_bars_in_cluster'; import { getAxesStylesSelector } from './get_axis_styles'; import { axisSpecsLookupSelector } from './get_specs'; -import { isHistogramModeEnabledSelector } from './is_histogram_mode_enabled'; import { getVisibleTickSetsSelector } from './visible_ticks'; import { createCustomCachedSelector } from '../../../../state/create_selector'; +import { computeSmallMultipleScalesSelector } from '../../../../state/selectors/compute_small_multiple_scales'; import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; import { getAxesGeometries } from '../../utils/axis_utils'; @@ -25,8 +23,6 @@ export const computeAxesGeometriesSelector = createCustomCachedSelector( axisSpecsLookupSelector, getAxesStylesSelector, computeSmallMultipleScalesSelector, - countBarsInClusterSelector, - isHistogramModeEnabledSelector, getVisibleTickSetsSelector, ], getAxesGeometries, diff --git a/packages/charts/src/chart_types/xy_chart/state/selectors/compute_per_panel_axes_geoms.ts b/packages/charts/src/chart_types/xy_chart/state/selectors/compute_per_panel_axes_geoms.ts index b73a4c167f..8af685496e 100644 --- a/packages/charts/src/chart_types/xy_chart/state/selectors/compute_per_panel_axes_geoms.ts +++ b/packages/charts/src/chart_types/xy_chart/state/selectors/compute_per_panel_axes_geoms.ts @@ -7,32 +7,25 @@ */ import { computeAxesGeometriesSelector } from './compute_axes_geometries'; -import { computeSmallMultipleScalesSelector, SmallMultipleScales } from './compute_small_multiple_scales'; -import { getSmallMultiplesIndexOrderSelector, SmallMultiplesGroupBy } from './get_specs'; +import { + getPanelTitle, + getPerPanelMap, + hasSMDomain, + PerPanelMap, + SmallMultipleScales, +} from '../../../../common/panel_utils'; import { createCustomCachedSelector } from '../../../../state/create_selector'; -import { Position, safeFormat } from '../../../../utils/common'; +import { computeSmallMultipleScalesSelector } from '../../../../state/selectors/compute_small_multiple_scales'; +import { getSmallMultiplesIndexOrderSelector } from '../../../../state/selectors/get_small_multiples_index_order'; +import { Position } from '../../../../utils/common'; import { isHorizontalAxis, isVerticalAxis } from '../../utils/axis_type_utils'; import { AxisGeometry } from '../../utils/axis_utils'; -import { hasSMDomain } from '../../utils/panel'; -import { PerPanelMap, getPerPanelMap } from '../../utils/panel_utils'; /** @internal */ export type PerPanelAxisGeoms = { axesGeoms: AxisGeometry[]; } & PerPanelMap; -const getPanelTitle = ( - isVertical: boolean, - verticalValue: any, - horizontalValue: any, - groupBy?: SmallMultiplesGroupBy, -): string => { - const formatter = isVertical ? groupBy?.vertical?.format : groupBy?.horizontal?.format; - const value = isVertical ? `${verticalValue}` : `${horizontalValue}`; - - return safeFormat(value, formatter); -}; - const isPrimaryColumnFn = ({ horizontal: { domain } }: SmallMultipleScales) => (position: Position, horizontalValue: any) => diff --git a/packages/charts/src/chart_types/xy_chart/state/selectors/compute_series_domains.ts b/packages/charts/src/chart_types/xy_chart/state/selectors/compute_series_domains.ts index 8f9071b00f..3539503fef 100644 --- a/packages/charts/src/chart_types/xy_chart/state/selectors/compute_series_domains.ts +++ b/packages/charts/src/chart_types/xy_chart/state/selectors/compute_series_domains.ts @@ -7,10 +7,11 @@ */ import { getScaleConfigsFromSpecsSelector } from './get_api_scale_configs'; -import { getAnnotationSpecsSelector, getSeriesSpecsSelector, getSmallMultiplesIndexOrderSelector } from './get_specs'; +import { getAnnotationSpecsSelector, getSeriesSpecsSelector } from './get_specs'; import { GlobalChartState } from '../../../../state/chart_state'; import { createCustomCachedSelector } from '../../../../state/create_selector'; import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_spec'; +import { getSmallMultiplesIndexOrderSelector } from '../../../../state/selectors/get_small_multiples_index_order'; import { computeSeriesDomains } from '../utils/utils'; const getDeselectedSeriesSelector = (state: GlobalChartState) => state.interactions.deselectedDataSeries; diff --git a/packages/charts/src/chart_types/xy_chart/state/selectors/compute_series_geometries.ts b/packages/charts/src/chart_types/xy_chart/state/selectors/compute_series_geometries.ts index 82a40f3d92..1badfae02f 100644 --- a/packages/charts/src/chart_types/xy_chart/state/selectors/compute_series_geometries.ts +++ b/packages/charts/src/chart_types/xy_chart/state/selectors/compute_series_geometries.ts @@ -8,11 +8,11 @@ import { getFallBackTickFormatter } from './compute_axis_ticks_dimensions'; import { computeSeriesDomainsSelector } from './compute_series_domains'; -import { computeSmallMultipleScalesSelector } from './compute_small_multiple_scales'; import { getSeriesColorsSelector } from './get_series_color_map'; import { getSeriesSpecsSelector, getAxisSpecsSelector } from './get_specs'; import { isHistogramModeEnabledSelector } from './is_histogram_mode_enabled'; import { createCustomCachedSelector } from '../../../../state/create_selector'; +import { computeSmallMultipleScalesSelector } from '../../../../state/selectors/compute_small_multiple_scales'; import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_spec'; import { withTextMeasure } from '../../../../utils/bbox/canvas_text_bbox_calculator'; diff --git a/packages/charts/src/chart_types/xy_chart/state/selectors/compute_small_multiple_scales.ts b/packages/charts/src/chart_types/xy_chart/state/selectors/compute_small_multiple_scales.ts deleted file mode 100644 index 86f6c7069a..0000000000 --- a/packages/charts/src/chart_types/xy_chart/state/selectors/compute_small_multiple_scales.ts +++ /dev/null @@ -1,47 +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 { computeChartDimensionsSelector } from './compute_chart_dimensions'; -import { computeSeriesDomainsSelector } from './compute_series_domains'; -import { ScaleBand } from '../../../../scales'; -import { DEFAULT_SM_PANEL_PADDING, RelativeBandsPadding } from '../../../../specs/small_multiples'; -import { createCustomCachedSelector } from '../../../../state/create_selector'; -import { getSmallMultiplesSpec } from '../../../../state/selectors/get_small_multiples_spec'; -import { OrdinalDomain } from '../../../../utils/domain'; - -/** @internal */ -export interface SmallMultipleScales { - horizontal: ScaleBand; - vertical: ScaleBand; -} - -/** - * Return the small multiple scales for horizontal and vertical grids - * @internal - */ -export const computeSmallMultipleScalesSelector = createCustomCachedSelector( - [computeSeriesDomainsSelector, computeChartDimensionsSelector, getSmallMultiplesSpec], - ({ smHDomain, smVDomain }, { chartDimensions: { width, height } }, smSpec): SmallMultipleScales => { - return { - horizontal: getScale(smHDomain, width, smSpec && smSpec[0].style?.horizontalPanelPadding), - vertical: getScale(smVDomain, height, smSpec && smSpec[0].style?.verticalPanelPadding), - }; - }, -); - -/** - * @internal - */ -export function getScale( - domain: OrdinalDomain, - maxRange: number, - padding: RelativeBandsPadding = DEFAULT_SM_PANEL_PADDING, -): ScaleBand { - const singlePanelSmallMultiple = domain.length <= 1; - return new ScaleBand(domain, [0, maxRange], undefined, singlePanelSmallMultiple ? 0 : padding); -} diff --git a/packages/charts/src/chart_types/xy_chart/state/selectors/get_brush_area.ts b/packages/charts/src/chart_types/xy_chart/state/selectors/get_brush_area.ts index 7f3cd02a86..074cb46dfe 100644 --- a/packages/charts/src/chart_types/xy_chart/state/selectors/get_brush_area.ts +++ b/packages/charts/src/chart_types/xy_chart/state/selectors/get_brush_area.ts @@ -7,10 +7,11 @@ */ import { computeChartDimensionsSelector } from './compute_chart_dimensions'; -import { computeSmallMultipleScalesSelector, SmallMultipleScales } from './compute_small_multiple_scales'; +import { SmallMultipleScales } from '../../../../common/panel_utils'; import { BrushAxis } from '../../../../specs/constants'; import { GlobalChartState } from '../../../../state/chart_state'; import { createCustomCachedSelector } from '../../../../state/create_selector'; +import { computeSmallMultipleScalesSelector } from '../../../../state/selectors/compute_small_multiple_scales'; import { getChartRotationSelector } from '../../../../state/selectors/get_chart_rotation'; import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_spec'; import { clamp, isNil, Rotation } from '../../../../utils/common'; diff --git a/packages/charts/src/chart_types/xy_chart/state/selectors/get_cursor_band.ts b/packages/charts/src/chart_types/xy_chart/state/selectors/get_cursor_band.ts index defb2e7440..172f5d7045 100644 --- a/packages/charts/src/chart_types/xy_chart/state/selectors/get_cursor_band.ts +++ b/packages/charts/src/chart_types/xy_chart/state/selectors/get_cursor_band.ts @@ -8,17 +8,18 @@ import { computeChartDimensionsSelector } from './compute_chart_dimensions'; import { computeSeriesGeometriesSelector } from './compute_series_geometries'; -import { computeSmallMultipleScalesSelector, SmallMultipleScales } from './compute_small_multiple_scales'; import { countBarsInClusterSelector } from './count_bars_in_cluster'; import { getGeometriesIndexKeysSelector } from './get_geometries_index_keys'; import { getOrientedProjectedPointerPositionSelector } from './get_oriented_projected_pointer_position'; import { PointerPosition } from './get_projected_pointer_position'; import { getSeriesSpecsSelector } from './get_specs'; import { isTooltipSnapEnableSelector } from './is_tooltip_snap_enabled'; +import { SmallMultipleScales } from '../../../../common/panel_utils'; import { Rect } from '../../../../geoms/types'; import { SettingsSpec, PointerEvent } from '../../../../specs/settings'; import { GlobalChartState } from '../../../../state/chart_state'; import { createCustomCachedSelector } from '../../../../state/create_selector'; +import { computeSmallMultipleScalesSelector } from '../../../../state/selectors/compute_small_multiple_scales'; import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_spec'; import { isNil } from '../../../../utils/common'; import { isValidPointerOverEvent } from '../../../../utils/events'; diff --git a/packages/charts/src/chart_types/xy_chart/state/selectors/get_grid_lines.ts b/packages/charts/src/chart_types/xy_chart/state/selectors/get_grid_lines.ts index e5ff396b6b..124a16fea2 100644 --- a/packages/charts/src/chart_types/xy_chart/state/selectors/get_grid_lines.ts +++ b/packages/charts/src/chart_types/xy_chart/state/selectors/get_grid_lines.ts @@ -7,9 +7,9 @@ */ import { computeAxesGeometriesSelector } from './compute_axes_geometries'; -import { computeSmallMultipleScalesSelector } from './compute_small_multiple_scales'; import { getAxisSpecsSelector } from './get_specs'; import { createCustomCachedSelector } from '../../../../state/create_selector'; +import { computeSmallMultipleScalesSelector } from '../../../../state/selectors/compute_small_multiple_scales'; import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; import { getGridLines } from '../../utils/grid_lines'; diff --git a/packages/charts/src/chart_types/xy_chart/state/selectors/get_oriented_projected_pointer_position.ts b/packages/charts/src/chart_types/xy_chart/state/selectors/get_oriented_projected_pointer_position.ts index 0fedc6b4fb..3b094e8346 100644 --- a/packages/charts/src/chart_types/xy_chart/state/selectors/get_oriented_projected_pointer_position.ts +++ b/packages/charts/src/chart_types/xy_chart/state/selectors/get_oriented_projected_pointer_position.ts @@ -6,13 +6,13 @@ * Side Public License, v 1. */ -import { computeSmallMultipleScalesSelector, SmallMultipleScales } from './compute_small_multiple_scales'; import { getProjectedPointerPositionSelector, PointerPosition } from './get_projected_pointer_position'; +import { SmallMultipleScales, getPanelSize } from '../../../../common/panel_utils'; import { SettingsSpec } from '../../../../specs/settings'; import { createCustomCachedSelector } from '../../../../state/create_selector'; +import { computeSmallMultipleScalesSelector } from '../../../../state/selectors/compute_small_multiple_scales'; import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_spec'; import { getOrientedXPosition, getOrientedYPosition } from '../../utils/interactions'; -import { getPanelSize } from '../../utils/panel'; /** @internal */ export const getOrientedProjectedPointerPositionSelector = createCustomCachedSelector( diff --git a/packages/charts/src/chart_types/xy_chart/state/selectors/get_projected_pointer_position.ts b/packages/charts/src/chart_types/xy_chart/state/selectors/get_projected_pointer_position.ts index 3ebea3d964..2886a014fe 100644 --- a/packages/charts/src/chart_types/xy_chart/state/selectors/get_projected_pointer_position.ts +++ b/packages/charts/src/chart_types/xy_chart/state/selectors/get_projected_pointer_position.ts @@ -7,9 +7,10 @@ */ import { computeChartDimensionsSelector } from './compute_chart_dimensions'; -import { computeSmallMultipleScalesSelector, SmallMultipleScales } from './compute_small_multiple_scales'; +import { SmallMultipleScales } from '../../../../common/panel_utils'; import { ScaleBand } from '../../../../scales/scale_band'; import { createCustomCachedSelector } from '../../../../state/create_selector'; +import { computeSmallMultipleScalesSelector } from '../../../../state/selectors/compute_small_multiple_scales'; import { getActivePointerPosition } from '../../../../state/selectors/get_active_pointer_position'; import { Dimensions } from '../../../../utils/dimensions'; import { Point } from '../../../../utils/point'; diff --git a/packages/charts/src/chart_types/xy_chart/state/selectors/get_specs.ts b/packages/charts/src/chart_types/xy_chart/state/selectors/get_specs.ts index 3c6691f285..ef6002ba83 100644 --- a/packages/charts/src/chart_types/xy_chart/state/selectors/get_specs.ts +++ b/packages/charts/src/chart_types/xy_chart/state/selectors/get_specs.ts @@ -7,19 +7,12 @@ */ import { ChartType } from '../../..'; -import { GroupBySpec, SmallMultiplesSpec } from '../../../../specs'; import { SpecType } from '../../../../specs/constants'; import { createCustomCachedSelector } from '../../../../state/create_selector'; import { getSpecs } from '../../../../state/selectors/get_specs'; import { getSpecsFromStore } from '../../../../state/utils'; import { AnnotationSpec, AxisSpec, BasicSeriesSpec } from '../../utils/specs'; -/** @internal */ -export interface SmallMultiplesGroupBy { - vertical?: GroupBySpec; - horizontal?: GroupBySpec; -} - /** @internal */ export const getAxisSpecsSelector = createCustomCachedSelector([getSpecs], (specs): AxisSpec[] => getSpecsFromStore(specs, ChartType.XYAxis, SpecType.Axis), @@ -40,16 +33,3 @@ export const getSeriesSpecsSelector = createCustomCachedSelector([getSpecs], (sp export const getAnnotationSpecsSelector = createCustomCachedSelector([getSpecs], (specs) => getSpecsFromStore(specs, ChartType.XYAxis, SpecType.Annotation), ); - -/** @internal */ -export const getSmallMultiplesIndexOrderSelector = createCustomCachedSelector( - [getSpecs], - (specs): SmallMultiplesGroupBy | undefined => { - const [smallMultiples] = getSpecsFromStore(specs, ChartType.Global, SpecType.SmallMultiples); - const groupBySpecs = getSpecsFromStore(specs, ChartType.Global, SpecType.IndexOrder); - return { - horizontal: groupBySpecs.find((s) => s.id === smallMultiples?.splitHorizontally), - vertical: groupBySpecs.find((s) => s.id === smallMultiples?.splitVertically), - }; - }, -); diff --git a/packages/charts/src/chart_types/xy_chart/state/selectors/get_tooltip_anchor_position.ts b/packages/charts/src/chart_types/xy_chart/state/selectors/get_tooltip_anchor_position.ts index 7bffbb8c62..ab50c07015 100644 --- a/packages/charts/src/chart_types/xy_chart/state/selectors/get_tooltip_anchor_position.ts +++ b/packages/charts/src/chart_types/xy_chart/state/selectors/get_tooltip_anchor_position.ts @@ -7,11 +7,11 @@ */ import { computeChartDimensionsSelector } from './compute_chart_dimensions'; -import { computeSmallMultipleScalesSelector } from './compute_small_multiple_scales'; import { getCursorBandPositionSelector } from './get_cursor_band'; import { getProjectedPointerPositionSelector } from './get_projected_pointer_position'; import { AnchorPosition } from '../../../../components/portal/types'; import { createCustomCachedSelector } from '../../../../state/create_selector'; +import { computeSmallMultipleScalesSelector } from '../../../../state/selectors/compute_small_multiple_scales'; import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_spec'; import { getTooltipSpecSelector } from '../../../../state/selectors/get_tooltip_spec'; import { isNil } from '../../../../utils/common'; diff --git a/packages/charts/src/chart_types/xy_chart/state/selectors/on_brush_end_caller.ts b/packages/charts/src/chart_types/xy_chart/state/selectors/on_brush_end_caller.ts index 3efccc1cb5..e2f9f87d5d 100644 --- a/packages/charts/src/chart_types/xy_chart/state/selectors/on_brush_end_caller.ts +++ b/packages/charts/src/chart_types/xy_chart/state/selectors/on_brush_end_caller.ts @@ -9,19 +9,20 @@ import { Selector } from 'reselect'; import { computeChartDimensionsSelector } from './compute_chart_dimensions'; -import { computeSmallMultipleScalesSelector, SmallMultipleScales } from './compute_small_multiple_scales'; import { getPlotAreaRestrictedPoint, getPointsConstraintToSinglePanel, PanelPoints } from './get_brush_area'; import { getComputedScalesSelector } from './get_computed_scales'; import { getSeriesSpecsSelector } from './get_specs'; import { isBrushAvailableSelector } from './is_brush_available'; import { isHistogramModeEnabledSelector } from './is_histogram_mode_enabled'; import { ChartType } from '../../..'; +import { SmallMultipleScales } from '../../../../common/panel_utils'; import { ScaleContinuous } from '../../../../scales'; import { isContinuousScale } from '../../../../scales/types'; import { GroupBrushExtent, SeriesSpecs, XYBrushEvent } from '../../../../specs'; import { BrushAxis } from '../../../../specs/constants'; import { DragState, GlobalChartState } from '../../../../state/chart_state'; import { createCustomCachedSelector } from '../../../../state/create_selector'; +import { computeSmallMultipleScalesSelector } from '../../../../state/selectors/compute_small_multiple_scales'; import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_spec'; import { clamp, Rotation } from '../../../../utils/common'; import { Dimensions } from '../../../../utils/dimensions'; diff --git a/packages/charts/src/chart_types/xy_chart/state/selectors/visible_ticks.ts b/packages/charts/src/chart_types/xy_chart/state/selectors/visible_ticks.ts index 1acf6243a3..f57457c7b8 100644 --- a/packages/charts/src/chart_types/xy_chart/state/selectors/visible_ticks.ts +++ b/packages/charts/src/chart_types/xy_chart/state/selectors/visible_ticks.ts @@ -9,15 +9,16 @@ import { AxisLabelFormatter } from './axis_tick_formatter'; import { getJoinedVisibleAxesData, getLabelBox, JoinedAxisData } from './compute_axis_ticks_dimensions'; import { computeSeriesDomainsSelector } from './compute_series_domains'; -import { computeSmallMultipleScalesSelector, SmallMultipleScales } from './compute_small_multiple_scales'; import { countBarsInClusterSelector } from './count_bars_in_cluster'; import { getBarPaddingsSelector } from './get_bar_paddings'; import { isHistogramModeEnabledSelector } from './is_histogram_mode_enabled'; +import { getPanelSize, SmallMultipleScales } from '../../../../common/panel_utils'; import { ScaleBand, ScaleContinuous } from '../../../../scales'; import { ScaleType } from '../../../../scales/constants'; import { isContinuousScale } from '../../../../scales/types'; import { AxisSpec, SettingsSpec } from '../../../../specs'; import { createCustomCachedSelector } from '../../../../state/create_selector'; +import { computeSmallMultipleScalesSelector } from '../../../../state/selectors/compute_small_multiple_scales'; import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_spec'; import { withTextMeasure } from '../../../../utils/bbox/canvas_text_bbox_calculator'; import { isRTLString, Position, Rotation } from '../../../../utils/common'; @@ -26,7 +27,6 @@ import { AxisId } from '../../../../utils/ids'; import { multilayerAxisEntry } from '../../axes/timeslip/multilayer_ticks'; import { isHorizontalAxis, isVerticalAxis } from '../../utils/axis_type_utils'; import { AxisTick, TextDirection, TickLabelBounds } from '../../utils/axis_utils'; -import { getPanelSize } from '../../utils/panel'; import { computeXScale } from '../../utils/scales'; import { SeriesDomainsAndData } from '../utils/types'; diff --git a/packages/charts/src/chart_types/xy_chart/state/utils/types.ts b/packages/charts/src/chart_types/xy_chart/state/utils/types.ts index 23bb6eb6f3..89a18eb8d5 100644 --- a/packages/charts/src/chart_types/xy_chart/state/utils/types.ts +++ b/packages/charts/src/chart_types/xy_chart/state/utils/types.ts @@ -6,8 +6,8 @@ * Side Public License, v 1. */ +import { SmallMultiplesSeriesDomains } from '../../../../common/panel_utils'; import { ScaleBand, ScaleContinuous } from '../../../../scales'; -import { OrdinalDomain } from '../../../../utils/domain'; import { PointGeometry, BarGeometry, @@ -64,11 +64,9 @@ export interface ComputedGeometries { } /** @internal */ -export interface SeriesDomainsAndData { +export interface SeriesDomainsAndData extends SmallMultiplesSeriesDomains { xDomain: XDomain; yDomains: YDomain[]; - smVDomain: OrdinalDomain; - smHDomain: OrdinalDomain; formattedDataSeries: DataSeries[]; } diff --git a/packages/charts/src/chart_types/xy_chart/state/utils/utils.ts b/packages/charts/src/chart_types/xy_chart/state/utils/utils.ts index 9be4742257..1ec4dce9e0 100644 --- a/packages/charts/src/chart_types/xy_chart/state/utils/utils.ts +++ b/packages/charts/src/chart_types/xy_chart/state/utils/utils.ts @@ -10,6 +10,7 @@ import { isHorizontalRotation } from './common'; import { getAxesSpecForSpecId, getSpecDomainGroupId, getSpecsById } from './spec'; import { ComputedGeometries, GeometriesCounts, SeriesDomainsAndData, Transform } from './types'; import { Color } from '../../../../common/colors'; +import { SmallMultipleScales, SmallMultiplesGroupBy } from '../../../../common/panel_utils'; import { getPredicateFn, Predicate } from '../../../../common/predicate'; import { SeriesIdentifier, SeriesKey } from '../../../../common/series_id'; import { ScaleBand, ScaleContinuous } from '../../../../scales'; @@ -63,9 +64,7 @@ import { isLineAnnotation, isLineSeriesSpec, } from '../../utils/specs'; -import { SmallMultipleScales } from '../selectors/compute_small_multiple_scales'; import { ScaleConfigs } from '../selectors/get_api_scale_configs'; -import { SmallMultiplesGroupBy } from '../selectors/get_specs'; /** * Return map association between `seriesKey` and only the custom colors string diff --git a/packages/charts/src/chart_types/xy_chart/utils/axis_utils.test.ts b/packages/charts/src/chart_types/xy_chart/utils/axis_utils.test.ts index 6824bebf6d..586883b959 100644 --- a/packages/charts/src/chart_types/xy_chart/utils/axis_utils.test.ts +++ b/packages/charts/src/chart_types/xy_chart/utils/axis_utils.test.ts @@ -20,10 +20,12 @@ import { } from './axis_utils'; import { computeXScale } from './scales'; import { AxisSpec, DomainRange, DEFAULT_GLOBAL_ID, TickFormatter } from './specs'; +import { SmallMultipleScales } from '../../../common/panel_utils'; import { MockGlobalSpec /*, MockSeriesSpec*/ } from '../../../mocks/specs/specs'; // import { MockStore } from '../../../mocks/store/store'; import { MockXDomain, MockYDomain } from '../../../mocks/xy/domains'; import { ScaleType } from '../../../scales/constants'; +import { getScale } from '../../../state/selectors/compute_small_multiple_scales'; import { Position, mergePartial, HorizontalAlignment, VerticalAlignment } from '../../../utils/common'; import { niceTimeFormatter } from '../../../utils/data/formatters'; import { OrdinalDomain } from '../../../utils/domain'; @@ -37,7 +39,6 @@ import { computeAxisTicksDimensionsSelector, } from '../state/selectors/compute_axis_ticks_dimensions'; */ -import { getScale, SmallMultipleScales } from '../state/selectors/compute_small_multiple_scales'; // import { getAxesStylesSelector } from '../state/selectors/get_axis_styles'; // import { getGridLinesSelector } from '../state/selectors/get_grid_lines'; import { mergeYCustomDomainsByGroupId } from '../state/selectors/merge_y_custom_domains'; diff --git a/packages/charts/src/chart_types/xy_chart/utils/axis_utils.ts b/packages/charts/src/chart_types/xy_chart/utils/axis_utils.ts index 70183d39b5..81ca607330 100644 --- a/packages/charts/src/chart_types/xy_chart/utils/axis_utils.ts +++ b/packages/charts/src/chart_types/xy_chart/utils/axis_utils.ts @@ -7,8 +7,8 @@ */ import { isHorizontalAxis, isVerticalAxis } from './axis_type_utils'; -import { getPanelSize, hasSMDomain } from './panel'; import { computeXScale, computeYScales } from './scales'; +import { SmallMultipleScales, hasSMDomain, getPanelSize } from '../../../common/panel_utils'; import { ScaleBand, ScaleContinuous } from '../../../scales'; import { AxisSpec, SettingsSpec } from '../../../specs'; import { @@ -25,7 +25,6 @@ import { AxisId } from '../../../utils/ids'; import { Point } from '../../../utils/point'; import { AxisStyle, TextAlignment, TextOffset, Theme } from '../../../utils/themes/theme'; import { MIN_STROKE_WIDTH } from '../renderer/canvas/primitives/line'; -import { SmallMultipleScales } from '../state/selectors/compute_small_multiple_scales'; import { Projection } from '../state/selectors/visible_ticks'; import { SeriesDomainsAndData } from '../state/utils/types'; @@ -318,8 +317,6 @@ export function getAxesGeometries( axisSpecs: Map, axesStyles: Map, smScales: SmallMultipleScales, - totalGroupsCount: number, - enableHistogramMode: boolean, visibleTicksSet: Map, ): AxisGeometry[] { const panel = getPanelSize(smScales); @@ -329,8 +326,15 @@ export function getAxesGeometries( if (axisSpec) { const vertical = isVerticalAxis(axisSpec.position); const axisStyle = axesStyles.get(axisId) ?? sharedAxesStyle; - const axisPositionData = getPosition(chartDims, chartMargins, axisStyle, axisSpec, labelBox, smScales, acc); - const { dimensions, topIncrement, bottomIncrement, leftIncrement, rightIncrement } = axisPositionData; + const { dimensions, topIncrement, bottomIncrement, leftIncrement, rightIncrement } = getPosition( + chartDims, + chartMargins, + axisStyle, + axisSpec, + labelBox, + smScales, + acc, + ); acc.top += topIncrement; acc.bottom += bottomIncrement; acc.left += leftIncrement; diff --git a/packages/charts/src/chart_types/xy_chart/utils/dimensions.ts b/packages/charts/src/chart_types/xy_chart/utils/dimensions.ts index 1d648f8a05..5be8789aae 100644 --- a/packages/charts/src/chart_types/xy_chart/utils/dimensions.ts +++ b/packages/charts/src/chart_types/xy_chart/utils/dimensions.ts @@ -38,7 +38,7 @@ export interface ChartDimensions { axisTickDimensions: AxesTicksDimensions, axesStyles: Map, axisSpecs: AxisSpec[], - [smSpec]: SmallMultiplesSpec[] = [], + smSpec?: SmallMultiplesSpec, ): ChartDimensions { const axesDimensions = getAxesDimensions(theme, axisTickDimensions, axesStyles, axisSpecs, smSpec); const chartWidth = parentDimensions.width - axesDimensions.left - axesDimensions.right; diff --git a/packages/charts/src/chart_types/xy_chart/utils/grid_lines.ts b/packages/charts/src/chart_types/xy_chart/utils/grid_lines.ts index b22c3ba227..020bc9eb5b 100644 --- a/packages/charts/src/chart_types/xy_chart/utils/grid_lines.ts +++ b/packages/charts/src/chart_types/xy_chart/utils/grid_lines.ts @@ -8,10 +8,9 @@ import { isVerticalAxis } from './axis_type_utils'; import { AxisGeometry, AxisTick } from './axis_utils'; -import { getPanelSize } from './panel'; -import { getPerPanelMap } from './panel_utils'; import { AxisSpec } from './specs'; import { colorToRgba, overrideOpacity } from '../../../common/color_library_wrappers'; +import { SmallMultipleScales, getPanelSize, getPerPanelMap } from '../../../common/panel_utils'; import { Line, Stroke } from '../../../geoms/types'; import { mergePartial, RecursivePartial } from '../../../utils/common'; import { Size } from '../../../utils/dimensions'; @@ -19,7 +18,6 @@ import { AxisId } from '../../../utils/ids'; import { Point } from '../../../utils/point'; import { AxisStyle, Theme } from '../../../utils/themes/theme'; import { MIN_STROKE_WIDTH } from '../renderer/canvas/primitives/line'; -import { SmallMultipleScales } from '../state/selectors/compute_small_multiple_scales'; /** @internal */ export const HIERARCHICAL_GRID_WIDTH = 1; // constant 1 scales well and solves some render issues due to fixed 1px wide overpaints diff --git a/packages/charts/src/chart_types/xy_chart/utils/panel.ts b/packages/charts/src/chart_types/xy_chart/utils/panel.ts deleted file mode 100644 index b4587bb919..0000000000 --- a/packages/charts/src/chart_types/xy_chart/utils/panel.ts +++ /dev/null @@ -1,19 +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 { Size } from '../../../utils/dimensions'; -import { SmallMultipleScales } from '../state/selectors/compute_small_multiple_scales'; - -/** @internal */ -export function getPanelSize({ horizontal, vertical }: SmallMultipleScales): Size { - return { width: horizontal.bandwidth, height: vertical.bandwidth }; -} - -/** @internal */ -export const hasSMDomain = ({ domain }: SmallMultipleScales['horizontal'] | SmallMultipleScales['vertical']) => - domain.length > 0 && domain[0] !== undefined; diff --git a/packages/charts/src/chart_types/xy_chart/utils/panel_utils.ts b/packages/charts/src/chart_types/xy_chart/utils/panel_utils.ts deleted file mode 100644 index 085c9e4079..0000000000 --- a/packages/charts/src/chart_types/xy_chart/utils/panel_utils.ts +++ /dev/null @@ -1,40 +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 { Point } from '../../../utils/point'; -import { SmallMultipleScales } from '../state/selectors/compute_small_multiple_scales'; - -type Value = string | number; - -/** @internal */ -export interface PerPanelMap { - panelAnchor: Point; - horizontalValue: Value; - verticalValue: Value; -} - -/** @internal */ -export function getPerPanelMap( - scales: SmallMultipleScales, - fn: (anchor: Point, horizontalValue: Value, verticalValue: Value, smScales: SmallMultipleScales) => T | null, -): Array { - const { horizontal, vertical } = scales; - return vertical.domain.reduce>((acc, verticalValue) => { - return [ - ...acc, - ...horizontal.domain.reduce>((hAcc, horizontalValue) => { - const panelAnchor: Point = { - x: horizontal.scale(horizontalValue) || 0, - y: vertical.scale(verticalValue) || 0, - }; - const fnObj = fn(panelAnchor, horizontalValue, verticalValue, scales); - return fnObj ? [...hAcc, { panelAnchor, horizontalValue, verticalValue, ...fnObj }] : hAcc; - }, []), - ]; - }, []); -} diff --git a/packages/charts/src/chart_types/xy_chart/utils/series.ts b/packages/charts/src/chart_types/xy_chart/utils/series.ts index ae6f98ba79..ef038de74a 100644 --- a/packages/charts/src/chart_types/xy_chart/utils/series.ts +++ b/packages/charts/src/chart_types/xy_chart/utils/series.ts @@ -11,6 +11,7 @@ import { groupBy } from './group_data_series'; import { BaseDatum, BasicSeriesSpec, SeriesNameConfigOptions, SeriesSpecs, SeriesType, StackMode } from './specs'; import { datumXSortPredicate, formatStackedDataSeriesValues } from './stacked_series_utils'; import { Color } from '../../../common/colors'; +import { SmallMultiplesDatum, SmallMultiplesGroupBy } from '../../../common/panel_utils'; import { SeriesIdentifier, SeriesKey } from '../../../common/series_id'; import { ScaleType } from '../../../scales/constants'; import { BinAgg, Direction, XScaleType } from '../../../specs'; @@ -23,7 +24,6 @@ import { Logger } from '../../../utils/logger'; import { ColorConfig } from '../../../utils/themes/theme'; import { groupSeriesByYGroup, isHistogramEnabled, isStackedSpec } from '../domains/y_domain'; import { X_SCALE_DEFAULT } from '../scales/scale_defaults'; -import { SmallMultiplesGroupBy } from '../state/selectors/get_specs'; /** @internal */ export const SERIES_DELIMITER = ' - '; @@ -59,12 +59,10 @@ export interface DataSeriesDatum { } /** @public */ -export interface XYChartSeriesIdentifier extends SeriesIdentifier { +export interface XYChartSeriesIdentifier extends SeriesIdentifier, SmallMultiplesDatum { xAccessor: Accessor; yAccessor: Accessor; splitAccessors: Map; // does the map have a size vs making it optional - smVerticalAccessorValue?: string | number; - smHorizontalAccessorValue?: string | number; seriesKeys: (string | number)[]; } diff --git a/packages/charts/src/common/panel_utils.ts b/packages/charts/src/common/panel_utils.ts new file mode 100644 index 0000000000..0fff18aadd --- /dev/null +++ b/packages/charts/src/common/panel_utils.ts @@ -0,0 +1,133 @@ +/* + * 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 { PrimitiveValue } from '../chart_types/partition_chart/layout/utils/group_by_rollup'; +import { ScaleBand } from '../scales'; +import { GroupBySpec } from '../specs/group_by'; +import { safeFormat } from '../utils/common'; +import { Dimensions, Size } from '../utils/dimensions'; +import { OrdinalDomain } from '../utils/domain'; +import { Point } from '../utils/point'; + +type Value = string | number; + +/** @internal */ +export interface PerPanelMap { + panelAnchor: Point; + horizontalValue: NonNullable; + verticalValue: NonNullable; +} + +/** @internal */ +export interface SmallMultipleScales { + horizontal: ScaleBand; + vertical: ScaleBand; +} + +/** @internal */ +export interface SmallMultiplesGroupBy { + vertical?: GroupBySpec; + horizontal?: GroupBySpec; +} + +/** @internal */ +export interface SmallMultiplesSeriesDomains { + smVDomain: OrdinalDomain; + smHDomain: OrdinalDomain; +} + +/** @public */ +export interface SmallMultiplesDatum { + smHorizontalAccessorValue?: NonNullable; + smVerticalAccessorValue?: NonNullable; +} + +/** @internal */ +export function getPerPanelMap( + scales: SmallMultipleScales, + fn: (anchor: Point, horizontalValue: Value, verticalValue: Value, smScales: SmallMultipleScales) => T | null, +): Array { + const { horizontal, vertical } = scales; + return vertical.domain.reduce>((acc, verticalValue) => { + return [ + ...acc, + ...horizontal.domain.reduce>((hAcc, horizontalValue) => { + const panelAnchor: Point = { + x: horizontal.scale(horizontalValue) || 0, + y: vertical.scale(verticalValue) || 0, + }; + const fnReturn = fn(panelAnchor, horizontalValue, verticalValue, scales); + return fnReturn ? [...hAcc, { panelAnchor, horizontalValue, verticalValue, ...fnReturn }] : hAcc; + }, []), + ]; + }, []); +} + +/** @internal */ +export function getPanelSize({ horizontal, vertical }: SmallMultipleScales): Size { + return { width: horizontal.bandwidth, height: vertical.bandwidth }; +} + +/** + * returns true for scales with empty input domains + * TODO: Cleanup See https://github.com/elastic/elastic-charts/issues/1990 + * @internal + */ +export const hasSMDomain = ({ domain }: SmallMultipleScales['horizontal'] | SmallMultipleScales['vertical']) => + domain.length > 0 && domain[0] !== undefined; + +/** @internal */ +export const getPanelTitle = ( + isVertical: boolean, + verticalValue: NonNullable, + horizontalValue: NonNullable, + groupBy?: SmallMultiplesGroupBy, +): string => { + return isVertical + ? safeFormat(`${verticalValue}`, groupBy?.vertical?.format) + : safeFormat(`${horizontalValue}`, groupBy?.horizontal?.format); +}; + +/** + * Returns true if pointer is within a panel inside the chart area, otherwise false + * Returns false when pointer is in the padding gutter and axes + * @internal + */ +export const isPointerOverPanelFn = + (smScales: SmallMultipleScales, chartDimensions: Dimensions, gridStroke: number) => + (pointer: Point): boolean => { + return ( + isPointerInsideChart(chartDimensions)(pointer) && + isPointerInBandwidth(smScales.horizontal, pointer.x - chartDimensions.left, gridStroke) && + isPointerInBandwidth(smScales.vertical, pointer.y - chartDimensions.top, gridStroke) + ); + }; + +function isPointerInBandwidth( + scale: SmallMultipleScales['horizontal'] | SmallMultipleScales['vertical'], + dimension: number, + gridStroke: number, +): boolean { + const { bandwidth, innerPadding } = scale; + const padding = innerPadding * bandwidth; + const divisor = bandwidth + padding + gridStroke * 2; + const vDiv = Math.floor(dimension / divisor); + const lower = vDiv * divisor; + const upper = lower + bandwidth + gridStroke * 2; + + return dimension > lower && dimension <= upper; +} + +/** + * Returns true if the pointer is within the chart dimensions, false otherwise + * @internal + */ +export const isPointerInsideChart = + ({ left, top, height, width }: Dimensions) => + ({ x, y }: Point): boolean => + x > left && x < left + width && y > top && y < top + height; diff --git a/packages/charts/src/index.ts b/packages/charts/src/index.ts index b931372104..f6e5d543f1 100644 --- a/packages/charts/src/index.ts +++ b/packages/charts/src/index.ts @@ -91,7 +91,13 @@ export { AnimKeyframe } from './chart_types/partition_chart/layout/types/config_ // heatmap export { Cell } from './chart_types/heatmap/layout/types/viewmodel_types'; -export { ColorBand, HeatmapBandsColorScale, HeatmapProps } from './chart_types/heatmap/specs/heatmap'; +export { HeatmapCellDatum } from './chart_types/heatmap/layout/viewmodel/viewmodel'; +export { + ColorBand, + HeatmapBandsColorScale, + HeatmapProps, + HeatmapHighlightedData, +} from './chart_types/heatmap/specs/heatmap'; // utilities export { @@ -123,6 +129,7 @@ export { FontStyle, FONT_STYLES } from './common/text_utils'; export { Color } from './common/colors'; export { RGB, A, RgbaTuple } from './common/color_library_wrappers'; export { Predicate } from './common/predicate'; +export { SmallMultiplesDatum } from './common/panel_utils'; export type { ESCalendarInterval, diff --git a/packages/charts/src/mocks/utils.ts b/packages/charts/src/mocks/utils.ts index cfa59c9a40..aca70385f0 100644 --- a/packages/charts/src/mocks/utils.ts +++ b/packages/charts/src/mocks/utils.ts @@ -56,8 +56,8 @@ export const getRandomNumberGenerator = (seed = getRNGSeed()): RandomNumberGener /** @internal */ export class SeededDataGenerator extends DataGenerator { - constructor(frequency = 500) { - super(frequency, getRandomNumberGenerator()); + constructor(frequency = 500, seed?: string) { + super(frequency, getRandomNumberGenerator(seed)); } } diff --git a/packages/charts/src/specs/settings.tsx b/packages/charts/src/specs/settings.tsx index c561635e2c..db4168897b 100644 --- a/packages/charts/src/specs/settings.tsx +++ b/packages/charts/src/specs/settings.tsx @@ -19,6 +19,7 @@ import { WordModel } from '../chart_types/wordcloud/layout/types/viewmodel_types import { XYChartSeriesIdentifier } from '../chart_types/xy_chart/utils/series'; import { CategoryLabel } from '../common/category'; import { Color } from '../common/colors'; +import { SmallMultiplesDatum } from '../common/panel_utils'; import { SeriesIdentifier } from '../common/series_id'; import { TooltipPortalSettings } from '../components'; import { ScaleContinuousType, ScaleOrdinalType } from '../scales'; @@ -191,11 +192,11 @@ export type BrushEndListener = (brushAreaEvent: BrushEvent) => void; export type ProjectionAreaChangeListener = (areas: { projection: Dimensions; parent: Dimensions }) => void; /** @public */ -export type HeatmapBrushEvent = { +export interface HeatmapBrushEvent extends SmallMultiplesDatum { cells: Cell[]; x: (string | number)[]; y: (string | number)[]; -}; +} /** @public */ export type LegendItemListener = (series: SeriesIdentifier[]) => void; diff --git a/packages/charts/src/state/chart_state.ts b/packages/charts/src/state/chart_state.ts index ee0364c363..9cce41742c 100644 --- a/packages/charts/src/state/chart_state.ts +++ b/packages/charts/src/state/chart_state.ts @@ -35,6 +35,7 @@ import { XYAxisChartState } from '../chart_types/xy_chart/state/chart_state'; import { CategoryKey } from '../common/category'; import { Color } from '../common/colors'; import { LegendItem, LegendItemExtraValues } from '../common/legend'; +import { SmallMultiplesSeriesDomains } from '../common/panel_utils'; import { SeriesIdentifier, SeriesKey } from '../common/series_id'; import { AnchorPosition } from '../components/portal/types'; import { TooltipInfo } from '../components/tooltip/types'; @@ -160,6 +161,11 @@ export interface InternalChartState { * Get the series types for the screen reader summary component */ getChartTypeDescription(globalState: GlobalChartState): string; + + /** + * Get the domain of the vertical and horizontal small multiple grids + */ + getSmallMultiplesDomains(globalState: GlobalChartState): SmallMultiplesSeriesDomains; } /** @internal */ diff --git a/packages/charts/src/chart_types/xy_chart/state/selectors/compute_panels.ts b/packages/charts/src/state/selectors/compute_panels.ts similarity index 75% rename from packages/charts/src/chart_types/xy_chart/state/selectors/compute_panels.ts rename to packages/charts/src/state/selectors/compute_panels.ts index c7b8153ad3..a975e6dab4 100644 --- a/packages/charts/src/chart_types/xy_chart/state/selectors/compute_panels.ts +++ b/packages/charts/src/state/selectors/compute_panels.ts @@ -7,10 +7,9 @@ */ import { computeSmallMultipleScalesSelector } from './compute_small_multiple_scales'; -import { createCustomCachedSelector } from '../../../../state/create_selector'; -import { Size } from '../../../../utils/dimensions'; -import { getPanelSize } from '../../utils/panel'; -import { PerPanelMap, getPerPanelMap } from '../../utils/panel_utils'; +import { getPanelSize, getPerPanelMap, PerPanelMap } from '../../common/panel_utils'; +import { Size } from '../../utils/dimensions'; +import { createCustomCachedSelector } from '../create_selector'; /** @internal */ export type PanelGeoms = Array; diff --git a/packages/charts/src/state/selectors/compute_small_multiple_scales.ts b/packages/charts/src/state/selectors/compute_small_multiple_scales.ts new file mode 100644 index 0000000000..a8efd723b5 --- /dev/null +++ b/packages/charts/src/state/selectors/compute_small_multiple_scales.ts @@ -0,0 +1,42 @@ +/* + * 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 { getInternalMainProjectionAreaSelector } from './get_internal_main_projection_area'; +import { getInternalSmallMultiplesDomains } from './get_internal_sm_domains'; +import { getSmallMultiplesSpec } from './get_small_multiples_spec'; +import { SmallMultipleScales } from '../../common/panel_utils'; +import { ScaleBand } from '../../scales'; +import { RelativeBandsPadding, DEFAULT_SM_PANEL_PADDING } from '../../specs'; +import { OrdinalDomain } from '../../utils/domain'; +import { createCustomCachedSelector } from '../create_selector'; + +/** + * Return the small multiple scales for horizontal and vertical grids + * @internal + */ +export const computeSmallMultipleScalesSelector = createCustomCachedSelector( + [getInternalSmallMultiplesDomains, getInternalMainProjectionAreaSelector, getSmallMultiplesSpec], + ({ smHDomain, smVDomain }, { width, height }, smSpec): SmallMultipleScales => { + return { + horizontal: getScale(smHDomain, width, smSpec?.style?.horizontalPanelPadding), + vertical: getScale(smVDomain, height, smSpec?.style?.verticalPanelPadding), + }; + }, +); + +/** + * @internal + */ +export function getScale( + domain: OrdinalDomain, + maxRange: number, + padding: RelativeBandsPadding = DEFAULT_SM_PANEL_PADDING, +): ScaleBand { + const singlePanelSmallMultiple = domain.length <= 1; + return new ScaleBand(domain, [0, maxRange], undefined, singlePanelSmallMultiple ? 0 : padding); +} diff --git a/packages/charts/src/state/selectors/get_internal_sm_domains.ts b/packages/charts/src/state/selectors/get_internal_sm_domains.ts new file mode 100644 index 0000000000..8c8d7dee12 --- /dev/null +++ b/packages/charts/src/state/selectors/get_internal_sm_domains.ts @@ -0,0 +1,21 @@ +/* + * 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 { SmallMultiplesSeriesDomains } from '../../common/panel_utils'; +import { GlobalChartState } from '../chart_state'; + +/** @internal */ +export const getInternalSmallMultiplesDomains = (state: GlobalChartState): SmallMultiplesSeriesDomains => { + if (state.internalChartState) { + return state.internalChartState.getSmallMultiplesDomains(state); + } + return { + smHDomain: [], + smVDomain: [], + }; +}; diff --git a/packages/charts/src/state/selectors/get_small_multiples_index_order.ts b/packages/charts/src/state/selectors/get_small_multiples_index_order.ts new file mode 100644 index 0000000000..6602f653a0 --- /dev/null +++ b/packages/charts/src/state/selectors/get_small_multiples_index_order.ts @@ -0,0 +1,28 @@ +/* + * 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 { getSpecs } from './get_specs'; +import { ChartType } from '../../chart_types'; +import { SmallMultiplesGroupBy } from '../../common/panel_utils'; +import { SmallMultiplesSpec, SpecType, GroupBySpec } from '../../specs'; +import { createCustomCachedSelector } from '../create_selector'; +import { getSpecsFromStore } from '../utils'; + +/** @internal */ +export const getSmallMultiplesIndexOrderSelector = createCustomCachedSelector( + [getSpecs], + (specs): SmallMultiplesGroupBy => { + const [smallMultiples] = getSpecsFromStore(specs, ChartType.Global, SpecType.SmallMultiples); + const groupBySpecs = getSpecsFromStore(specs, ChartType.Global, SpecType.IndexOrder); + + return { + horizontal: groupBySpecs.find((s) => s.id === smallMultiples?.splitHorizontally), + vertical: groupBySpecs.find((s) => s.id === smallMultiples?.splitVertically), + }; + }, +); diff --git a/packages/charts/src/state/selectors/get_small_multiples_spec.ts b/packages/charts/src/state/selectors/get_small_multiples_spec.ts index 124dd7ee25..944d4f836b 100644 --- a/packages/charts/src/state/selectors/get_small_multiples_spec.ts +++ b/packages/charts/src/state/selectors/get_small_multiples_spec.ts @@ -11,7 +11,7 @@ import { ChartType } from '../../chart_types'; import { SpecType } from '../../specs/constants'; import { SmallMultiplesSpec } from '../../specs/small_multiples'; import { createCustomCachedSelector } from '../create_selector'; -import { getSpecsFromStore } from '../utils'; +import { getSpecsFromStore, getSpecFromStore } from '../utils'; /** * Return the small multiple specs @@ -25,6 +25,6 @@ export const getSmallMultiplesSpecs = createCustomCachedSelector([getSpecs], (sp * Return the small multiple spec * @internal */ -export const getSmallMultiplesSpec = createCustomCachedSelector([getSmallMultiplesSpecs], (smallMultiples) => - smallMultiples.length === 1 ? smallMultiples : undefined, +export const getSmallMultiplesSpec = createCustomCachedSelector([getSpecs], (specs) => + getSpecFromStore(specs, ChartType.Global, SpecType.SmallMultiples), ); diff --git a/packages/charts/src/state/selectors/is_external_tooltip_visible.ts b/packages/charts/src/state/selectors/is_external_tooltip_visible.ts index cfbcc2ba17..6f186659cc 100644 --- a/packages/charts/src/state/selectors/is_external_tooltip_visible.ts +++ b/packages/charts/src/state/selectors/is_external_tooltip_visible.ts @@ -6,9 +6,9 @@ * Side Public License, v 1. */ +import { getInternalMainProjectionAreaSelector } from './get_internal_main_projection_area'; import { getSettingsSpecSelector } from './get_settings_spec'; import { hasExternalEventSelector } from './has_external_pointer_event'; -import { computeChartDimensionsSelector } from '../../chart_types/xy_chart/state/selectors/compute_chart_dimensions'; import { getComputedScalesSelector } from '../../chart_types/xy_chart/state/selectors/get_computed_scales'; import { PointerEventType } from '../../specs'; import { isNil } from '../../utils/common'; @@ -24,9 +24,9 @@ export const isExternalTooltipVisibleSelector = createCustomCachedSelector( hasExternalEventSelector, getExternalEventPointer, getComputedScalesSelector, - computeChartDimensionsSelector, + getInternalMainProjectionAreaSelector, ], - ({ externalPointerEvents }, hasExternalEvent, pointer, { xScale }, { chartDimensions }): boolean => { + ({ externalPointerEvents }, hasExternalEvent, pointer, { xScale }, chartDimensions): boolean => { if ( !pointer || pointer.type !== PointerEventType.Over || diff --git a/packages/charts/src/state/types.ts b/packages/charts/src/state/types.ts index d815f9fdb1..66c90ef46a 100644 --- a/packages/charts/src/state/types.ts +++ b/packages/charts/src/state/types.ts @@ -82,7 +82,7 @@ export type DebugStateBar = DebugStateBase & { labels: any[]; }; -type CellDebug = Pick & { fill: string }; +type CellDebug = Pick & { fill: string }; type HeatmapDebugState = { cells: CellDebug[]; diff --git a/packages/charts/src/state/utils.ts b/packages/charts/src/state/utils.ts index 16c6d6b184..ab16cea167 100644 --- a/packages/charts/src/state/utils.ts +++ b/packages/charts/src/state/utils.ts @@ -10,11 +10,28 @@ import { PointerState, PointerStates, SpecList, TooltipInteractionState } from ' import { ChartType } from '../chart_types'; import { Spec } from '../specs'; -/** @internal */ +/** + * Returns all matching specs + * @internal + */ export function getSpecsFromStore(specs: SpecList, chartType: ChartType, specType: string): U[] { return Object.values(specs).filter((spec) => spec.chartType === chartType && spec.specType === specType) as U[]; } +/** + * Returns first matching spec + * @internal + */ +export function getSpecFromStore( + specs: SpecList, + chartType: ChartType, + specType: string, +): U | undefined { + return ( + (Object.values(specs).find((spec) => spec.chartType === chartType && spec.specType === specType) as U) ?? undefined + ); +} + /** @internal */ export function isClicking(prevClick: PointerState | null, lastClick: PointerState | null) { return lastClick && (!prevClick || prevClick.time !== lastClick.time); diff --git a/packages/charts/src/utils/common.tsx b/packages/charts/src/utils/common.tsx index 4e66054354..06c39073ff 100644 --- a/packages/charts/src/utils/common.tsx +++ b/packages/charts/src/utils/common.tsx @@ -135,7 +135,7 @@ export function compareByValueAsc(a: number | string, b: number | string): numbe } /** @internal */ -export function clamp(value: number, lowerBound: number, upperBound: number) { +export function clamp(value: number, lowerBound: number, upperBound: number): number { return Math.min(Math.max(value, lowerBound), upperBound); } diff --git a/packages/charts/src/utils/data_generators/data_generator.ts b/packages/charts/src/utils/data_generators/data_generator.ts index e8021f3e42..9aa1b0a221 100644 --- a/packages/charts/src/utils/data_generators/data_generator.ts +++ b/packages/charts/src/utils/data_generators/data_generator.ts @@ -26,6 +26,8 @@ function defaultRNG(min = 0, max = 1, fractionDigits = 0, inclusive = true) { return num / precision; } +const fillGroups = (n: number) => new Array(Math.max(n, 1)).fill(0).map((_, i) => String.fromCharCode(97 + i)); + /** @public */ export class DataGenerator { private randomNumberGenerator: RandomNumberGenerator; @@ -59,9 +61,7 @@ export class DataGenerator { } generateGroupedSeries(totalPoints = 50, totalGroups = 2, groupPrefix = '') { - const groups = new Array(totalGroups) - .fill(0) - .map((group, i) => this.generateSimpleSeries(totalPoints, i, groupPrefix)); + const groups = new Array(totalGroups).fill(0).map((_, i) => this.generateSimpleSeries(totalPoints, i, groupPrefix)); return groups.reduce((acc, curr) => [...acc, ...curr]); } @@ -77,9 +77,29 @@ export class DataGenerator { } generateRandomGroupedSeries(totalPoints = 50, totalGroups = 2, groupPrefix = '') { - const groups = new Array(totalGroups) - .fill(0) - .map((group, i) => this.generateRandomSeries(totalPoints, i, groupPrefix)); + const groups = new Array(totalGroups).fill(0).map((_, i) => this.generateRandomSeries(totalPoints, i, groupPrefix)); return groups.reduce((acc, curr) => [...acc, ...curr]); } + + /** + * Generate data given a list or number of vertical and/or horizontal panels + */ + generateSMGroupedSeries>( + verticalGroups: Array | number, + horizontalGroups: Array | number, + seriesGenerator: (h: string | number, v: string | number) => T[], + ) { + const vGroups = typeof verticalGroups === 'number' ? fillGroups(verticalGroups) : verticalGroups; + const hGroups = typeof horizontalGroups === 'number' ? fillGroups(horizontalGroups) : horizontalGroups; + + return vGroups.flatMap((v) => { + return hGroups.flatMap((h) => { + return seriesGenerator(h, v).map((row) => ({ + h, + v, + ...row, + })); + }); + }); + } } diff --git a/packages/charts/src/utils/themes/dark_theme.ts b/packages/charts/src/utils/themes/dark_theme.ts index f9d1c6d0cc..e18d51c727 100644 --- a/packages/charts/src/utils/themes/dark_theme.ts +++ b/packages/charts/src/utils/themes/dark_theme.ts @@ -321,8 +321,6 @@ export const DARK_THEME: Theme = { sectorLineStroke: 'black', }, heatmap: { - maxRowHeight: 30, - maxColumnWidth: 30, brushArea: { visible: true, stroke: '#D3DAE6', // euiColorLightShade, @@ -360,14 +358,6 @@ export const DARK_THEME: Theme = { padding: { top: 5, bottom: 5, left: 5, right: 5 }, }, grid: { - cellWidth: { - min: 0, - max: 30, - }, - cellHeight: { - min: 12, - max: 30, - }, stroke: { width: 1, color: 'snow', diff --git a/packages/charts/src/utils/themes/light_theme.ts b/packages/charts/src/utils/themes/light_theme.ts index 4132fa373f..a873d21140 100644 --- a/packages/charts/src/utils/themes/light_theme.ts +++ b/packages/charts/src/utils/themes/light_theme.ts @@ -321,8 +321,6 @@ export const LIGHT_THEME: Theme = { sectorLineStroke: 'white', }, heatmap: { - maxRowHeight: 30, - maxColumnWidth: 30, brushArea: { visible: true, stroke: '#69707D', // euiColorDarkShade, @@ -359,14 +357,6 @@ export const LIGHT_THEME: Theme = { padding: { top: 5, bottom: 5, left: 5, right: 5 }, }, grid: { - cellWidth: { - min: 0, - max: 30, - }, - cellHeight: { - min: 12, - max: 30, - }, stroke: { width: 1, color: 'gray', diff --git a/packages/charts/src/utils/themes/theme.ts b/packages/charts/src/utils/themes/theme.ts index 2878b46bd0..1bdacf51fa 100644 --- a/packages/charts/src/utils/themes/theme.ts +++ b/packages/charts/src/utils/themes/theme.ts @@ -248,8 +248,6 @@ export interface GoalStyles { * @public */ export interface HeatmapStyle { - maxRowHeight: Pixels; - maxColumnWidth: Pixels; /** * Config of the mask over the area outside of the selected cells */ @@ -283,14 +281,6 @@ export interface HeatmapStyle { padding: Pixels | Padding; }; grid: { - cellWidth: { - min: Pixels; - max: Pixels | 'fill'; - }; - cellHeight: { - min: Pixels; - max: Pixels | 'fill'; - }; stroke: { color: string; width: number; diff --git a/storybook/stories/heatmap/1_basic.story.tsx b/storybook/stories/heatmap/1_basic.story.tsx index 691c2324a5..b43348c3ea 100644 --- a/storybook/stories/heatmap/1_basic.story.tsx +++ b/storybook/stories/heatmap/1_basic.story.tsx @@ -7,15 +7,12 @@ */ import { action } from '@storybook/addon-actions'; -import { boolean, button } from '@storybook/addon-knobs'; -import React, { useCallback, useMemo, useState } from 'react'; +import { boolean, number } from '@storybook/addon-knobs'; +import React, { useMemo } from 'react'; import { Chart, - ElementClickListener, Heatmap, - HeatmapBrushEvent, - HeatmapElementEvent, HeatmapStyle, niceTimeFormatter, PointerEvent, @@ -27,33 +24,26 @@ import { import { DATA_6 } from '../../../packages/charts/src/utils/data_samples/test_dataset_heatmap'; import { useBaseTheme } from '../../use_base_theme'; import { getDebugStateLogger } from '../utils/debug_state_logger'; +import { useHeatmapSelection } from '../utils/use_heatmap_selection'; export const Example = () => { - const [selection, setSelection] = useState<{ x: (string | number)[]; y: (string | number)[] } | undefined>(); + const { highlightedData, onElementClick, onBrushEnd } = useHeatmapSelection(); - const persistCellsSelection = boolean('Persist cells selection', true); const debugState = boolean('Enable debug state', true); const showXAxisTitle = boolean('Show x axis title', false); const showYAxisTitle = boolean('Show y axis title', false); + const showBrushTool = boolean('Show pointer brush area', true); + const pointerUpdate = (event: PointerEvent) => { action('onPointerUpdate')(event); }; - const handler = useCallback(() => { - setSelection(undefined); - }, []); - - button('Clear cells selection', handler); - const heatmap = useMemo(() => { const styles: RecursivePartial = { brushTool: { - visible: true, + visible: showBrushTool, }, grid: { - cellHeight: { - min: 20, - }, stroke: { width: 0.5, color: '#bababa', @@ -78,14 +68,7 @@ export const Example = () => { }; return styles; - }, []); - - const onElementClick: ElementClickListener = useCallback((e) => { - const cell = (e as HeatmapElementEvent[])[0][0]; - setSelection({ x: [cell.datum.x, cell.datum.x], y: [cell.datum.y] }); - }, []); - - const onBrushEnd = action('onBrushEnd'); + }, [showBrushTool]); return ( @@ -100,11 +83,7 @@ export const Example = () => { debugState={debugState} theme={{ heatmap }} baseTheme={useBaseTheme()} - onBrushEnd={(e) => { - onBrushEnd(e); - const { x, y } = e as HeatmapBrushEvent; - setSelection({ x, y }); - }} + onBrushEnd={onBrushEnd} /> { return niceTimeFormatter([1572825600000, 1572912000000])(value, { timeZone: 'UTC' }); }} timeZone={DATA_6.timeZone} - onBrushEnd={(e) => { - setSelection({ x: e.x, y: e.y }); - }} - highlightedData={persistCellsSelection ? selection : undefined} + highlightedData={highlightedData} xAxisTitle={showXAxisTitle ? 'Bottom axis' : undefined} yAxisTitle={showYAxisTitle ? 'Left axis' : undefined} /> diff --git a/storybook/stories/heatmap/2_categorical.story.tsx b/storybook/stories/heatmap/2_categorical.story.tsx index bd57a304f8..9c436fe845 100644 --- a/storybook/stories/heatmap/2_categorical.story.tsx +++ b/storybook/stories/heatmap/2_categorical.story.tsx @@ -25,14 +25,11 @@ export const Example = () => { const minFontSize = number('min fontSize', 6, { step: 1, min: 4, max: 10, range: true }, 'labels'); const maxFontSize = number('max fontSize', 12, { step: 1, min: 10, max: 64, range: true }, 'labels'); - const minCellHeight = number('min cell height', 10, { step: 1, min: 3, max: 8, range: true }, 'grid'); - const maxCellHeight = number('max cell height', 30, { step: 1, min: 8, max: 45, range: true }, 'grid'); - const showXAxisTitle = boolean('Show x axis title', true); const showYAxisTitle = boolean('Show y axis title', true); return ( - + { stroke: { width: 0, }, - cellHeight: { - min: minCellHeight, - max: maxCellHeight, - }, }, cell: { maxWidth: 'fill', diff --git a/storybook/stories/heatmap/3_time.story.tsx b/storybook/stories/heatmap/3_time.story.tsx index 6e282a8386..50ed46bc53 100644 --- a/storybook/stories/heatmap/3_time.story.tsx +++ b/storybook/stories/heatmap/3_time.story.tsx @@ -32,9 +32,6 @@ export const Example = () => { const heatmap = useMemo(() => { const styles: RecursivePartial = { grid: { - cellHeight: { - min: 20, - }, stroke: { width: 0.5, color: 'transparent', @@ -80,7 +77,7 @@ export const Example = () => { {DateTime.fromMillis(start.toMillis() + startTimeOffset).toISO()} to{' '} {DateTime.fromMillis(end.toMillis() + endTimeOffset).toISO()} - + { pointerUpdateTrigger="both" theme={{ heatmap: { - grid: { - cellHeight: { min: 50, max: 50 }, - cellWidth: { min: 50, max: 50 }, - }, cell: { border: { strokeWidth: 0.001, diff --git a/storybook/stories/small_multiples/3_grid_lines.story.tsx b/storybook/stories/small_multiples/3_grid_lines.story.tsx index a2333bec84..9e1d1484b8 100644 --- a/storybook/stories/small_multiples/3_grid_lines.story.tsx +++ b/storybook/stories/small_multiples/3_grid_lines.story.tsx @@ -7,7 +7,7 @@ */ import { action } from '@storybook/addon-actions'; -import { boolean, text } from '@storybook/addon-knobs'; +import { boolean, number, text } from '@storybook/addon-knobs'; import { startCase } from 'lodash'; import { DateTime } from 'luxon'; import React from 'react'; @@ -128,7 +128,36 @@ export const Example = () => { { + const debug = boolean('Debug', false); + const debugState = boolean('Enable debug state', true); + const timeBasedData = boolean('Time data', false); + const showLegend = boolean('Show Legend', false); + const vSplit = boolean('v - split', true, 'Data'); + const hSplit = boolean('h - split', true, 'Data'); + const vSplitCount = number('v - split count', 2, { min: 0 }, 'Data'); + const hSplitCount = number('h - split count', 2, { min: 0 }, 'Data'); + const vSplitCountAbs = vSplit ? vSplitCount : 1; + const hSplitCountAbs = hSplit ? hSplitCount : 1; + const categories = number('categories', 4, { min: 1, step: 1, range: true }, 'Data'); + const density = number('cell density(%)', 75, { min: 5, max: 100, step: 5, range: true }, 'Data') / 100; + const xScaleType = customKnobs.enum.scaleType('xScaleType', ScaleType.Linear, { + include: ['Linear', 'Ordinal'], + group: 'Data', + }); + + const smStyles: SmallMultiplesStyle = { + horizontalPanelPadding: { + // Note: not fully supported, See https://github.com/elastic/elastic-charts/issues/1992 + outer: number( + 'Horizontal outer pad', + 0, + { + range: true, + min: 0, + max: 0.5, + step: 0.05, + }, + 'SmallMultiples Styles', + ), + inner: number( + 'Horizontal inner pad', + 0.05, + { + range: true, + min: 0, + max: 0.5, + step: 0.05, + }, + 'SmallMultiples Styles', + ), + }, + verticalPanelPadding: { + // Note: not fully supported, See https://github.com/elastic/elastic-charts/issues/1992 + outer: number( + 'Vertical outer pad', + 0, + { + range: true, + min: 0, + max: 0.5, + step: 0.05, + }, + 'SmallMultiples Styles', + ), + inner: number( + 'Vertical inner pad', + 0.1, + { + range: true, + min: 0, + max: 0.5, + step: 0.05, + }, + 'SmallMultiples Styles', + ), + }, + }; + const showAxesTitles = boolean('Show axes title', true, 'SmallMultiples Styles'); + const showAxesPanelTitles = boolean('Show axes panel titles', true, 'SmallMultiples Styles'); + + const dataCount = timeBasedData ? numOfDays : 10; + const fullData = useMemo( + () => + dg.generateSMGroupedSeries(vSplitCountAbs, hSplitCountAbs, () => { + return dg.generateSimpleSeries(dataCount).flatMap((d) => + range(0, categories, 1).map((y) => { + return { + y, + x: d.x, + value: rng(0, 1000), + t: DateTime.fromISO('2020-01-01T00:00:00Z').plus({ days: d.x }).toMillis(), + }; + }), + ); + }), + [vSplitCountAbs, hSplitCountAbs, dataCount, categories], + ); + const data = useMemo( + () => sampleSize(fullData, vSplitCountAbs * hSplitCountAbs * dataCount * categories * density), + [categories, dataCount, density, fullData, vSplitCountAbs, hSplitCountAbs], + ); + const { highlightedData, onElementClick, onBrushEnd } = useHeatmapSelection(); + + return ( + + + + v} format={(v) => `Metric ${v}`} sort="numDesc" /> + h} format={(v) => `Host ${v}`} sort="numAsc" /> + + + Math.floor(d.y)} + valueAccessor="value" + valueFormatter={(d) => `${Number(d.toFixed(2))}`} + ySortPredicate="numAsc" + xScale={ + timeBasedData + ? { + type: ScaleType.Time, + interval: { + type: 'calendar', + value: 1, + unit: 'week', + }, + } + : { + type: xScaleType, + } + } + xAxisLabelFormatter={timeBasedData ? tickTimeFormatter : (v) => `C${v}`} + yAxisLabelFormatter={(v) => `R${v}`} + timeZone="UTC" + highlightedData={highlightedData} + xAxisTitle="Bottom axis" + yAxisTitle="Left axis" + /> + + ); +}; + +Example.parameters = { + background: { default: 'white' }, +}; diff --git a/storybook/stories/small_multiples/small_multiples.stories.tsx b/storybook/stories/small_multiples/small_multiples.stories.tsx index f890de1562..f8a6a26d3a 100644 --- a/storybook/stories/small_multiples/small_multiples.stories.tsx +++ b/storybook/stories/small_multiples/small_multiples.stories.tsx @@ -18,5 +18,6 @@ export { Example as gridLines } from './3_grid_lines.story'; export { Example as histogramBars } from './5_histogram_bars.story'; export { Example as heterogeneous } from './6_heterogeneous_cartesians.story'; export { Example as sunbursts } from './7_sunbursts.story'; +export { Example as heatmap } from './9_heatmap.story'; export { Example as sorting } from './8_sorting.story'; diff --git a/storybook/stories/sunburst/33_ordered_slices.story.tsx b/storybook/stories/sunburst/33_ordered_slices.story.tsx index 5acdccd7b0..1f5fb7d646 100644 --- a/storybook/stories/sunburst/33_ordered_slices.story.tsx +++ b/storybook/stories/sunburst/33_ordered_slices.story.tsx @@ -52,6 +52,10 @@ const sortPredicate = ([name1, node1]: ArrayEntry, [name2, node2]: ArrayEntry) = */ export const Example = () => { + const sortPredicateEnabled = boolean('Move "Other" to end', true); + const clockwiseSectors = boolean('clockwiseSectors', true); + const specialFirstInnermostSector = boolean('specialFirstInnermostSector', false); + return ( { { groupByRollup: (d: Datum) => d.dest, nodeLabel: (d: any) => countryLookup[d]?.name ?? d, - sortPredicate: boolean('Move "Other" to end', true) ? sortPredicate : null, + sortPredicate: sortPredicateEnabled ? sortPredicate : null, fillLabel: { fontWeight: 600, fontStyle: 'italic', @@ -103,8 +107,8 @@ export const Example = () => { }, }, ]} - clockwiseSectors - specialFirstInnermostSector={false} + clockwiseSectors={clockwiseSectors} + specialFirstInnermostSector={specialFirstInnermostSector} /> ); diff --git a/storybook/stories/utils/use_heatmap_selection.ts b/storybook/stories/utils/use_heatmap_selection.ts new file mode 100644 index 0000000000..5308657783 --- /dev/null +++ b/storybook/stories/utils/use_heatmap_selection.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { action } from '@storybook/addon-actions'; +import { boolean, button } from '@storybook/addon-knobs'; +import { useEffect, useCallback, useState } from 'react'; + +import { ElementClickListener, HeatmapBrushEvent, HeatmapElementEvent, HeatmapHighlightedData } from '@elastic/charts'; + +export const useHeatmapSelection = (disableActions = false) => { + const [selection, setSelection] = useState(); + const clearSelection = useCallback(() => setSelection(undefined), []); + const onElementClick: ElementClickListener = useCallback( + (e) => { + if (!disableActions) action('onElementClick')(e); + const { x, y, smHorizontalAccessorValue, smVerticalAccessorValue } = (e as HeatmapElementEvent[])[0][0].datum; + setSelection({ + x: [x], + y: [y], + smHorizontalAccessorValue, + smVerticalAccessorValue, + }); + }, + [disableActions], + ); + const onBrushEnd = useCallback( + (e) => { + if (!disableActions) action('brushEvent')(e); + setSelection(e as HeatmapBrushEvent); + }, + [disableActions], + ); + useEffect(() => { + document.addEventListener('keyup', ({ key }) => { + if (key === 'Escape') clearSelection(); + }); + }, [clearSelection]); + + const persistCellsSelection = boolean('Persist cells selection', true); + button('Clear cells selection', clearSelection); + + return { + selection, + setSelection, + onBrushEnd, + onElementClick, + highlightedData: persistCellsSelection ? selection : undefined, + }; +}; diff --git a/storybook/stories/utils/utils.ts b/storybook/stories/utils/utils.ts index 866cb45982..8dc9e00fd3 100644 --- a/storybook/stories/utils/utils.ts +++ b/storybook/stories/utils/utils.ts @@ -6,6 +6,9 @@ * Side Public License, v 1. */ +import _, { Dictionary, NumericDictionary } from 'lodash'; +import seedrandom from 'seedrandom'; + import { arrayToLookup, hueInterpolator } from '@elastic/charts/src/common/color_calcs'; import { countryDimension, @@ -14,6 +17,7 @@ import { productPriceNames, } from '@elastic/charts/src/mocks/hierarchical/dimension_codes'; import { palettes } from '@elastic/charts/src/mocks/hierarchical/palettes'; +import { getRNGSeed } from '@elastic/charts/src/mocks/utils'; export const productLookup = arrayToLookup((d: any) => d.sitc1, productDimension); export const regionLookup = arrayToLookup((d: any) => d.region, regionDimension); @@ -230,3 +234,13 @@ export const discreteColor = export const decreasingOpacityCET2 = (opacity: number) => (d: any, i: number, a: any[]) => hueInterpolator(palettes.CET2s.map(([r, g, b]) => [r, g, b, opacity]))(i / (a.length + 1)); + +export function sampleSize( + collection: Dictionary | NumericDictionary | null | undefined, + n?: number, + seed = getRNGSeed(), +): T[] { + seedrandom(seed, { global: true }); + const lodash = _.runInContext(); + return lodash.sampleSize(collection, n); +}