diff --git a/.playground/playground.tsx b/.playground/playground.tsx index 8b6c3c71a2..c3bd5e6f2b 100644 --- a/.playground/playground.tsx +++ b/.playground/playground.tsx @@ -1,12 +1,8 @@ import React from 'react'; - import { Chart, ScaleType, Position, Axis, getAxisId, timeFormatter, getSpecId, AreaSeries } from '../src'; import { KIBANA_METRICS } from '../src/utils/data_samples/test_dataset_kibana'; -export class Playground extends React.Component<{}, { isSunburstShown: boolean }> { +export class Playground extends React.Component { chartRef: React.RefObject = React.createRef(); - state = { - isSunburstShown: true, - }; onBrushEnd = (min: number, max: number) => { // eslint-disable-next-line no-console console.log({ min, max }); diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-annotations-rect-styling-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-annotations-rect-styling-visually-looks-correct-1-snap.png index e5cdc25fc1..5046e34f4a 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-annotations-rect-styling-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-annotations-rect-styling-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-area-chart-band-area-chart-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-area-chart-band-area-chart-visually-looks-correct-1-snap.png index 8587b2c98d..403fecddd7 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-area-chart-band-area-chart-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-area-chart-band-area-chart-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-area-chart-stacked-band-area-chart-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-area-chart-stacked-band-area-chart-visually-looks-correct-1-snap.png index 9ef85704c4..d9a809b01b 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-area-chart-stacked-band-area-chart-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-area-chart-stacked-band-area-chart-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-area-chart-stacked-w-axis-and-legend-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-area-chart-stacked-w-axis-and-legend-visually-looks-correct-1-snap.png index 073dcb6045..a518cb4cb5 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-area-chart-stacked-w-axis-and-legend-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-area-chart-stacked-w-axis-and-legend-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-area-chart-stacked-with-separated-specs-same-naming-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-area-chart-stacked-with-separated-specs-same-naming-visually-looks-correct-1-snap.png index 7656229b36..3e65eed2ce 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-area-chart-stacked-with-separated-specs-same-naming-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-area-chart-stacked-with-separated-specs-same-naming-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-area-chart-stacked-with-separated-specs-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-area-chart-stacked-with-separated-specs-visually-looks-correct-1-snap.png index 073dcb6045..a518cb4cb5 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-area-chart-stacked-with-separated-specs-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-area-chart-stacked-with-separated-specs-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-area-chart-with-4-axes-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-area-chart-with-4-axes-visually-looks-correct-1-snap.png index 923f01d36c..e4d4614486 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-area-chart-with-4-axes-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-area-chart-with-4-axes-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-axis-customizing-domain-limits-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-axis-customizing-domain-limits-visually-looks-correct-1-snap.png index dcc0d05c85..03b374c562 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-axis-customizing-domain-limits-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-axis-customizing-domain-limits-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-axis-w-many-tick-labels-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-axis-w-many-tick-labels-visually-looks-correct-1-snap.png index 5903e4a098..f5eb306afe 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-axis-w-many-tick-labels-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-axis-w-many-tick-labels-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-axis-with-multi-axis-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-axis-with-multi-axis-visually-looks-correct-1-snap.png index 1af7531a37..5afbeddc31 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-axis-with-multi-axis-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-axis-with-multi-axis-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-bar-chart-band-bar-chart-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-bar-chart-band-bar-chart-visually-looks-correct-1-snap.png index 18f1507e5b..db4c12a661 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-bar-chart-band-bar-chart-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-bar-chart-band-bar-chart-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-bar-chart-with-high-data-volume-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-bar-chart-with-high-data-volume-visually-looks-correct-1-snap.png index 7f8db335a8..95bd396293 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-bar-chart-with-high-data-volume-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-bar-chart-with-high-data-volume-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-bar-chart-with-value-label-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-bar-chart-with-value-label-visually-looks-correct-1-snap.png index 3b749d7a76..7b92c2493e 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-bar-chart-with-value-label-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-bar-chart-with-value-label-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-grids-multiple-axes-with-the-same-position-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-grids-multiple-axes-with-the-same-position-visually-looks-correct-1-snap.png index 53277a35ec..2304651253 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-grids-multiple-axes-with-the-same-position-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-grids-multiple-axes-with-the-same-position-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-interactions-crosshair-with-time-axis-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-interactions-crosshair-with-time-axis-visually-looks-correct-1-snap.png index f931337614..e5a1868e9a 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-interactions-crosshair-with-time-axis-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-interactions-crosshair-with-time-axis-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-legend-display-values-in-legend-elements-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-legend-display-values-in-legend-elements-visually-looks-correct-1-snap.png index f9c91eea50..b36613b15e 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-legend-display-values-in-legend-elements-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-legend-display-values-in-legend-elements-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-legend-legend-spacing-buffer-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-legend-legend-spacing-buffer-visually-looks-correct-1-snap.png index cf8b08664c..a665a5a314 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-legend-legend-spacing-buffer-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-legend-legend-spacing-buffer-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-line-chart-multiserieswithlogvalues-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-line-chart-multiserieswithlogvalues-visually-looks-correct-1-snap.png index 9cf302557f..17e6d545ef 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-line-chart-multiserieswithlogvalues-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-line-chart-multiserieswithlogvalues-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-mixed-charts-test-bar-lines-time-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-mixed-charts-test-bar-lines-time-visually-looks-correct-1-snap.png index fd158f8b2c..789d43759c 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-mixed-charts-test-bar-lines-time-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-mixed-charts-test-bar-lines-time-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-rotations-with-ordinal-axis-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-rotations-with-ordinal-axis-visually-looks-correct-1-snap.png index 2644f58005..c095404db1 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-rotations-with-ordinal-axis-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-rotations-with-ordinal-axis-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-scales-x-scale-year-scale-custom-timezone-same-tone-tooltip-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-scales-x-scale-year-scale-custom-timezone-same-tone-tooltip-visually-looks-correct-1-snap.png index 5c0976828d..a723bd7f84 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-scales-x-scale-year-scale-custom-timezone-same-tone-tooltip-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-scales-x-scale-year-scale-custom-timezone-same-tone-tooltip-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-stylings-custom-series-styles-area-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-stylings-custom-series-styles-area-visually-looks-correct-1-snap.png index d9b5a3e02d..d53011a231 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-stylings-custom-series-styles-area-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-stylings-custom-series-styles-area-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-stylings-custom-series-styles-bars-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-stylings-custom-series-styles-bars-visually-looks-correct-1-snap.png index fcda5d7347..8745bd1590 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-stylings-custom-series-styles-bars-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-stylings-custom-series-styles-bars-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-stylings-theme-style-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-stylings-theme-style-visually-looks-correct-1-snap.png index dbe254a3ce..5e84959e60 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-stylings-theme-style-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-stylings-theme-style-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/area-stories-test-ts-area-series-stories-accessor-formats-should-show-custom-format-1-snap.png b/integration/tests/__image_snapshots__/area-stories-test-ts-area-series-stories-accessor-formats-should-show-custom-format-1-snap.png index 9b9d45a907..4b4cf9e0c1 100644 Binary files a/integration/tests/__image_snapshots__/area-stories-test-ts-area-series-stories-accessor-formats-should-show-custom-format-1-snap.png and b/integration/tests/__image_snapshots__/area-stories-test-ts-area-series-stories-accessor-formats-should-show-custom-format-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/area-stories-test-ts-area-series-stories-scale-to-extents-scaley-scale-to-data-extent-is-false-should-show-correct-extents-banded-1-snap.png b/integration/tests/__image_snapshots__/area-stories-test-ts-area-series-stories-scale-to-extents-scaley-scale-to-data-extent-is-false-should-show-correct-extents-banded-1-snap.png index 9ef85704c4..d9a809b01b 100644 Binary files a/integration/tests/__image_snapshots__/area-stories-test-ts-area-series-stories-scale-to-extents-scaley-scale-to-data-extent-is-false-should-show-correct-extents-banded-1-snap.png and b/integration/tests/__image_snapshots__/area-stories-test-ts-area-series-stories-scale-to-extents-scaley-scale-to-data-extent-is-false-should-show-correct-extents-banded-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/area-stories-test-ts-area-series-stories-scale-to-extents-scaley-scale-to-data-extent-is-false-should-show-correct-extents-stacked-1-snap.png b/integration/tests/__image_snapshots__/area-stories-test-ts-area-series-stories-scale-to-extents-scaley-scale-to-data-extent-is-false-should-show-correct-extents-stacked-1-snap.png index 9ef85704c4..d9a809b01b 100644 Binary files a/integration/tests/__image_snapshots__/area-stories-test-ts-area-series-stories-scale-to-extents-scaley-scale-to-data-extent-is-false-should-show-correct-extents-stacked-1-snap.png and b/integration/tests/__image_snapshots__/area-stories-test-ts-area-series-stories-scale-to-extents-scaley-scale-to-data-extent-is-false-should-show-correct-extents-stacked-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/area-stories-test-ts-area-series-stories-scale-to-extents-scaley-scale-to-data-extent-is-true-should-show-correct-extents-banded-1-snap.png b/integration/tests/__image_snapshots__/area-stories-test-ts-area-series-stories-scale-to-extents-scaley-scale-to-data-extent-is-true-should-show-correct-extents-banded-1-snap.png index afd5877d25..f7d06156ef 100644 Binary files a/integration/tests/__image_snapshots__/area-stories-test-ts-area-series-stories-scale-to-extents-scaley-scale-to-data-extent-is-true-should-show-correct-extents-banded-1-snap.png and b/integration/tests/__image_snapshots__/area-stories-test-ts-area-series-stories-scale-to-extents-scaley-scale-to-data-extent-is-true-should-show-correct-extents-banded-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/area-stories-test-ts-area-series-stories-scale-to-extents-scaley-scale-to-data-extent-is-true-should-show-correct-extents-stacked-1-snap.png b/integration/tests/__image_snapshots__/area-stories-test-ts-area-series-stories-scale-to-extents-scaley-scale-to-data-extent-is-true-should-show-correct-extents-stacked-1-snap.png index afd5877d25..f7d06156ef 100644 Binary files a/integration/tests/__image_snapshots__/area-stories-test-ts-area-series-stories-scale-to-extents-scaley-scale-to-data-extent-is-true-should-show-correct-extents-stacked-1-snap.png and b/integration/tests/__image_snapshots__/area-stories-test-ts-area-series-stories-scale-to-extents-scaley-scale-to-data-extent-is-true-should-show-correct-extents-stacked-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-should-hide-bottom-axis-1-snap.png b/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-should-hide-bottom-axis-1-snap.png index f633f54844..e2baa23bf2 100644 Binary files a/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-should-hide-bottom-axis-1-snap.png and b/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-should-hide-bottom-axis-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-should-render-proper-tick-count-with-show-overlapping-labels-1-snap.png b/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-should-render-proper-tick-count-with-show-overlapping-labels-1-snap.png index b8ace661f0..e16300ac9f 100644 Binary files a/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-should-render-proper-tick-count-with-show-overlapping-labels-1-snap.png and b/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-should-render-proper-tick-count-with-show-overlapping-labels-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-should-render-tick-padding-1-snap.png b/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-should-render-tick-padding-1-snap.png index a7da74a8ab..112bb41fd3 100644 Binary files a/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-should-render-tick-padding-1-snap.png and b/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-should-render-tick-padding-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-should-render-ticks-with-varied-rotations-1-snap.png b/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-should-render-ticks-with-varied-rotations-1-snap.png index af6b3b5958..008a5350c2 100644 Binary files a/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-should-render-ticks-with-varied-rotations-1-snap.png and b/integration/tests/__image_snapshots__/axis-stories-test-ts-axis-stories-should-render-ticks-with-varied-rotations-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/legend-stories-test-ts-legend-stories-should-0-legend-buffer-1-snap.png b/integration/tests/__image_snapshots__/legend-stories-test-ts-legend-stories-should-0-legend-buffer-1-snap.png index c140c9a2b4..d96e6a41d6 100644 Binary files a/integration/tests/__image_snapshots__/legend-stories-test-ts-legend-stories-should-0-legend-buffer-1-snap.png and b/integration/tests/__image_snapshots__/legend-stories-test-ts-legend-stories-should-0-legend-buffer-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/line-stories-test-ts-line-series-stories-rotation-rotation-90-1-snap.png b/integration/tests/__image_snapshots__/line-stories-test-ts-line-series-stories-rotation-rotation-90-1-snap.png index 13fd5ea4e5..32eb742c10 100644 Binary files a/integration/tests/__image_snapshots__/line-stories-test-ts-line-series-stories-rotation-rotation-90-1-snap.png and b/integration/tests/__image_snapshots__/line-stories-test-ts-line-series-stories-rotation-rotation-90-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/line-stories-test-ts-line-series-stories-rotation-rotation-negative-90-1-snap.png b/integration/tests/__image_snapshots__/line-stories-test-ts-line-series-stories-rotation-rotation-negative-90-1-snap.png index 96d61ea8c5..bffe2e2307 100644 Binary files a/integration/tests/__image_snapshots__/line-stories-test-ts-line-series-stories-rotation-rotation-negative-90-1-snap.png and b/integration/tests/__image_snapshots__/line-stories-test-ts-line-series-stories-rotation-rotation-negative-90-1-snap.png differ diff --git a/package.json b/package.json index 9bf501ae26..0b7ee35b9f 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "typecheck:all": "tsc -p ./tsconfig.json --noEmit", "playground": "cd .playground && webpack-dev-server", "playground:ie": "cd .playground && webpack-dev-server --host=0.0.0.0 --disable-host-check --useLocalIp", - "jest:integration": "TZ=UTC JEST_PUPPETEER_CONFIG=integration/jest-puppeteer.config.js jest --verbose --rootDir=integration -c=integration/jest.config.js --runInBand", + "jest:integration": "rm -rf ./integration/tests/__image_snapshots__/__diff_output__ && TZ=UTC JEST_PUPPETEER_CONFIG=integration/jest-puppeteer.config.js jest --verbose --rootDir=integration -c=integration/jest.config.js --runInBand", "test:browsers": "cd .playground && jest -c=../browsers/jest.config.js ../browsers/browsers.test.ts", "ts:prune": "ts-prune", "backport": "backport" @@ -165,13 +165,11 @@ "d3-color": "^1.4.0", "d3-scale": "^1.0.7", "d3-shape": "^1.3.4", - "konva": "^4.0.18", "newtype-ts": "^0.2.4", + "path2d-polyfill": "^0.4.2", "prop-types": "^15.7.2", "re-reselect": "^3.4.0", - "react-konva": "16.10.1-0", "react-redux": "^7.1.0", - "react-spring": "^8.0.8", "redux": "^4.0.4", "reselect": "^4.0.0", "resize-observer-polyfill": "^1.5.1", diff --git a/src/chart_types/partition_chart/layout/types/types.ts b/src/chart_types/partition_chart/layout/types/types.ts index eb76d4c194..daa713563e 100644 --- a/src/chart_types/partition_chart/layout/types/types.ts +++ b/src/chart_types/partition_chart/layout/types/types.ts @@ -30,6 +30,19 @@ export interface Font { export type PartialFont = Partial; +export const TEXT_ALIGNS = Object.freeze(['start', 'end', 'left', 'right', 'center'] as const); +export type TextAlign = typeof TEXT_ALIGNS[number]; + +export const TEXT_BASELINE = Object.freeze([ + 'top', + 'hanging', + 'middle', + 'alphabetic', + 'ideographic', + 'bottom', +] as const); +export type TextBaseline = typeof TEXT_BASELINE[number]; + export interface Box extends Font { text: string; } diff --git a/src/chart_types/partition_chart/layout/utils/d3_utils.ts b/src/chart_types/partition_chart/layout/utils/d3_utils.ts index dfecdf4742..21e9b38b63 100644 --- a/src/chart_types/partition_chart/layout/utils/d3_utils.ts +++ b/src/chart_types/partition_chart/layout/utils/d3_utils.ts @@ -20,3 +20,8 @@ export function argsToRGBString(r: number, g: number, b: number, opacity: number // d3.rgb returns an Rgb instance, which has a specialized `toString` method return argsToRGB(r, g, b, opacity).toString(); } + +export function RGBtoString(rgb: RgbObject): string { + const { r, g, b, opacity } = rgb; + return argsToRGBString(r, g, b, opacity); +} diff --git a/src/chart_types/partition_chart/renderer/canvas/canvas_renderers.ts b/src/chart_types/partition_chart/renderer/canvas/canvas_renderers.ts index 50fc48cf64..c83aa518a3 100644 --- a/src/chart_types/partition_chart/renderer/canvas/canvas_renderers.ts +++ b/src/chart_types/partition_chart/renderer/canvas/canvas_renderers.ts @@ -1,4 +1,4 @@ -import { Coordinate, Pixels } from '../../layout/types/geometry_types'; +import { Pixels } from '../../layout/types/geometry_types'; import { addOpacity } from '../../layout/utils/calcs'; import { LinkLabelVM, @@ -11,35 +11,12 @@ import { import { TAU } from '../../layout/utils/math'; import { PartitionLayout } from '../../layout/types/config_types'; import { cssFontShorthand } from '../../layout/utils/measure'; +import { withContext, renderLayers, clearCanvas } from '../../../../renderers/canvas'; // the burnout avoidance in the center of the pie const LINE_WIDTH_MULT = 10; // border can be a maximum 1/LINE_WIDTH_MULT - th of the sector angle, otherwise the border would dominate const TAPER_OFF_LIMIT = 50; // taper off within a radius of TAPER_OFF_LIMIT to avoid burnout in the middle of the pie when there are hundreds of pies -// withContext abstracts out the otherwise error-prone save/restore pairing; it can be nested and/or put into sequence -// The idea is that you just set what's needed for the enclosed snippet, which may temporarily override values in the -// outer withContext. Example: we use a +y = top convention, so when doing text rendering, y has to be flipped (ctx.scale) -// otherwise the text will render upside down. -function withContext(ctx: CanvasRenderingContext2D, fun: (ctx: CanvasRenderingContext2D) => void) { - ctx.save(); - fun(ctx); - ctx.restore(); -} - -function clearCanvas( - ctx: CanvasRenderingContext2D, - width: Coordinate, - height: Coordinate /*, backgroundColor: string*/, -) { - withContext(ctx, (ctx) => { - // two steps, as the backgroundColor may have a non-one opacity - // todo we should avoid `fillRect` by setting the element background via CSS - ctx.clearRect(-width, -height, 2 * width, 2 * height); // remove past contents - // ctx.fillStyle = backgroundColor; - // ctx.fillRect(-width, -height, 2 * width, 2 * height); // new background - }); -} - function renderTextRow(ctx: CanvasRenderingContext2D, { fontSize, fillTextColor, rotation }: RowSet) { return (currentRow: TextRow) => { const crx = currentRow.rowCentroidX - (Math.cos(rotation) * currentRow.length) / 2; @@ -145,11 +122,6 @@ function renderRectangles(ctx: CanvasRenderingContext2D, quadViewModel: QuadView }); } -// order of rendering is important; determined by the order of layers in the array -function renderLayers(ctx: CanvasRenderingContext2D, layers: Array<(ctx: CanvasRenderingContext2D) => void>) { - layers.forEach((renderLayer) => renderLayer(ctx)); -} - function renderFillOutsideLinks( ctx: CanvasRenderingContext2D, outsideLinksViewModel: OutsideLinksViewModel[], diff --git a/src/chart_types/partition_chart/renderer/canvas/partition.tsx b/src/chart_types/partition_chart/renderer/canvas/partition.tsx index 18731a7cba..9564403fa7 100644 --- a/src/chart_types/partition_chart/renderer/canvas/partition.tsx +++ b/src/chart_types/partition_chart/renderer/canvas/partition.tsx @@ -63,7 +63,10 @@ class PartitionComponent extends React.Component { // the DOM element has just been appended, and getContext('2d') is always non-null, // so we could use a couple of ! non-null assertions but no big plus this.tryCanvasContext(); - this.drawCanvas(); + if (this.props.initialized) { + this.drawCanvas(); + this.props.onChartRendered(); + } } render() { diff --git a/src/chart_types/xy_chart/annotations/annotation_utils.ts b/src/chart_types/xy_chart/annotations/annotation_utils.ts index a278287ebf..71b6f7bdd2 100644 --- a/src/chart_types/xy_chart/annotations/annotation_utils.ts +++ b/src/chart_types/xy_chart/annotations/annotation_utils.ts @@ -151,7 +151,6 @@ export function computeAnnotationDimensions( // Annotations should always align with the axis line in histogram mode const xScaleOffset = computeXScaleOffset(xScale, enableHistogramMode, HistogramModeAlignments.Start); - annotations.forEach((annotationSpec) => { const { id } = annotationSpec; if (isLineAnnotation(annotationSpec)) { @@ -195,7 +194,7 @@ export function computeAnnotationDimensions( export function computeAnnotationTooltipState( cursorPosition: Point, - annotationDimensions: Map, + annotationDimensions: Map, annotationSpecs: AnnotationSpec[], chartRotation: Rotation, axesSpecs: AxisSpec[], @@ -216,7 +215,7 @@ export function computeAnnotationTooltipState( } const lineAnnotationTooltipState = computeLineAnnotationTooltipState( cursorPosition, - annotationDimension, + annotationDimension as AnnotationLineProps[], groupId, spec.domainType, axesSpecs, @@ -228,7 +227,7 @@ export function computeAnnotationTooltipState( } else if (isRectAnnotation(spec)) { const rectAnnotationTooltipState = computeRectAnnotationTooltipState( cursorPosition, - annotationDimension, + annotationDimension as AnnotationRectProps[], chartRotation, chartDimensions, spec.renderTooltip, diff --git a/src/chart_types/xy_chart/renderer/canvas/annotations/index.ts b/src/chart_types/xy_chart/renderer/canvas/annotations/index.ts new file mode 100644 index 0000000000..5cc8b99754 --- /dev/null +++ b/src/chart_types/xy_chart/renderer/canvas/annotations/index.ts @@ -0,0 +1,38 @@ +import { AnnotationDimensions } from '../../../annotations/annotation_utils'; +import { AnnotationSpec, isLineAnnotation, isRectAnnotation } from '../../../utils/specs'; +import { getSpecsById } from '../../../state/utils'; +import { AnnotationId } from '../../../../../utils/ids'; +import { mergeWithDefaultAnnotationLine, mergeWithDefaultAnnotationRect } from '../../../../../utils/themes/theme'; +import { renderLineAnnotations } from './lines'; +import { AnnotationLineProps } from '../../../annotations/line_annotation_tooltip'; +import { renderRectAnnotations } from './rect'; +import { AnnotationRectProps } from '../../../annotations/rect_annotation_tooltip'; + +interface AnnotationProps { + annotationDimensions: Map; + annotationSpecs: AnnotationSpec[]; +} +export function renderAnnotations( + ctx: CanvasRenderingContext2D, + props: AnnotationProps, + renderOnBackground: boolean = true, +) { + const { annotationDimensions, annotationSpecs } = props; + + annotationDimensions.forEach((annotation, id) => { + const spec = getSpecsById(annotationSpecs, id); + if (!spec) { + return null; + } + const isBackground = !spec.zIndex || (spec.zIndex && spec.zIndex <= 0); + if ((isBackground && renderOnBackground) || (!isBackground && !renderOnBackground)) { + if (isLineAnnotation(spec)) { + const lineStyle = mergeWithDefaultAnnotationLine(spec.style); + renderLineAnnotations(ctx, annotation as AnnotationLineProps[], lineStyle); + } else if (isRectAnnotation(spec)) { + const rectStyle = mergeWithDefaultAnnotationRect(spec.style); + renderRectAnnotations(ctx, annotation as AnnotationRectProps[], rectStyle); + } + } + }); +} diff --git a/src/chart_types/xy_chart/renderer/canvas/annotations/lines.ts b/src/chart_types/xy_chart/renderer/canvas/annotations/lines.ts new file mode 100644 index 0000000000..b2decd3abe --- /dev/null +++ b/src/chart_types/xy_chart/renderer/canvas/annotations/lines.ts @@ -0,0 +1,32 @@ +import { Stroke, Line } from '../../../../../geoms/types'; +import { stringToRGB } from '../../../../partition_chart/layout/utils/d3_utils'; +import { AnnotationLineProps } from '../../../annotations/line_annotation_tooltip'; +import { LineAnnotationStyle } from '../../../../../utils/themes/theme'; +import { renderMultiLine } from '../primitives/line'; + +export function renderLineAnnotations( + ctx: CanvasRenderingContext2D, + annotations: AnnotationLineProps[], + lineStyle: LineAnnotationStyle, +) { + const lines = annotations.map((annotation) => { + const { + start: { x1, y1 }, + end: { x2, y2 }, + } = annotation.linePathPoints; + return { + x1, + y1, + x2, + y2, + }; + }); + const strokeColor = stringToRGB(lineStyle.line.stroke); + strokeColor.opacity = strokeColor.opacity * lineStyle.line.opacity; + const stroke: Stroke = { + color: strokeColor, + width: lineStyle.line.strokeWidth, + }; + + renderMultiLine(ctx, lines, stroke); +} diff --git a/src/chart_types/xy_chart/renderer/canvas/annotations/rect.ts b/src/chart_types/xy_chart/renderer/canvas/annotations/rect.ts new file mode 100644 index 0000000000..84f9f2b396 --- /dev/null +++ b/src/chart_types/xy_chart/renderer/canvas/annotations/rect.ts @@ -0,0 +1,36 @@ +import { renderRect } from '../primitives/rect'; +import { Rect, Fill, Stroke } from '../../../../../geoms/types'; +import { AnnotationRectProps } from '../../../annotations/rect_annotation_tooltip'; +import { RectAnnotationStyle } from '../../../../../utils/themes/theme'; +import { stringToRGB } from '../../../../partition_chart/layout/utils/d3_utils'; +import { withContext } from '../../../../../renderers/canvas'; + +export function renderRectAnnotations( + ctx: CanvasRenderingContext2D, + annotations: AnnotationRectProps[], + rectStyle: RectAnnotationStyle, +) { + const rects = annotations.map((annotation) => { + return annotation.rect; + }); + const fillColor = stringToRGB(rectStyle.fill); + fillColor.opacity = fillColor.opacity * rectStyle.opacity; + const fill: Fill = { + color: fillColor, + }; + const strokeColor = stringToRGB(rectStyle.stroke); + strokeColor.opacity = strokeColor.opacity * rectStyle.opacity; + const stroke: Stroke = { + color: strokeColor, + width: rectStyle.strokeWidth, + }; + + const rectsLength = rects.length; + + for (let i = 0; i < rectsLength; i++) { + const rect = rects[i]; + withContext(ctx, (ctx) => { + renderRect(ctx, rect, fill, stroke); + }); + } +} diff --git a/src/chart_types/xy_chart/renderer/canvas/area_geometries.tsx b/src/chart_types/xy_chart/renderer/canvas/area_geometries.tsx deleted file mode 100644 index 9fba822db1..0000000000 --- a/src/chart_types/xy_chart/renderer/canvas/area_geometries.tsx +++ /dev/null @@ -1,166 +0,0 @@ -import React from 'react'; -import { Group as KonvaGroup } from 'konva/types/Group'; -import { PathConfig } from 'konva/types/shapes/Path'; -import { Circle, Group, Path } from 'react-konva'; -import { deepEqual } from '../../../../utils/fast_deep_equal'; -import { - buildAreaRenderProps, - buildPointStyleProps, - buildPointRenderProps, - PointStyleProps, - buildLineRenderProps, -} from './utils/rendering_props_utils'; -import { getSeriesIdentifierPrefixedKey, getGeometryStateStyle } from '../../rendering/rendering'; -import { mergePartial } from '../../../../utils/commons'; -import { AreaGeometry, PointGeometry } from '../../../../utils/geometry'; -import { PointStyle, SharedGeometryStateStyle } from '../../../../utils/themes/theme'; -import { LegendItem } from '../../legend/legend'; -import { Clippings, clipRanges } from './bar_values_utils'; -import { SeriesIdentifier } from '../../utils/series'; - -interface AreaGeometriesDataProps { - animated?: boolean; - areas: AreaGeometry[]; - sharedStyle: SharedGeometryStateStyle; - highlightedLegendItem: LegendItem | null; - clippings: Clippings; -} - -export class AreaGeometries extends React.Component { - static defaultProps: Partial = { - animated: false, - }; - private readonly barSeriesRef: React.RefObject = React.createRef(); - constructor(props: AreaGeometriesDataProps) { - super(props); - this.barSeriesRef = React.createRef(); - } - - shouldComponentUpdate(nextProps: AreaGeometriesDataProps) { - return !deepEqual(this.props, nextProps); - } - - render() { - return ( - - {this.renderAreaGeoms()} - - ); - } - private renderAreaGeoms = (): JSX.Element[] => { - const { sharedStyle, highlightedLegendItem, areas, clippings } = this.props; - return areas.reduce((acc, glyph, i) => { - const { seriesAreaLineStyle, seriesAreaStyle, seriesPointStyle, seriesIdentifier } = glyph; - if (seriesAreaStyle.visible) { - acc.push(this.renderArea(glyph, sharedStyle, highlightedLegendItem, clippings)); - } - if (seriesAreaLineStyle.visible) { - acc.push(this.renderAreaLines(glyph, i, sharedStyle, highlightedLegendItem, clippings)); - } - if (seriesPointStyle.visible) { - const geometryStateStyle = getGeometryStateStyle( - seriesIdentifier, - this.props.highlightedLegendItem, - sharedStyle, - ); - const pointStyleProps = buildPointStyleProps(glyph.color, seriesPointStyle, geometryStateStyle); - acc.push(...this.renderPoints(glyph.points, i, pointStyleProps, glyph.seriesIdentifier)); - } - return acc; - }, []); - }; - private renderArea = ( - glyph: AreaGeometry, - sharedStyle: SharedGeometryStateStyle, - highlightedLegendItem: LegendItem | null, - clippings: Clippings, - ): JSX.Element => { - const { area, color, transform, seriesIdentifier, seriesAreaStyle, clippedRanges } = glyph; - const geometryStateStyle = getGeometryStateStyle(seriesIdentifier, highlightedLegendItem, sharedStyle); - const key = getSeriesIdentifierPrefixedKey(seriesIdentifier, 'area-'); - const areaProps = buildAreaRenderProps(transform.x, area, color, seriesAreaStyle, geometryStateStyle); - - if (clippedRanges.length > 0) { - return ( - - - - - - - - - ); - } - - return ( - - - - ); - }; - private renderAreaLines = ( - glyph: AreaGeometry, - areaIndex: number, - sharedStyle: SharedGeometryStateStyle, - highlightedLegendItem: LegendItem | null, - clippings: Clippings, - ): JSX.Element => { - const { lines, color, seriesIdentifier, transform, seriesAreaLineStyle, clippedRanges } = glyph; - const geometryStateStyle = getGeometryStateStyle(seriesIdentifier, highlightedLegendItem, sharedStyle); - const groupKey = getSeriesIdentifierPrefixedKey(seriesIdentifier, `area-line-${areaIndex}`); - const linesElementProps = lines.map<{ key: string; props: PathConfig }>((linePath, lineIndex) => { - const key = getSeriesIdentifierPrefixedKey(seriesIdentifier, `area-line-${areaIndex}-${lineIndex}`); - const props = buildLineRenderProps(transform.x, linePath, color, seriesAreaLineStyle, geometryStateStyle); - return { key, props }; - }); - - if (clippedRanges.length > 0) { - return ( - - - {linesElementProps.map(({ key, props }) => ( - - ))} - - - {linesElementProps.map(({ key, props }) => ( - - ))} - - - ); - } - - return ( - - {linesElementProps.map(({ key, props }) => ( - - ))} - - ); - }; - - private mergePointPropsWithOverrides(props: PointStyleProps, overrides?: Partial): PointStyleProps { - if (!overrides) { - return props; - } - - return mergePartial(props, overrides); - } - - private renderPoints = ( - areaPoints: PointGeometry[], - areaIndex: number, - pointStyleProps: PointStyleProps, - seriesIdentifier: SeriesIdentifier, - ): JSX.Element[] => { - return areaPoints.map((areaPoint, pointIndex) => { - const { x, y, transform, styleOverrides } = areaPoint; - const key = getSeriesIdentifierPrefixedKey(seriesIdentifier, `area-point-${areaIndex}-${pointIndex}-`); - const pointStyle = this.mergePointPropsWithOverrides(pointStyleProps, styleOverrides); - const pointProps = buildPointRenderProps(transform.x + x, y, pointStyle); - return ; - }); - }; -} diff --git a/src/chart_types/xy_chart/renderer/canvas/areas.ts b/src/chart_types/xy_chart/renderer/canvas/areas.ts new file mode 100644 index 0000000000..1c0985bd66 --- /dev/null +++ b/src/chart_types/xy_chart/renderer/canvas/areas.ts @@ -0,0 +1,79 @@ +import { getGeometryStateStyle } from '../../rendering/rendering'; +import { AreaGeometry } from '../../../../utils/geometry'; +import { SharedGeometryStateStyle } from '../../../../utils/themes/theme'; +import { LegendItem } from '../../legend/legend'; +import { withClip, withContext } from '../../../../renderers/canvas'; +import { renderPoints } from './points'; +import { renderLinePaths, renderAreaPath } from './primitives/path'; +import { Rect } from '../../../../geoms/types'; +import { buildAreaStyles } from './styles/area'; +import { buildLineStyles } from './styles/line'; + +interface AreaGeometriesProps { + areas: AreaGeometry[]; + sharedStyle: SharedGeometryStateStyle; + highlightedLegendItem: LegendItem | null; + clippings: Rect; +} + +export function renderAreas(ctx: CanvasRenderingContext2D, props: AreaGeometriesProps) { + withContext(ctx, (ctx) => { + const { sharedStyle, highlightedLegendItem, areas, clippings } = props; + withClip(ctx, clippings, (ctx: CanvasRenderingContext2D) => { + ctx.save(); + + for (let i = 0; i < areas.length; i++) { + const glyph = areas[i]; + const { seriesAreaLineStyle, seriesAreaStyle } = glyph; + if (seriesAreaStyle.visible) { + withContext(ctx, () => { + renderArea(ctx, glyph, sharedStyle, highlightedLegendItem, clippings); + }); + } + if (seriesAreaLineStyle.visible) { + withContext(ctx, () => { + renderAreaLines(ctx, glyph, sharedStyle, highlightedLegendItem, clippings); + }); + } + } + ctx.rect(clippings.x, clippings.y, clippings.width, clippings.height); + ctx.clip(); + ctx.restore(); + }); + for (let i = 0; i < areas.length; i++) { + const glyph = areas[i]; + const { seriesPointStyle, seriesIdentifier } = glyph; + if (seriesPointStyle.visible) { + const geometryStateStyle = getGeometryStateStyle(seriesIdentifier, highlightedLegendItem, sharedStyle); + withContext(ctx, () => { + renderPoints(ctx, glyph.points, seriesPointStyle, geometryStateStyle); + }); + } + } + }); +} + +function renderArea( + ctx: CanvasRenderingContext2D, + glyph: AreaGeometry, + sharedStyle: SharedGeometryStateStyle, + highlightedLegendItem: LegendItem | null, + clippings: Rect, +) { + const { area, color, transform, seriesIdentifier, seriesAreaStyle, clippedRanges } = glyph; + const geometryStateStyle = getGeometryStateStyle(seriesIdentifier, highlightedLegendItem, sharedStyle); + const fill = buildAreaStyles(color, seriesAreaStyle, geometryStateStyle); + renderAreaPath(ctx, transform.x, area, fill, clippedRanges, clippings); +} +function renderAreaLines( + ctx: CanvasRenderingContext2D, + glyph: AreaGeometry, + sharedStyle: SharedGeometryStateStyle, + highlightedLegendItem: LegendItem | null, + clippings: Rect, +) { + const { lines, color, seriesIdentifier, transform, seriesAreaLineStyle, clippedRanges } = glyph; + const geometryStateStyle = getGeometryStateStyle(seriesIdentifier, highlightedLegendItem, sharedStyle); + const stroke = buildLineStyles(color, seriesAreaLineStyle, geometryStateStyle); + renderLinePaths(ctx, transform.x, lines, stroke, clippedRanges, clippings); +} diff --git a/src/chart_types/xy_chart/renderer/canvas/axes/index.ts b/src/chart_types/xy_chart/renderer/canvas/axes/index.ts new file mode 100644 index 0000000000..6d457607e8 --- /dev/null +++ b/src/chart_types/xy_chart/renderer/canvas/axes/index.ts @@ -0,0 +1,86 @@ +import { AxisTick, AxisTicksDimensions } from '../../../utils/axis_utils'; +import { AxisSpec } from '../../../utils/specs'; +import { AxisConfig } from '../../../../../utils/themes/theme'; +import { Dimensions } from '../../../../../utils/dimensions'; +import { AxisId } from '../../../../../utils/ids'; +import { getSpecsById } from '../../../state/utils'; +import { withContext } from '../../../../../renderers/canvas'; +import { renderDebugRect } from '../utils/debug'; +import { renderTitle } from './title'; +import { renderLine } from './line'; +import { renderTickLabel } from './tick_label'; +import { renderTick } from './tick'; + +export interface AxisProps { + axisConfig: AxisConfig; + axisSpec: AxisSpec; + axisTicksDimensions: AxisTicksDimensions; + axisPosition: Dimensions; + ticks: AxisTick[]; + debug: boolean; + chartDimensions: Dimensions; +} +export interface AxesProps { + axesVisibleTicks: Map; + axesSpecs: AxisSpec[]; + axesTicksDimensions: Map; + axesPositions: Map; + axisStyle: AxisConfig; + debug: boolean; + chartDimensions: Dimensions; +} + +export function renderAxes(ctx: CanvasRenderingContext2D, props: AxesProps) { + const { axesVisibleTicks, axesSpecs, axesTicksDimensions, axesPositions, axisStyle, debug, chartDimensions } = props; + axesVisibleTicks.forEach((ticks, axisId) => { + const axisSpec = getSpecsById(axesSpecs, axisId); + const axisTicksDimensions = axesTicksDimensions.get(axisId); + const axisPosition = axesPositions.get(axisId); + if (!ticks || !axisSpec || !axisTicksDimensions || !axisPosition) { + return; + } + renderAxis(ctx, { + axisSpec, + axisTicksDimensions, + axisPosition, + ticks, + axisConfig: axisStyle, + debug, + chartDimensions, + }); + }); +} + +function renderAxis(ctx: CanvasRenderingContext2D, props: AxisProps) { + withContext(ctx, (ctx) => { + const { ticks, axisPosition, debug } = props; + ctx.translate(axisPosition.left, axisPosition.top); + if (debug) { + renderDebugRect(ctx, { + x: 0, + y: 0, + width: axisPosition.width, + height: axisPosition.height, + }); + } + + withContext(ctx, (ctx) => { + renderLine(ctx, props); + }); + withContext(ctx, (ctx) => { + ticks.map((tick) => { + renderTick(ctx, tick, props); + }); + }); + withContext(ctx, (ctx) => { + ticks + .filter((tick) => tick.label !== null) + .map((tick) => { + renderTickLabel(ctx, tick, props); + }); + }); + withContext(ctx, (ctx) => { + renderTitle(ctx, props); + }); + }); +} diff --git a/src/chart_types/xy_chart/renderer/canvas/axes/line.ts b/src/chart_types/xy_chart/renderer/canvas/axes/line.ts new file mode 100644 index 0000000000..0e285a572e --- /dev/null +++ b/src/chart_types/xy_chart/renderer/canvas/axes/line.ts @@ -0,0 +1,29 @@ +import { isVerticalAxis } from '../../../utils/axis_utils'; +import { AxisProps } from '.'; +import { Position } from '../../../utils/specs'; + +export function renderLine(ctx: CanvasRenderingContext2D, props: AxisProps) { + const { + axisSpec: { position }, + axisPosition, + axisConfig: { axisLineStyle }, + } = props; + const lineProps: number[] = []; + if (isVerticalAxis(position)) { + lineProps[0] = position === Position.Left ? axisPosition.width : 0; + lineProps[2] = position === Position.Left ? axisPosition.width : 0; + lineProps[1] = 0; + lineProps[3] = axisPosition.height; + } else { + lineProps[0] = 0; + lineProps[2] = axisPosition.width; + lineProps[1] = position === Position.Top ? axisPosition.height : 0; + lineProps[3] = position === Position.Top ? axisPosition.height : 0; + } + ctx.beginPath(); + ctx.moveTo(lineProps[0], lineProps[1]); + ctx.lineTo(lineProps[2], lineProps[3]); + ctx.strokeStyle = axisLineStyle.stroke; + ctx.lineWidth = axisLineStyle.strokeWidth; + ctx.stroke(); +} diff --git a/src/chart_types/xy_chart/renderer/canvas/axes/tick.ts b/src/chart_types/xy_chart/renderer/canvas/axes/tick.ts new file mode 100644 index 0000000000..6feeb4a133 --- /dev/null +++ b/src/chart_types/xy_chart/renderer/canvas/axes/tick.ts @@ -0,0 +1,65 @@ +import { AxisTick, isVerticalAxis } from '../../../utils/axis_utils'; +import { AxisProps } from '.'; +import { Position } from '../../../utils/specs'; +import { TickStyle } from '../../../../../utils/themes/theme'; +import { renderLine, MIN_STROKE_WIDTH } from '../primitives/line'; +import { stringToRGB } from '../../../../partition_chart/layout/utils/d3_utils'; + +export function renderTick(ctx: CanvasRenderingContext2D, tick: AxisTick, props: AxisProps) { + const { + axisSpec: { tickSize, position }, + axisPosition, + axisConfig: { tickLineStyle }, + } = props; + if (!tickLineStyle.visible || tickLineStyle.strokeWidth < MIN_STROKE_WIDTH) { + return; + } + if (isVerticalAxis(position)) { + renderVerticalTick(ctx, position, axisPosition.width, tickSize, tick.position, tickLineStyle); + } else { + renderHorizontalTick(ctx, position, axisPosition.height, tickSize, tick.position, tickLineStyle); + } +} + +function renderVerticalTick( + ctx: CanvasRenderingContext2D, + position: Position, + axisWidth: number, + tickSize: number, + tickPosition: number, + tickStyle: TickStyle, +) { + const isLeftAxis = position === Position.Left; + const x1 = isLeftAxis ? axisWidth : 0; + const x2 = isLeftAxis ? axisWidth - tickSize : tickSize; + renderLine( + ctx, + { x1, y1: tickPosition, x2, y2: tickPosition }, + { + color: stringToRGB(tickStyle.stroke), + width: tickStyle.strokeWidth, + }, + ); +} + +function renderHorizontalTick( + ctx: CanvasRenderingContext2D, + position: Position, + axisHeight: number, + tickSize: number, + tickPosition: number, + tickStyle: TickStyle, +) { + const isTopAxis = position === Position.Top; + const y1 = isTopAxis ? axisHeight - tickSize : 0; + const y2 = isTopAxis ? axisHeight : tickSize; + + renderLine( + ctx, + { x1: tickPosition, y1, x2: tickPosition, y2 }, + { + color: stringToRGB(tickStyle.stroke), + width: tickStyle.strokeWidth, + }, + ); +} diff --git a/src/chart_types/xy_chart/renderer/canvas/axes/tick_label.ts b/src/chart_types/xy_chart/renderer/canvas/axes/tick_label.ts new file mode 100644 index 0000000000..6665a20e52 --- /dev/null +++ b/src/chart_types/xy_chart/renderer/canvas/axes/tick_label.ts @@ -0,0 +1,86 @@ +import { AxisTick, getTickLabelProps } from '../../../utils/axis_utils'; +import { AxisProps } from '.'; +import { renderText } from '../primitives/text'; +import { renderDebugRectCenterRotated } from '../utils/debug'; +import { Font, FontStyle } from '../../../../partition_chart/layout/types/types'; +import { withContext } from '../../../../../renderers/canvas'; + +export function renderTickLabel(ctx: CanvasRenderingContext2D, tick: AxisTick, props: AxisProps) { + /** + * padding is already computed through width + * and bbox_calculator using tickLabelPadding + * set padding to 0 to avoid conflict + */ + const labelStyle = { + ...props.axisConfig.tickLabelStyle, + padding: 0, + }; + + const { + axisSpec: { tickSize, tickPadding, position }, + axisTicksDimensions, + axisPosition, + debug, + } = props; + + const tickLabelRotation = props.axisSpec.tickLabelRotation || 0; + + const tickLabelProps = getTickLabelProps( + tickLabelRotation, + tickSize, + tickPadding, + tick.position, + position, + axisPosition, + axisTicksDimensions, + ); + + const { maxLabelTextWidth, maxLabelTextHeight } = axisTicksDimensions; + + const { x, y, offsetX, offsetY, align, verticalAlign } = tickLabelProps; + + if (debug) { + renderDebugRectCenterRotated( + ctx, + { + x: x + offsetX, + y: y + offsetY, + }, + { + x: x + offsetX, + y: y + offsetY, + height: maxLabelTextHeight, + width: maxLabelTextWidth, + }, + undefined, + undefined, + tickLabelRotation, + ); + } + const font: Font = { + fontFamily: labelStyle.fontFamily, + fontStyle: labelStyle.fontStyle ? (labelStyle.fontStyle as FontStyle) : 'normal', + fontVariant: 'normal', + fontWeight: 'normal', + }; + withContext(ctx, (ctx) => { + const textOffsetX = tickLabelRotation === 0 ? 0 : offsetX; + const textOffsetY = tickLabelRotation === 0 ? 0 : offsetY; + renderText( + ctx, + { + x: x + textOffsetX, + y: y + textOffsetY, + }, + tick.label, + { + ...font, + fontSize: labelStyle.fontSize, + fill: labelStyle.fill, + align: align as CanvasTextAlign, + baseline: verticalAlign as CanvasTextBaseline, + }, + tickLabelRotation, + ); + }); +} diff --git a/src/chart_types/xy_chart/renderer/canvas/axes/title.ts b/src/chart_types/xy_chart/renderer/canvas/axes/title.ts new file mode 100644 index 0000000000..5787cdac48 --- /dev/null +++ b/src/chart_types/xy_chart/renderer/canvas/axes/title.ts @@ -0,0 +1,99 @@ +import { AxisProps } from '.'; +import { isHorizontalAxis } from '../../../utils/axis_utils'; +import { renderDebugRect } from '../utils/debug'; +import { renderText } from '../primitives/text'; +import { Position } from '../../../utils/specs'; +import { Font, FontStyle } from '../../../../partition_chart/layout/types/types'; + +export function renderTitle(ctx: CanvasRenderingContext2D, props: AxisProps) { + const { + axisSpec: { title, position }, + } = props; + if (!title) { + return null; + } + if (isHorizontalAxis(position)) { + return renderHorizontalTitle(ctx, props); + } + return renderVerticalTitle(ctx, props); +} + +function renderVerticalTitle(ctx: CanvasRenderingContext2D, props: AxisProps) { + const { + axisPosition: { height }, + axisSpec: { title, position, tickSize, tickPadding }, + axisTicksDimensions: { maxLabelTextWidth }, + axisConfig: { axisTitleStyle }, + debug, + } = props; + if (!title) { + return null; + } + const { padding, ...titleStyle } = axisTitleStyle; + const top = height; + const left = position === Position.Left ? 0 : tickSize + tickPadding + maxLabelTextWidth + padding; + + if (debug) { + renderDebugRect(ctx, { x: left, y: top, width: height, height: titleStyle.fontSize }, undefined, undefined, -90); + } + + const font: Font = { + fontFamily: titleStyle.fontFamily, + fontVariant: 'normal', + fontStyle: titleStyle.fontStyle ? (titleStyle.fontStyle as FontStyle) : 'normal', + fontWeight: 'normal', + }; + renderText( + ctx, + { + x: left + titleStyle.fontSize / 2, + y: top - height / 2, + }, + title, + { ...font, fill: titleStyle.fill, align: 'center', baseline: 'middle', fontSize: titleStyle.fontSize }, + -90, + ); +} +function renderHorizontalTitle(ctx: CanvasRenderingContext2D, props: AxisProps) { + const { + axisPosition: { width }, + axisSpec: { title, position, tickSize, tickPadding }, + axisTicksDimensions: { maxLabelBboxHeight }, + axisConfig: { + axisTitleStyle: { padding, ...titleStyle }, + }, + debug, + } = props; + + if (!title) { + return; + } + + const top = position === Position.Top ? 0 : maxLabelBboxHeight + tickPadding + tickSize + padding; + + const left = 0; + if (debug) { + renderDebugRect(ctx, { x: left, y: top, width, height: titleStyle.fontSize }); + } + const font: Font = { + fontFamily: titleStyle.fontFamily, + fontVariant: 'normal', + fontStyle: titleStyle.fontStyle ? (titleStyle.fontStyle as FontStyle) : 'normal', + fontWeight: 'normal', + }; + renderText( + ctx, + { + x: left + width / 2, + y: top + titleStyle.fontSize / 2, + }, + title, + { + ...font, + fill: titleStyle.fill, + align: 'center', + baseline: 'middle', + fontSize: titleStyle.fontSize, + }, + ); +} diff --git a/src/chart_types/xy_chart/renderer/canvas/axis.tsx b/src/chart_types/xy_chart/renderer/canvas/axis.tsx deleted file mode 100644 index cbf79e5b05..0000000000 --- a/src/chart_types/xy_chart/renderer/canvas/axis.tsx +++ /dev/null @@ -1,329 +0,0 @@ -import React from 'react'; -import { Group, Line, Rect, Text } from 'react-konva'; -import { deepEqual } from '../../../../utils/fast_deep_equal'; -import { - AxisTick, - AxisTicksDimensions, - centerRotationOrigin, - getHorizontalAxisTickLineProps, - getTickLabelProps, - getVerticalAxisTickLineProps, - isHorizontalAxis, - isVerticalAxis, -} from '../../utils/axis_utils'; -import { AxisSpec, Position } from '../../utils/specs'; -import { Theme } from '../../../../utils/themes/theme'; -import { Dimensions } from '../../../../utils/dimensions'; -import { connect } from 'react-redux'; -import { GlobalChartState } from '../../../../state/chart_state'; -import { computeAxisVisibleTicksSelector } from '../../state/selectors/compute_axis_visible_ticks'; -import { getAxisSpecsSelector } from '../../state/selectors/get_specs'; -import { AxisId } from '../../../../utils/ids'; -import { computeAxisTicksDimensionsSelector } from '../../state/selectors/compute_axis_ticks_dimensions'; -import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; -import { computeChartDimensionsSelector } from '../../state/selectors/compute_chart_dimensions'; -import { LIGHT_THEME } from '../../../../utils/themes/light_theme'; -import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; -import { getSpecsById } from '../../state/utils'; -import { ChartTypes } from '../../..'; - -interface AxisProps { - theme: Theme; - axisSpec: AxisSpec; - axisTicksDimensions: AxisTicksDimensions; - axisPosition: Dimensions; - ticks: AxisTick[]; - debug: boolean; - chartDimensions: Dimensions; -} - -export class Axis extends React.Component { - shouldComponentUpdate(nextProps: AxisProps) { - return !deepEqual(this.props, nextProps); - } - - private renderTickLabel = (tick: AxisTick, i: number) => { - /** - * padding is already computed through width - * and bbox_calculator using tickLabelPadding - * set padding to 0 to avoid conflict - */ - const labelStyle = { - ...this.props.theme.axes.tickLabelStyle, - padding: 0, - }; - - const { - axisSpec: { tickSize, tickPadding, position }, - axisTicksDimensions, - axisPosition, - debug, - } = this.props; - - const tickLabelRotation = this.props.axisSpec.tickLabelRotation || 0; - - const tickLabelProps = getTickLabelProps( - tickLabelRotation, - tickSize, - tickPadding, - tick.position, - position, - axisPosition, - axisTicksDimensions, - ); - - const { maxLabelTextWidth, maxLabelTextHeight } = axisTicksDimensions; - - const centeredRectProps = centerRotationOrigin(axisTicksDimensions, { - x: tickLabelProps.x, - y: tickLabelProps.y, - }); - - const textProps = { - width: maxLabelTextWidth, - height: maxLabelTextHeight, - rotation: tickLabelRotation, - ...tickLabelProps, - ...centeredRectProps, - }; - - return ( - - {debug && } - - - ); - }; - - private renderTickLine = (tick: AxisTick, i: number) => { - const { - axisSpec: { tickSize, position }, - axisPosition, - theme: { - axes: { tickLineStyle }, - }, - } = this.props; - - const lineProps = isVerticalAxis(position) - ? getVerticalAxisTickLineProps(position, axisPosition.width, tickSize, tick.position) - : getHorizontalAxisTickLineProps(position, axisPosition.height, tickSize, tick.position); - - return ; - }; - - private renderAxisLine = () => { - const { - axisSpec: { position }, - axisPosition, - theme: { - axes: { axisLineStyle }, - }, - } = this.props; - const lineProps: number[] = []; - if (isVerticalAxis(position)) { - lineProps[0] = position === Position.Left ? axisPosition.width : 0; - lineProps[2] = position === Position.Left ? axisPosition.width : 0; - lineProps[1] = 0; - lineProps[3] = axisPosition.height; - } else { - lineProps[0] = 0; - lineProps[2] = axisPosition.width; - lineProps[1] = position === Position.Top ? axisPosition.height : 0; - lineProps[3] = position === Position.Top ? axisPosition.height : 0; - } - return ; - }; - - private renderAxisTitle() { - const { - axisSpec: { title, position }, - } = this.props; - if (!title) { - return null; - } - if (isHorizontalAxis(position)) { - return this.renderHorizontalAxisTitle(); - } - return this.renderVerticalAxisTitle(); - } - private renderVerticalAxisTitle() { - const { - axisPosition: { height }, - axisSpec: { title, position, tickSize, tickPadding }, - axisTicksDimensions: { maxLabelBboxWidth }, - theme: { - axes: { axisTitleStyle }, - }, - debug, - } = this.props; - if (!title) { - return null; - } - const { padding, ...titleStyle } = axisTitleStyle; - const top = height; - const left = position === Position.Left ? 0 : tickSize + tickPadding + maxLabelBboxWidth + padding; - - return ( - - {debug && ( - - )} - - - ); - } - private renderHorizontalAxisTitle() { - const { - axisPosition: { width, height }, - axisSpec: { title, position, tickSize, tickPadding }, - axisTicksDimensions: { maxLabelBboxHeight }, - theme: { - axes: { - axisTitleStyle: { padding, ...titleStyle }, - }, - }, - debug, - } = this.props; - - if (!title) { - return; - } - - const top = position === Position.Top ? 0 : maxLabelBboxHeight + tickPadding + tickSize + padding; - - const left = 0; - return ( - - {debug && ( - - )} - - - ); - } - render() { - const { ticks, axisPosition, debug } = this.props; - return ( - - {debug && ( - - )} - {this.renderAxisLine()} - {ticks.map(this.renderTickLine)} - {ticks.filter((tick) => tick.label !== null).map(this.renderTickLabel)} - {this.renderAxisTitle()} - - ); - } -} - -interface AxesProps { - axesVisibleTicks: Map; - axesSpecs: AxisSpec[]; - axesTicksDimensions: Map; - axesPositions: Map; - theme: Theme; - debug: boolean; - chartDimensions: Dimensions; -} -class AxesComponent extends React.Component { - shouldComponentUpdate(nextProps: AxesProps) { - return !deepEqual(this.props, nextProps); - } - - render() { - const { - axesVisibleTicks, - axesSpecs, - axesTicksDimensions, - axesPositions, - theme, - debug, - chartDimensions, - } = this.props; - const axesComponents: JSX.Element[] = []; - axesVisibleTicks.forEach((axisTicks, axisId) => { - const axisSpec = getSpecsById(axesSpecs, axisId); - const axisTicksDimensions = axesTicksDimensions.get(axisId); - const axisPosition = axesPositions.get(axisId); - const ticks = axesVisibleTicks.get(axisId); - if (!ticks || !axisSpec || !axisTicksDimensions || !axisPosition) { - return; - } - axesComponents.push( - , - ); - }); - return axesComponents; - } -} - -const mapStateToProps = (state: GlobalChartState): AxesProps => { - // check for correct chartType required because live in a different - // redux context (see ReactiveChart render method) - if (!state.specsInitialized || state.chartType !== ChartTypes.XYAxis) { - return { - theme: LIGHT_THEME, - chartDimensions: { - width: 0, - left: 0, - top: 0, - height: 0, - }, - debug: false, - axesSpecs: [], - axesPositions: new Map(), - axesTicksDimensions: new Map(), - axesVisibleTicks: new Map(), - }; - } - const axisTickPositions = computeAxisVisibleTicksSelector(state); - return { - theme: getChartThemeSelector(state), - chartDimensions: computeChartDimensionsSelector(state).chartDimensions, - debug: getSettingsSpecSelector(state).debug, - axesPositions: axisTickPositions.axisPositions, - axesSpecs: getAxisSpecsSelector(state), - axesTicksDimensions: computeAxisTicksDimensionsSelector(state), - axesVisibleTicks: axisTickPositions.axisVisibleTicks, - }; -}; - -export const Axes = connect(mapStateToProps)(AxesComponent); diff --git a/src/chart_types/xy_chart/renderer/canvas/bar_geometries.tsx b/src/chart_types/xy_chart/renderer/canvas/bar_geometries.tsx deleted file mode 100644 index e7d957f906..0000000000 --- a/src/chart_types/xy_chart/renderer/canvas/bar_geometries.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import { Group as KonvaGroup } from 'konva/types/Group'; -import React from 'react'; -import { Group, Rect } from 'react-konva'; -import { animated, Spring } from 'react-spring/renderprops-konva.cjs'; -import { deepEqual } from '../../../../utils/fast_deep_equal'; -import { buildBarRenderProps, buildBarBorderRenderProps } from './utils/rendering_props_utils'; -import { BarGeometry } from '../../../../utils/geometry'; -import { LegendItem } from '../../../../chart_types/xy_chart/legend/legend'; -import { SharedGeometryStateStyle } from '../../../../utils/themes/theme'; -import { getGeometryStateStyle } from '../../rendering/rendering'; -import { Clippings } from './bar_values_utils'; - -interface BarGeometriesDataProps { - animated?: boolean; - bars: BarGeometry[]; - sharedStyle: SharedGeometryStateStyle; - highlightedLegendItem: LegendItem | null; - clippings: Clippings; -} -interface BarGeometriesDataState { - overBar?: BarGeometry; -} -export class BarGeometries extends React.Component { - static defaultProps: Partial = { - animated: false, - }; - private readonly barSeriesRef: React.RefObject = React.createRef(); - constructor(props: BarGeometriesDataProps) { - super(props); - this.barSeriesRef = React.createRef(); - this.state = { - overBar: undefined, - }; - } - - shouldComponentUpdate(nextProps: BarGeometriesDataProps, nextState: BarGeometriesDataState) { - return !deepEqual(this.props, nextProps) || !deepEqual(this.state, nextState); - } - - render() { - const { bars, clippings } = this.props; - return ( - - {this.renderBarGeoms(bars)} - - ); - } - - private renderBarGeoms = (bars: BarGeometry[]): JSX.Element[] => { - const { overBar } = this.state; - const { sharedStyle } = this.props; - return bars.map((bar, index) => { - const { x, y, width, height, color, seriesStyle } = bar; - - // Properties to determine if we need to highlight individual bars depending on hover state - const hasGeometryHover = overBar != null; - const hasHighlight = overBar === bar; - const individualHighlight = { - hasGeometryHover, - hasHighlight, - }; - - const geometryStyle = getGeometryStateStyle( - bar.seriesIdentifier, - this.props.highlightedLegendItem, - sharedStyle, - individualHighlight, - ); - const key = `bar-${index}`; - - if (this.props.animated) { - return ( - - - {(props: { y: number; height: number }) => { - const barPropsBorder = buildBarBorderRenderProps( - x, - props.y, - width, - props.height, - seriesStyle.rect, - seriesStyle.rectBorder, - geometryStyle, - ); - const barProps = buildBarRenderProps( - x, - props.y, - width, - props.height, - color, - seriesStyle.rect, - seriesStyle.rectBorder, - geometryStyle, - ); - - return ( - - - {barPropsBorder && } - - ); - }} - - - ); - } else { - const barPropsBorder = buildBarBorderRenderProps( - x, - y, - width, - height, - seriesStyle.rect, - seriesStyle.rectBorder, - geometryStyle, - ); - const barProps = buildBarRenderProps( - x, - y, - width, - height, - color, - seriesStyle.rect, - seriesStyle.rectBorder, - geometryStyle, - ); - - return ( - - - {barPropsBorder && } - - ); - } - }); - }; -} diff --git a/src/chart_types/xy_chart/renderer/canvas/bar_values.tsx b/src/chart_types/xy_chart/renderer/canvas/bar_values.tsx deleted file mode 100644 index 9d804f25a8..0000000000 --- a/src/chart_types/xy_chart/renderer/canvas/bar_values.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import React from 'react'; -import { Group, Rect, Text } from 'react-konva'; -import { deepEqual } from '../../../../utils/fast_deep_equal'; -import { Rotation } from '../../utils/specs'; -import { Theme } from '../../../../utils/themes/theme'; -import { Dimensions } from '../../../../utils/dimensions'; -import { BarGeometry } from '../../../../utils/geometry'; -import { buildBarValueProps } from './bar_values_utils'; -import { connect } from 'react-redux'; -import { GlobalChartState } from '../../../../state/chart_state'; -import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; -import { computeChartDimensionsSelector } from '../../state/selectors/compute_chart_dimensions'; -import { getChartRotationSelector } from '../../../../state/selectors/get_chart_rotation'; -import { computeSeriesGeometriesSelector } from '../../state/selectors/compute_series_geometries'; -import { LIGHT_THEME } from '../../../../utils/themes/light_theme'; -import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; -import { ChartTypes } from '../../..'; - -interface BarValuesProps { - theme: Theme; - chartDimensions: Dimensions; - chartRotation: Rotation; - debug: boolean; - bars: BarGeometry[]; -} - -export class BarValuesComponent extends React.Component { - shouldComponentUpdate(nextProps: BarValuesProps) { - return !deepEqual(this.props, nextProps); - } - - render() { - const { chartDimensions, bars } = this.props; - if (!bars) { - return; - } - - return ( - - {this.renderBarValues()} - - ); - } - - private renderBarValues = () => { - const { bars, debug, chartRotation, chartDimensions, theme } = this.props; - const displayValueStyle = theme.barSeriesStyle.displayValue; - - return bars.map((bar, index) => { - const { displayValue, x, y, height, width } = bar; - if (!displayValue) { - return; - } - - const key = `bar-value-${index}`; - const displayValueProps = buildBarValueProps({ - x, - y, - barHeight: height, - barWidth: width, - displayValueStyle, - displayValue, - chartRotation, - chartDimensions, - }); - - const debugProps = { - ...displayValueProps, - stroke: 'violet', - strokeWidth: 1, - fill: 'transparent', - }; - - return ( - - {debug && } - {displayValue && } - - ); - }); - }; -} - -const mapStateToProps = (state: GlobalChartState): BarValuesProps => { - // check for correct chartType required because live in a different - // redux context (see ReactiveChart render method) - if (!state.specsInitialized || state.chartType !== ChartTypes.XYAxis) { - return { - theme: LIGHT_THEME, - chartDimensions: { - width: 0, - left: 0, - top: 0, - height: 0, - }, - chartRotation: 0, - debug: false, - bars: [], - }; - } - const geometries = computeSeriesGeometriesSelector(state); - return { - theme: getChartThemeSelector(state), - chartDimensions: computeChartDimensionsSelector(state).chartDimensions, - chartRotation: getChartRotationSelector(state), - debug: getSettingsSpecSelector(state).debug, - bars: geometries.geometries.bars, - }; -}; - -export const BarValues = connect(mapStateToProps)(BarValuesComponent); diff --git a/src/chart_types/xy_chart/renderer/canvas/bar_values_utils.ts b/src/chart_types/xy_chart/renderer/canvas/bar_values_utils.ts deleted file mode 100644 index c553cd9c78..0000000000 --- a/src/chart_types/xy_chart/renderer/canvas/bar_values_utils.ts +++ /dev/null @@ -1,272 +0,0 @@ -import { Required } from 'utility-types'; -import { Rotation } from '../../utils/specs'; -import { Dimensions } from '../../../../utils/dimensions'; -import { DisplayValueStyle } from '../../../../utils/themes/theme'; -import { ContainerConfig } from 'konva/types/Container'; -import { ClippedRanges } from '../../../../utils/geometry'; - -export interface PointStyleProps { - radius: number; - stroke: string; - strokeWidth: number; - strokeEnabled: boolean; - fill: string; - opacity: number; -} - -export type Clippings = Required; - -export function rotateBarValueProps( - chartRotation: Rotation, - chartDimensions: Dimensions, - barDimensions: Dimensions, - displayValueDimensions: Dimensions, - displayValue: { - text: string; - width: number; - height: number; - isValueContainedInElement?: boolean; - }, - props: DisplayValueStyle & { - x: number; - y: number; - align: string; - verticalAlign: string; - text: string; - width: number; - height: number; - }, -) { - const chartWidth = chartDimensions.width; - const chartHeight = chartDimensions.height; - - const barWidth = barDimensions.width; - const barHeight = barDimensions.height; - const x = barDimensions.left; - const y = barDimensions.top; - - const displayValueWidth = displayValueDimensions.width; - const displayValueHeight = displayValueDimensions.height; - const displayValueX = displayValueDimensions.left; - const displayValueY = displayValueDimensions.top; - - const rotatedDisplayValueX = - displayValueHeight > barWidth - ? x - Math.abs(barWidth - displayValueHeight) / 2 - : x + Math.abs(barWidth - displayValueHeight) / 2; - - switch (chartRotation) { - case 0: - props.x = displayValueX; - props.y = displayValueY; - break; - case 180: - props.x = chartWidth - displayValueX - displayValueWidth; - props.y = chartHeight - displayValueY - displayValueHeight; - props.verticalAlign = 'bottom'; - break; - case 90: - props.x = - barHeight >= displayValueWidth ? chartWidth - displayValueY - displayValueWidth : chartWidth - displayValueY; - props.y = rotatedDisplayValueX; - props.verticalAlign = 'middle'; - - if (displayValue.isValueContainedInElement) { - props.x = chartWidth - y - barHeight; - props.y = x; - props.width = barHeight >= displayValueWidth ? barHeight : 0; - props.height = displayValue.height <= barWidth ? barWidth : 0; - props.align = 'right'; - } - break; - case -90: - props.x = barHeight >= displayValueWidth ? displayValueY : displayValueY - displayValueWidth; - props.y = chartHeight - rotatedDisplayValueX - displayValueHeight; - props.verticalAlign = 'middle'; - - if (displayValue.isValueContainedInElement) { - props.x = y; - props.y = chartHeight - x - barWidth; - props.width = barHeight >= displayValueWidth ? barHeight : 0; - props.height = displayValue.height <= barWidth ? barWidth : 0; - props.align = 'left'; - } - break; - } - - return props; -} - -export function buildBarValueProps({ - x, - y, - barHeight, - barWidth, - displayValueStyle, - displayValue, - chartRotation, - chartDimensions, -}: { - x: number; - y: number; - barHeight: number; - barWidth: number; - displayValueStyle: DisplayValueStyle; - displayValue: { - text: string; - width: number; - height: number; - hideClippedValue?: boolean; - isValueContainedInElement?: boolean; - }; - chartRotation: Rotation; - chartDimensions: Dimensions; -}): DisplayValueStyle & { - x: number; - y: number; - align: string; - text: string; - width: number; - height: number; -} { - const { padding } = displayValueStyle; - const elementHeight = displayValue.isValueContainedInElement ? barHeight : displayValue.height; - - const displayValueHeight = elementHeight + padding; - const displayValueWidth = displayValue.width + padding; - - const displayValueY = barHeight >= displayValueHeight ? y : y - displayValueHeight; - const displayValueX = - displayValueWidth > barWidth - ? x - Math.abs(barWidth - displayValueWidth) / 2 - : x + Math.abs(barWidth - displayValueWidth) / 2; - - const displayValueOffsetY = displayValueStyle.offsetY || 0; - const displayValueOffsetX = displayValueStyle.offsetX || 0; - - const baseProps = { - align: 'center', - verticalAlign: 'top', - ...displayValueStyle, - text: displayValue.text, - width: displayValueWidth, - height: displayValueHeight, - offsetY: displayValueOffsetY, - x: displayValueX, - y: displayValueY, - }; - - const barDimensions = { - width: barWidth, - height: barHeight, - left: x, - top: y, - }; - - const displayValueDimensions = { - width: displayValueWidth, - height: displayValueHeight, - left: displayValueX, - top: displayValueY, - }; - - const props = rotateBarValueProps( - chartRotation, - chartDimensions, - barDimensions, - displayValueDimensions, - displayValue, - baseProps, - ); - - const clip = getBarValueClipDimensions(displayValue, props, barHeight, chartRotation); - - const hideOverflow = isBarValueOverflow( - chartDimensions, - clip, - { x: props.x, y: props.y, offsetX: displayValueOffsetX, offsetY: displayValueOffsetY }, - displayValue.hideClippedValue, - ); - - if (hideOverflow) { - props.width = 0; - props.height = 0; - } - - return props; -} - -export function getBarValueClipDimensions( - displayValue: { width: number; height: number; isValueContainedInElement?: boolean }, - computedDimensions: { width: number; height: number }, - barHeight: number, - chartRotation: Rotation, -): { width: number; height: number; offsetX: number; offsetY: number } { - const height = displayValue.isValueContainedInElement ? displayValue.height : computedDimensions.height; - const width = displayValue.isValueContainedInElement ? displayValue.width : computedDimensions.width; - - const offsetY = chartRotation === 180 ? barHeight - displayValue.height : 0; - const offsetX = chartRotation === 90 ? barHeight - displayValue.width : 0; - - return { height, width, offsetX, offsetY }; -} - -export function isBarValueOverflow( - chartDimensions: Dimensions, - clip: { width: number; height: number; offsetX: number; offsetY: number }, - valuePosition: { x: number; y: number; offsetX: number; offsetY: number }, - hideClippedValue?: boolean, -): boolean { - const chartHeight = chartDimensions.height; - const chartWidth = chartDimensions.width; - - const isOverflowX = - valuePosition.x + clip.width - valuePosition.offsetX > chartWidth || - valuePosition.x + clip.offsetX - valuePosition.offsetX < 0; - const isOverflowY = - valuePosition.y + clip.height - valuePosition.offsetY > chartHeight || - valuePosition.y + clip.offsetY - valuePosition.offsetY < 0; - - return !!hideClippedValue && (isOverflowX || isOverflowY); -} - -/** - * Creates `clipFunc` for Konva paths that have clipped ranges - * - * @param clippedRanges ranges to be clipped from rendering - * @param clippings konva global clippings - * @param negate show, rather than exclude, only selected ranges - */ -export function clipRanges( - clippedRanges: ClippedRanges, - clippings: Clippings, - negate = false, -): (ctx: CanvasRenderingContext2D) => void { - const length = clippedRanges.length; - const { clipHeight, clipWidth } = clippings; - - if (negate) { - return (ctx) => { - clippedRanges.forEach(([x0, x1]) => { - ctx.rect(x0, 0, x1 - x0, clippings.clipHeight); - }); - }; - } - - return (ctx) => { - if (length > 0) { - ctx.rect(0, 0, clippedRanges[0][0], clipHeight); - const lastX = clippedRanges[length - 1][1]; - ctx.rect(lastX, 0, clipWidth - lastX, clipHeight); - } - - if (length > 1) { - for (let i = 1; i < length; i++) { - const [, x0] = clippedRanges[i - 1]; - const [x1] = clippedRanges[i]; - - ctx.rect(x0, 0, x1 - x0, clipHeight); - } - } - }; -} diff --git a/src/chart_types/xy_chart/renderer/canvas/bars.ts b/src/chart_types/xy_chart/renderer/canvas/bars.ts new file mode 100644 index 0000000000..91dc30f7ee --- /dev/null +++ b/src/chart_types/xy_chart/renderer/canvas/bars.ts @@ -0,0 +1,33 @@ +import { withContext, withClip } from '../../../../renderers/canvas'; +import { BarGeometry } from '../../../../utils/geometry'; +import { buildBarStyles } from './styles/bar'; +import { SharedGeometryStateStyle } from '../../../../utils/themes/theme'; +import { getGeometryStateStyle } from '../../rendering/rendering'; +import { LegendItem } from '../../legend/legend'; +import { renderRect } from './primitives/rect'; +import { Rect } from '../../../../geoms/types'; + +export function renderBars( + ctx: CanvasRenderingContext2D, + barGeometries: BarGeometry[], + sharedStyle: SharedGeometryStateStyle, + clippings: Rect, + highlightedLegendItem?: LegendItem, +) { + withContext(ctx, (ctx) => { + withClip(ctx, clippings, (ctx: CanvasRenderingContext2D) => { + // ctx.scale(1, -1); // D3 and Canvas2d use a left-handed coordinate system (+y = down) but the ViewModel uses +y = up, so we must locally invert Y + barGeometries.forEach((barGeometry) => { + const { x, y, width, height, color, seriesStyle } = barGeometry; + const geometryStateStyle = getGeometryStateStyle( + barGeometry.seriesIdentifier, + highlightedLegendItem || null, + sharedStyle, + ); + const { fill, stroke } = buildBarStyles(color, seriesStyle.rect, seriesStyle.rectBorder, geometryStateStyle); + const rect = { x, y, width, height }; + renderRect(ctx, rect, fill, stroke); + }); + }); + }); +} diff --git a/src/chart_types/xy_chart/renderer/canvas/grid.tsx b/src/chart_types/xy_chart/renderer/canvas/grid.tsx deleted file mode 100644 index c986c0b79a..0000000000 --- a/src/chart_types/xy_chart/renderer/canvas/grid.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import React from 'react'; -import { Group, Line } from 'react-konva'; -import { connect } from 'react-redux'; -import { deepEqual } from '../../../../utils/fast_deep_equal'; -import { AxisLinePosition, isVerticalGrid } from '../../utils/axis_utils'; -import { GridLineConfig, mergeGridLineConfigs, Theme } from '../../../../utils/themes/theme'; -import { Dimensions } from '../../../../utils/dimensions'; -import { AxisId } from '../../../../utils/ids'; -import { AxisSpec } from '../../../../chart_types/xy_chart/utils/specs'; -import { GlobalChartState } from '../../../../state/chart_state'; -import { computeChartDimensionsSelector } from '../../state/selectors/compute_chart_dimensions'; -import { getAxisSpecsSelector } from '../../state/selectors/get_specs'; -import { computeAxisVisibleTicksSelector } from '../../state/selectors/compute_axis_visible_ticks'; -import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; -import { LIGHT_THEME } from '../../../../utils/themes/light_theme'; -import { getSpecsById } from '../../state/utils'; -import { ChartTypes } from '../../..'; - -interface GridProps { - chartTheme: Theme; - axesGridLinesPositions: Map; - axesSpecs: AxisSpec[]; - chartDimensions: Dimensions; -} - -class GridComponent extends React.Component { - shouldComponentUpdate(nextProps: GridProps) { - return !deepEqual(this.props, nextProps); - } - - render() { - const { axesGridLinesPositions, axesSpecs, chartDimensions, chartTheme } = this.props; - const gridComponents: JSX.Element[] = []; - axesGridLinesPositions.forEach((axisGridLinesPositions, axisId) => { - const axisSpec = getSpecsById(axesSpecs, axisId); - if (axisSpec && axisGridLinesPositions.length > 0) { - const themeConfig = isVerticalGrid(axisSpec.position) - ? chartTheme.axes.gridLineStyle.vertical - : chartTheme.axes.gridLineStyle.horizontal; - - const axisSpecConfig = axisSpec.gridLineStyle; - const gridLineStyle = axisSpecConfig ? mergeGridLineConfigs(axisSpecConfig, themeConfig) : themeConfig; - gridComponents.push( - - - {axisGridLinesPositions.map((linePosition, index) => { - return this.renderGridLine(linePosition, index, gridLineStyle); - })} - - , - ); - } - }); - - return gridComponents; - } - private renderGridLine = (linePosition: AxisLinePosition, i: number, gridLineStyle?: GridLineConfig) => { - return ; - }; -} - -const mapStateToProps = (state: GlobalChartState): GridProps => { - // check for correct chartType required because leave in a different - // redux context (see ReactiveChart render method) - if (!state.specsInitialized || state.chartType !== ChartTypes.XYAxis) { - return { - chartTheme: LIGHT_THEME, - chartDimensions: { - width: 0, - left: 0, - top: 0, - height: 0, - }, - axesSpecs: [], - axesGridLinesPositions: new Map(), - }; - } - return { - chartTheme: getChartThemeSelector(state), - chartDimensions: computeChartDimensionsSelector(state).chartDimensions, - axesSpecs: getAxisSpecsSelector(state), - axesGridLinesPositions: computeAxisVisibleTicksSelector(state).axisGridLinesPositions, - }; -}; - -export const Grid = connect(mapStateToProps)(GridComponent); diff --git a/src/chart_types/xy_chart/renderer/canvas/grids.ts b/src/chart_types/xy_chart/renderer/canvas/grids.ts new file mode 100644 index 0000000000..fab1048637 --- /dev/null +++ b/src/chart_types/xy_chart/renderer/canvas/grids.ts @@ -0,0 +1,55 @@ +import { AxisLinePosition, isVerticalGrid } from '../../utils/axis_utils'; +import { mergeGridLineConfigs, Theme } from '../../../../utils/themes/theme'; +import { Dimensions } from '../../../../utils/dimensions'; +import { AxisId } from '../../../../utils/ids'; +import { AxisSpec } from '../../../../chart_types/xy_chart/utils/specs'; +import { getSpecsById } from '../../state/utils'; +import { renderMultiLine, MIN_STROKE_WIDTH } from './primitives/line'; +import { Line, Stroke } from '../../../../geoms/types'; +import { stringToRGB } from '../../../partition_chart/layout/utils/d3_utils'; +import { withContext } from '../../../../renderers/canvas'; + +interface GridProps { + chartTheme: Theme; + axesGridLinesPositions: Map; + axesSpecs: AxisSpec[]; + chartDimensions: Dimensions; +} + +export function renderGrids(ctx: CanvasRenderingContext2D, props: GridProps) { + const { axesGridLinesPositions, axesSpecs, chartDimensions, chartTheme } = props; + withContext(ctx, (ctx) => { + ctx.translate(chartDimensions.left, chartDimensions.top); + axesGridLinesPositions.forEach((axisGridLinesPositions, axisId) => { + const axisSpec = getSpecsById(axesSpecs, axisId); + if (axisSpec && axisGridLinesPositions.length > 0) { + const themeConfig = isVerticalGrid(axisSpec.position) + ? chartTheme.axes.gridLineStyle.vertical + : chartTheme.axes.gridLineStyle.horizontal; + + const axisSpecConfig = axisSpec.gridLineStyle; + const gridLineStyle = axisSpecConfig ? mergeGridLineConfigs(axisSpecConfig, themeConfig) : themeConfig; + if (!gridLineStyle.stroke || !gridLineStyle.strokeWidth || gridLineStyle.strokeWidth < MIN_STROKE_WIDTH) { + return; + } + const strokeColor = stringToRGB(gridLineStyle.stroke); + strokeColor.opacity = + gridLineStyle.opacity !== undefined ? strokeColor.opacity * gridLineStyle.opacity : strokeColor.opacity; + const stroke: Stroke = { + color: strokeColor, + width: gridLineStyle.strokeWidth, + dash: gridLineStyle.dash, + }; + const lines = axisGridLinesPositions.map((position) => { + return { + x1: position[0], + y1: position[1], + x2: position[2], + y2: position[3], + }; + }); + renderMultiLine(ctx, lines, stroke); + } + }); + }); +} diff --git a/src/chart_types/xy_chart/renderer/canvas/line_annotation.tsx b/src/chart_types/xy_chart/renderer/canvas/line_annotation.tsx deleted file mode 100644 index 34ce9405a7..0000000000 --- a/src/chart_types/xy_chart/renderer/canvas/line_annotation.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import { Group, Line } from 'react-konva'; -import { deepEqual } from '../../../../utils/fast_deep_equal'; -import { LineAnnotationStyle } from '../../../../utils/themes/theme'; -import { AnnotationLineProps } from '../../annotations/line_annotation_tooltip'; - -interface LineAnnotationProps { - lines: AnnotationLineProps[]; - lineStyle: LineAnnotationStyle; -} - -export class LineAnnotation extends React.Component { - shouldComponentUpdate(nextProps: LineAnnotationProps) { - return !deepEqual(this.props, nextProps); - } - - render() { - const { lines } = this.props; - - return {lines.map(this.renderAnnotationLine)}; - } - private renderAnnotationLine = (lineConfig: AnnotationLineProps, index: number) => { - const { line } = this.props.lineStyle; - const { - start: { x1, y1 }, - end: { x2, y2 }, - } = lineConfig.linePathPoints; - const lineProps = { - points: [x1, y1, x2, y2], - ...line, - }; - return ; - }; -} diff --git a/src/chart_types/xy_chart/renderer/canvas/line_geometries.tsx b/src/chart_types/xy_chart/renderer/canvas/line_geometries.tsx deleted file mode 100644 index a2f63da3ce..0000000000 --- a/src/chart_types/xy_chart/renderer/canvas/line_geometries.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import React from 'react'; -import { Group as KonvaGroup } from 'konva/types/Group'; -import { Circle, Group, Path } from 'react-konva'; -import { deepEqual } from '../../../../utils/fast_deep_equal'; -import { - buildLineRenderProps, - buildPointStyleProps, - PointStyleProps, - buildPointRenderProps, -} from './utils/rendering_props_utils'; -import { getSeriesIdentifierPrefixedKey, getGeometryStateStyle } from '../../rendering/rendering'; -import { mergePartial } from '../../../../utils/commons'; -import { LineGeometry, PointGeometry } from '../../../../utils/geometry'; -import { PointStyle, SharedGeometryStateStyle } from '../../../../utils/themes/theme'; -import { LegendItem } from '../../../../chart_types/xy_chart/legend/legend'; -import { Clippings, clipRanges } from './bar_values_utils'; - -interface LineGeometriesDataProps { - animated?: boolean; - lines: LineGeometry[]; - sharedStyle: SharedGeometryStateStyle; - highlightedLegendItem: LegendItem | null; - clippings: Clippings; -} - -export class LineGeometries extends React.Component { - static defaultProps: Partial = { - animated: false, - }; - private readonly barSeriesRef: React.RefObject = React.createRef(); - constructor(props: LineGeometriesDataProps) { - super(props); - this.barSeriesRef = React.createRef(); - this.state = { - overPoint: undefined, - }; - } - - shouldComponentUpdate(nextProps: LineGeometriesDataProps) { - return !deepEqual(this.props, nextProps); - } - - render() { - return ( - - {this.renderLineGeoms()} - - ); - } - - private mergePointPropsWithOverrides(props: PointStyleProps, overrides?: Partial): PointStyleProps { - if (!overrides) { - return props; - } - - return mergePartial(props, overrides); - } - - private renderPoints = ( - linePoints: PointGeometry[], - lineKey: string, - pointStyleProps: PointStyleProps, - ): JSX.Element[] => { - const linePointElements: JSX.Element[] = []; - linePoints.forEach((linePoint, pointIndex) => { - const { x, y, transform, styleOverrides } = linePoint; - const key = `line-point-${lineKey}-${pointIndex}`; - const pointStyle = this.mergePointPropsWithOverrides(pointStyleProps, styleOverrides); - const pointProps = buildPointRenderProps(transform.x + x, y, pointStyle); - linePointElements.push(); - }); - return linePointElements; - }; - - private renderLineGeoms = (): JSX.Element[] => { - const { lines, sharedStyle } = this.props; - - return lines.reduce((acc, line) => { - const { seriesLineStyle, seriesPointStyle, seriesIdentifier } = line; - const key = getSeriesIdentifierPrefixedKey(seriesIdentifier, 'line-'); - if (seriesLineStyle.visible) { - acc.push(this.getLineToRender(line, sharedStyle, key)); - } - - if (seriesPointStyle.visible) { - acc.push(...this.getPointToRender(line, sharedStyle, key)); - } - - return acc; - }, []); - }; - - getLineToRender(line: LineGeometry, sharedStyle: SharedGeometryStateStyle, key: string) { - const { clippings } = this.props; - const { line: linePath, color, transform, seriesIdentifier, seriesLineStyle, clippedRanges } = line; - const geometryStyle = getGeometryStateStyle(seriesIdentifier, this.props.highlightedLegendItem, sharedStyle); - - const lineProps = buildLineRenderProps(transform.x, linePath, color, seriesLineStyle, geometryStyle); - - if (clippedRanges.length > 0) { - return ( - - - - - - - - - ); - } - - return ( - - - - ); - } - - getPointToRender(line: LineGeometry, sharedStyle: SharedGeometryStateStyle, key: string) { - const { points, color, seriesIdentifier, seriesPointStyle } = line; - const geometryStyle = getGeometryStateStyle(seriesIdentifier, this.props.highlightedLegendItem, sharedStyle); - const pointStyleProps = buildPointStyleProps(color, seriesPointStyle, geometryStyle); - return this.renderPoints(points, key, pointStyleProps); - } -} diff --git a/src/chart_types/xy_chart/renderer/canvas/lines.ts b/src/chart_types/xy_chart/renderer/canvas/lines.ts new file mode 100644 index 0000000000..da0e8bf507 --- /dev/null +++ b/src/chart_types/xy_chart/renderer/canvas/lines.ts @@ -0,0 +1,52 @@ +import { getGeometryStateStyle } from '../../rendering/rendering'; +import { LineGeometry } from '../../../../utils/geometry'; +import { SharedGeometryStateStyle } from '../../../../utils/themes/theme'; +import { LegendItem } from '../../legend/legend'; +import { withContext } from '../../../../renderers/canvas'; +import { renderPoints } from './points'; +import { renderLinePaths } from './primitives/path'; +import { Rect } from '../../../../geoms/types'; +import { buildLineStyles } from './styles/line'; + +interface LineGeometriesDataProps { + animated?: boolean; + lines: LineGeometry[]; + sharedStyle: SharedGeometryStateStyle; + highlightedLegendItem: LegendItem | null; + clippings: Rect; +} +export function renderLines(ctx: CanvasRenderingContext2D, props: LineGeometriesDataProps) { + withContext(ctx, (ctx) => { + const { lines, sharedStyle, highlightedLegendItem, clippings } = props; + + lines.forEach((line) => { + const { seriesLineStyle, seriesPointStyle } = line; + + if (seriesLineStyle.visible) { + withContext(ctx, (ctx) => { + renderLine(ctx, line, highlightedLegendItem, sharedStyle, clippings); + }); + } + + if (seriesPointStyle.visible) { + withContext(ctx, (ctx) => { + const geometryStyle = getGeometryStateStyle(line.seriesIdentifier, highlightedLegendItem, sharedStyle); + renderPoints(ctx, line.points, line.seriesPointStyle, geometryStyle); + }); + } + }); + }); +} + +function renderLine( + ctx: CanvasRenderingContext2D, + line: LineGeometry, + highlightedLegendItem: LegendItem | null, + sharedStyle: SharedGeometryStateStyle, + clippings: Rect, +) { + const { color, transform, seriesIdentifier, seriesLineStyle, clippedRanges } = line; + const geometryStyle = getGeometryStateStyle(seriesIdentifier, highlightedLegendItem, sharedStyle); + const stroke = buildLineStyles(color, seriesLineStyle, geometryStyle); + renderLinePaths(ctx, transform.x, [line.line], stroke, clippedRanges, clippings); +} diff --git a/src/chart_types/xy_chart/renderer/canvas/points.ts b/src/chart_types/xy_chart/renderer/canvas/points.ts new file mode 100644 index 0000000000..9f6706d2f3 --- /dev/null +++ b/src/chart_types/xy_chart/renderer/canvas/points.ts @@ -0,0 +1,25 @@ +import { PointGeometry } from '../../../../utils/geometry'; +import { PointStyle, GeometryStateStyle } from '../../../../utils/themes/theme'; +import { renderCircle } from './primitives/arc'; +import { Circle } from '../../../../geoms/types'; +import { buildPointStyles } from './styles/point'; + +export function renderPoints( + ctx: CanvasRenderingContext2D, + points: PointGeometry[], + themeStyle: PointStyle, + geometryStateStyle: GeometryStateStyle, +) { + return points.map((point) => { + const { x, y, color, transform, styleOverrides } = point; + const { fill, stroke, radius } = buildPointStyles(color, themeStyle, geometryStateStyle, styleOverrides); + + const circle: Circle = { + x: x + transform.x, + y, + radius, + }; + + renderCircle(ctx, circle, fill, stroke); + }); +} diff --git a/src/chart_types/xy_chart/renderer/canvas/primitives/arc.ts b/src/chart_types/xy_chart/renderer/canvas/primitives/arc.ts new file mode 100644 index 0000000000..67bdd47a9f --- /dev/null +++ b/src/chart_types/xy_chart/renderer/canvas/primitives/arc.ts @@ -0,0 +1,44 @@ +import { withContext } from '../../../../../renderers/canvas'; +import { Circle, Stroke, Fill, Arc } from '../../../../../geoms/types'; +import { RGBtoString } from '../../../../partition_chart/layout/utils/d3_utils'; +import { MIN_STROKE_WIDTH } from './line'; + +export function renderCircle(ctx: CanvasRenderingContext2D, circle: Circle, fill?: Fill, stroke?: Stroke) { + if (!fill && !stroke) { + return; + } + renderArc( + ctx, + { + ...circle, + startAngle: 0, + endAngle: Math.PI * 2, + }, + fill, + stroke, + ); +} + +export function renderArc(ctx: CanvasRenderingContext2D, arc: Arc, fill?: Fill, stroke?: Stroke) { + if (!fill && !stroke) { + return; + } + withContext(ctx, (ctx) => { + const { x, y, radius, startAngle, endAngle } = arc; + ctx.translate(x, y); + ctx.beginPath(); + ctx.arc(0, 0, radius, startAngle, endAngle, false); + if (fill) { + ctx.fillStyle = RGBtoString(fill.color); + ctx.fill(); + } + if (stroke && stroke.width > MIN_STROKE_WIDTH) { + ctx.strokeStyle = RGBtoString(stroke.color); + ctx.lineWidth = stroke.width; + if (stroke.dash) { + ctx.setLineDash(stroke.dash); + } + ctx.stroke(); + } + }); +} diff --git a/src/chart_types/xy_chart/renderer/canvas/primitives/line.ts b/src/chart_types/xy_chart/renderer/canvas/primitives/line.ts new file mode 100644 index 0000000000..93e0afb4c6 --- /dev/null +++ b/src/chart_types/xy_chart/renderer/canvas/primitives/line.ts @@ -0,0 +1,45 @@ +import { Stroke, Line } from '../../../../../geoms/types'; +import { RGBtoString } from '../../../../partition_chart/layout/utils/d3_utils'; +import { withContext } from '../../../../../renderers/canvas'; + +// Canvas2d stroke ignores an exact zero line width +export const MIN_STROKE_WIDTH = 0.001; + +export function renderLine(ctx: CanvasRenderingContext2D, line: Line, stroke: Stroke) { + if (stroke.width < MIN_STROKE_WIDTH) { + return; + } + withContext(ctx, (ctx) => { + if (stroke.dash) { + ctx.setLineDash(stroke.dash); + } + const { x1, y1, x2, y2 } = line; + ctx.strokeStyle = RGBtoString(stroke.color); + ctx.lineWidth = stroke.width; + ctx.beginPath(); + ctx.moveTo(x1, y1); + ctx.lineTo(x2, y2); + ctx.stroke(); + }); +} + +export function renderMultiLine(ctx: CanvasRenderingContext2D, lines: Line[], stroke: Stroke) { + if (stroke.width < MIN_STROKE_WIDTH) { + return; + } + withContext(ctx, (ctx) => { + const lineLength = lines.length; + ctx.strokeStyle = RGBtoString(stroke.color); + ctx.lineWidth = stroke.width; + if (stroke.dash) { + ctx.setLineDash(stroke.dash); + } + ctx.beginPath(); + for (let i = 0; i < lineLength; i++) { + const { x1, y1, x2, y2 } = lines[i]; + ctx.moveTo(x1, y1); + ctx.lineTo(x2, y2); + } + ctx.stroke(); + }); +} diff --git a/src/chart_types/xy_chart/renderer/canvas/primitives/path.ts b/src/chart_types/xy_chart/renderer/canvas/primitives/path.ts new file mode 100644 index 0000000000..2d500fc116 --- /dev/null +++ b/src/chart_types/xy_chart/renderer/canvas/primitives/path.ts @@ -0,0 +1,87 @@ +import { ClippedRanges } from '../../../../../utils/geometry'; +import { withContext, withClipRanges } from '../../../../../renderers/canvas'; +import { RGBtoString } from '../../../../partition_chart/layout/utils/d3_utils'; +import { Rect, Stroke, Fill } from '../../../../../geoms/types'; +import { MIN_STROKE_WIDTH } from './line'; + +export function renderLinePaths( + ctx: CanvasRenderingContext2D, + transformX: number, + linePaths: Array, + stroke: Stroke, + clippedRanges: ClippedRanges, + clippings: Rect, +) { + ctx.translate(transformX, 0); + if (clippedRanges.length > 0) { + withClipRanges(ctx, clippedRanges, clippings, false, (ctx) => { + linePaths.map((path) => { + renderPathStroke(ctx, path, stroke); + }); + }); + withClipRanges(ctx, clippedRanges, clippings, true, (ctx) => { + linePaths.map((path) => { + renderPathStroke(ctx, path, { ...stroke, dash: [5, 5] }); + }); + }); + return; + } + + linePaths.map((path) => { + withContext(ctx, (ctx) => { + renderPathStroke(ctx, path, stroke); + }); + }); +} + +export function renderAreaPath( + ctx: CanvasRenderingContext2D, + transformX: number, + area: string, + fill: Fill, + clippedRanges: ClippedRanges, + clippings: Rect, +) { + if (clippedRanges.length > 0) { + withClipRanges(ctx, clippedRanges, clippings, false, (ctx) => { + ctx.translate(transformX, 0); + renderPathFill(ctx, area, fill); + }); + withClipRanges(ctx, clippedRanges, clippings, true, (ctx) => { + ctx.translate(transformX, 0); + const { opacity } = fill.color; + const color = { + ...fill.color, + opacity: opacity / 2, + }; + renderPathFill(ctx, area, { ...fill, color }); + }); + return; + } + withContext(ctx, (ctx) => { + ctx.translate(transformX, 0); + renderPathFill(ctx, area, fill); + }); +} + +function renderPathStroke(ctx: CanvasRenderingContext2D, path: string, stroke: Stroke) { + if (stroke.width < MIN_STROKE_WIDTH) { + return; + } + const path2d = new Path2D(path); + + ctx.strokeStyle = RGBtoString(stroke.color); + ctx.lineWidth = stroke.width; + if (stroke.dash) { + ctx.setLineDash(stroke.dash); + } + ctx.beginPath(); + ctx.stroke(path2d); +} + +function renderPathFill(ctx: CanvasRenderingContext2D, path: string, fill: Fill) { + const path2d = new Path2D(path); + ctx.fillStyle = RGBtoString(fill.color); + ctx.beginPath(); + ctx.fill(path2d); +} diff --git a/src/chart_types/xy_chart/renderer/canvas/primitives/rect.ts b/src/chart_types/xy_chart/renderer/canvas/primitives/rect.ts new file mode 100644 index 0000000000..5ff38bcb50 --- /dev/null +++ b/src/chart_types/xy_chart/renderer/canvas/primitives/rect.ts @@ -0,0 +1,84 @@ +import { Rect, Fill, Stroke } from '../../../../../geoms/types'; +import { RGBtoString } from '../../../../partition_chart/layout/utils/d3_utils'; + +export function renderRect( + ctx: CanvasRenderingContext2D, + rect: Rect, + fill?: Fill, + stroke?: Stroke, + disableBoardOffset: boolean = false, +) { + if (!fill && !stroke) { + return; + } + + // fill + + if (fill) { + const borderOffset = !disableBoardOffset && stroke && stroke.width > 0.001 ? stroke.width : 0; + // console.log(stroke, borderOffset); + const x = rect.x + borderOffset; + const y = rect.y + borderOffset; + const width = rect.width - borderOffset * 2; + const height = rect.height - borderOffset * 2; + drawRect(ctx, { x, y, width, height }); + ctx.fillStyle = RGBtoString(fill.color); + ctx.fill(); + } + + if (stroke && stroke.width > 0.001) { + const borderOffset = !disableBoardOffset && stroke && stroke.width > 0.001 ? stroke.width / 2 : 0; + // console.log(stroke, borderOffset); + const x = rect.x + borderOffset; + const y = rect.y + borderOffset; + const width = rect.width - borderOffset * 2; + const height = rect.height - borderOffset * 2; + + ctx.strokeStyle = RGBtoString(stroke.color); + ctx.lineWidth = stroke.width; + drawRect(ctx, { x, y, width, height }); + if (stroke.dash) { + ctx.setLineDash(stroke.dash); + } + + ctx.stroke(); + } +} + +function drawRect(ctx: CanvasRenderingContext2D, rect: Rect) { + const { x, y, width, height } = rect; + ctx.beginPath(); + ctx.moveTo(x, y); + ctx.lineTo(x + width, y); + ctx.lineTo(x + width, y + height); + ctx.lineTo(x, y + height); + ctx.lineTo(x, y); +} + +export function renderMultiRect(ctx: CanvasRenderingContext2D, rects: Rect[], fill?: Fill, stroke?: Stroke) { + if (!fill && !stroke && rects.length > 0) { + return; + } + + const rectsLength = rects.length; + ctx.beginPath(); + for (let i = 0; i < rectsLength; i++) { + const { width, height, x, y } = rects[i]; + ctx.moveTo(x, y); + ctx.lineTo(x + width, y); + ctx.lineTo(x + width, y + height!); + ctx.lineTo(x, y + height); + ctx.lineTo(x, y); + } + + if (fill) { + ctx.fillStyle = RGBtoString(fill.color); + ctx.fill(); + } + if (stroke && stroke.width > 0.001) { + // Canvas2d stroke ignores an exact zero line width + ctx.strokeStyle = RGBtoString(stroke.color); + ctx.lineWidth = stroke.width; + ctx.stroke(); + } +} diff --git a/src/chart_types/xy_chart/renderer/canvas/primitives/text.ts b/src/chart_types/xy_chart/renderer/canvas/primitives/text.ts new file mode 100644 index 0000000000..aae8d7ce2e --- /dev/null +++ b/src/chart_types/xy_chart/renderer/canvas/primitives/text.ts @@ -0,0 +1,142 @@ +import { withContext, withRotatedOrigin } from '../../../../../renderers/canvas'; +import { Font, TextAlign, TextBaseline } from '../../../../partition_chart/layout/types/types'; +import { cssFontShorthand, measureText } from '../../../../partition_chart/layout/utils/measure'; +import { Point } from '../../../../../utils/point'; + +export function renderText( + ctx: CanvasRenderingContext2D, + origin: Point, + text: string, + font: Font & { fill: string; fontSize: number; align: TextAlign; baseline: TextBaseline }, + degree: number = 0, +) { + if (text === undefined || text === null) { + return; + } + withRotatedOrigin(ctx, origin, degree, (ctx) => { + withContext(ctx, (ctx) => { + ctx.fillStyle = font.fill; + ctx.textAlign = font.align; + ctx.textBaseline = font.baseline; + ctx.font = cssFontShorthand(font, font.fontSize); + ctx.fillText(text, origin.x, origin.y); + }); + }); +} + +const SPACE = ' '; +const ELLIPSIS = '…'; +const DASH = '-'; + +export function wrapLines( + ctx: CanvasRenderingContext2D, + text: string, + font: Font, + fontSize: number, + fixedWidth: number, + fixedHeight: number, +) { + const lineHeight = 1; + const lines = text.split('\n'); + let textWidth = 0; + const lineHeightPx = lineHeight * fontSize; + + const padding = 0; + const maxWidth = fixedWidth - padding * 2; + const maxHeightPx = fixedHeight - padding * 2; + let currentHeightPx = 0; + const shouldWrap = true; + const wrapAtWord = true; + const shouldAddEllipsis = false; + const textArr: string[] = []; + const textMeasureProcessor = measureText(ctx); + const getTextWidth = (text: string) => { + const measuredText = textMeasureProcessor(fontSize, [ + { + text, + ...font, + }, + ]); + const measure = measuredText[0]; + if (measure) { + return measure.width; + } + return 0; + }; + + const additionalWidth = 0; + + for (let i = 0, max = lines.length; i < max; ++i) { + let line = lines[i]; + let lineWidth = getTextWidth(line); + if (fixedWidth && lineWidth > maxWidth) { + while (line.length > 0) { + let low = 0, + high = line.length, + match = '', + matchWidth = 0; + while (low < high) { + const mid = (low + high) >>> 1, + substr = line.slice(0, mid + 1), + substrWidth = getTextWidth(substr) + additionalWidth; + if (substrWidth <= maxWidth) { + low = mid + 1; + match = substr + (shouldAddEllipsis ? ELLIPSIS : ''); + matchWidth = substrWidth; + } else { + high = mid; + } + } + if (match) { + if (wrapAtWord) { + let wrapIndex; + const nextChar = line[match.length]; + const nextIsSpaceOrDash = nextChar === SPACE || nextChar === DASH; + if (nextIsSpaceOrDash && matchWidth <= maxWidth) { + wrapIndex = match.length; + } else { + wrapIndex = Math.max(match.lastIndexOf(SPACE), match.lastIndexOf(DASH)) + 1; + } + if (wrapIndex > 0) { + low = wrapIndex; + match = match.slice(0, low); + matchWidth = getTextWidth(match); + } + } + match = match.trimRight(); + textArr.push(match); + textWidth = Math.max(textWidth, matchWidth); + currentHeightPx += lineHeightPx; + if (!shouldWrap || (fixedHeight && currentHeightPx + lineHeightPx > maxHeightPx)) { + break; + } + line = line.slice(low); + line = line.trimLeft(); + if (line.length > 0) { + lineWidth = getTextWidth(line); + if (lineWidth <= maxWidth) { + textArr.push(line); + currentHeightPx += lineHeightPx; + textWidth = Math.max(textWidth, lineWidth); + break; + } + } + } else { + break; + } + } + } else { + textArr.push(line); + currentHeightPx += lineHeightPx; + textWidth = Math.max(textWidth, lineWidth); + } + if (fixedHeight && currentHeightPx + lineHeightPx > maxHeightPx) { + break; + } + } + return { + lines: textArr, + height: fontSize, + width: textWidth, + }; +} diff --git a/src/chart_types/xy_chart/renderer/canvas/reactive_chart.tsx b/src/chart_types/xy_chart/renderer/canvas/reactive_chart.tsx deleted file mode 100644 index 63d727d204..0000000000 --- a/src/chart_types/xy_chart/renderer/canvas/reactive_chart.tsx +++ /dev/null @@ -1,357 +0,0 @@ -import React, { RefObject } from 'react'; -import { bindActionCreators, Dispatch } from 'redux'; -import { connect, ReactReduxContext, Provider } from 'react-redux'; -import { Layer, Rect, Stage } from 'react-konva'; -import { AreaGeometries } from './area_geometries'; -import { BarGeometries } from './bar_geometries'; -import { LineGeometries } from './line_geometries'; -import { LineAnnotation } from './line_annotation'; -import { RectAnnotation } from './rect_annotation'; -import { Grid } from './grid'; -import { Axes } from './axis'; -import { BarValues } from './bar_values'; -import { AnnotationDimensions } from '../../annotations/annotation_utils'; -import { AnnotationLineProps } from '../../annotations/line_annotation_tooltip'; -import { AnnotationRectProps } from '../../annotations/rect_annotation_tooltip'; -import { computeAnnotationDimensionsSelector } from '../../state/selectors/compute_annotations'; -import { computeChartTransformSelector } from '../../state/selectors/compute_chart_transform'; -import { getAnnotationSpecsSelector } from '../../state/selectors/get_specs'; -import { getHighlightedSeriesSelector } from '../../state/selectors/get_highlighted_series'; -import { isChartEmptySelector } from '../../state/selectors/is_chart_empty'; -import { isChartAnimatableSelector } from '../../state/selectors/is_chart_animatable'; -import { isBrushAvailableSelector } from '../../state/selectors/is_brush_available'; -import { Transform, getSpecsById, Geometries } from '../../state/utils'; -import { Rotation, AnnotationSpec, isLineAnnotation, isRectAnnotation } from '../../utils/specs'; -import { onChartRendered } from '../../../../state/actions/chart'; -import { isInitialized } from '../../../../state/selectors/is_initialized'; -import { getChartRotationSelector } from '../../../../state/selectors/get_chart_rotation'; -import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; -import { GlobalChartState } from '../../../../state/chart_state'; -import { Dimensions } from '../../../../utils/dimensions'; -import { AnnotationId } from '../../../../utils/ids'; -import { Theme, mergeWithDefaultAnnotationLine, mergeWithDefaultAnnotationRect } from '../../../../utils/themes/theme'; -import { LIGHT_THEME } from '../../../../utils/themes/light_theme'; -import { computeSeriesGeometriesSelector } from '../../state/selectors/compute_series_geometries'; -import { LegendItem } from '../../../../chart_types/xy_chart/legend/legend'; -import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; -import { computeChartDimensionsSelector } from '../../state/selectors/compute_chart_dimensions'; -import { getChartContainerDimensionsSelector } from '../../../../state/selectors/get_chart_container_dimensions'; -import { Clippings } from './bar_values_utils'; -import { ChartTypes } from '../../..'; - -interface ReactiveChartStateProps { - initialized: boolean; - geometries: Geometries; - debug: boolean; - chartContainerDimensions: Dimensions; - chartRotation: Rotation; - chartDimensions: Dimensions; - chartTransform: Transform; - theme: Theme; - isChartAnimatable: boolean; - isChartEmpty: boolean; - annotationDimensions: Map; - annotationSpecs: AnnotationSpec[]; - isBrushAvailable: boolean; - highlightedLegendItem?: LegendItem; -} -interface ReactiveChartDispatchProps { - onChartRendered: typeof onChartRendered; -} -interface ReactiveChartOwnProps { - forwardStageRef: RefObject; -} -interface ReactiveChartElementIndex { - element: JSX.Element; - zIndex: number; -} - -type ReactiveChartProps = ReactiveChartOwnProps & ReactiveChartStateProps & ReactiveChartDispatchProps; -class Chart extends React.Component { - static displayName = 'ReactiveChart'; - firstRender = true; - - componentDidUpdate() { - if (this.props.initialized) { - this.props.onChartRendered(); - } - } - renderBarSeries = (clippings: Clippings): ReactiveChartElementIndex[] => { - const { geometries, theme, isChartAnimatable, highlightedLegendItem } = this.props; - if (geometries.bars.length === 0) { - return []; - } - - const element = ( - - ); - - return [ - { - element, - zIndex: 0, - }, - ]; - }; - - renderLineSeries = (clippings: Clippings): ReactiveChartElementIndex[] => { - const { geometries, theme, isChartAnimatable, highlightedLegendItem } = this.props; - if (geometries.lines.length === 0) { - return []; - } - - const element = ( - - ); - - return [ - { - element, - zIndex: 0, - }, - ]; - }; - - renderAreaSeries = (clippings: Clippings): ReactiveChartElementIndex[] => { - const { geometries, theme, isChartAnimatable, highlightedLegendItem } = this.props; - if (geometries.areas.length === 0) { - return []; - } - const element = ( - - ); - - return [ - { - element, - zIndex: 0, - }, - ]; - }; - - renderAnnotations = (): ReactiveChartElementIndex[] => { - const { annotationDimensions, annotationSpecs } = this.props; - const annotationElements: ReactiveChartElementIndex[] = []; - - annotationDimensions.forEach((annotation: AnnotationDimensions, id: AnnotationId) => { - const spec = getSpecsById(annotationSpecs, id); - - if (!spec) { - return; - } - - if (isLineAnnotation(spec)) { - const lineStyle = mergeWithDefaultAnnotationLine(spec.style); - const element = ( - - ); - annotationElements.push({ - element, - zIndex: spec.zIndex || 0, - }); - } else if (isRectAnnotation(spec)) { - const rectStyle = mergeWithDefaultAnnotationRect(spec.style); - const element = ( - - ); - annotationElements.push({ - element, - zIndex: spec.zIndex || 0, - }); - } - }); - return annotationElements; - }; - - sortAndRenderElements() { - const { chartDimensions, chartRotation } = this.props; - const clippings = { - clipX: 0, - clipY: 0, - clipWidth: [90, -90].includes(chartRotation) ? chartDimensions.height : chartDimensions.width, - clipHeight: [90, -90].includes(chartRotation) ? chartDimensions.width : chartDimensions.height, - }; - - const bars = this.renderBarSeries(clippings); - const areas = this.renderAreaSeries(clippings); - const lines = this.renderLineSeries(clippings); - const annotations: ReactiveChartElementIndex[] = this.renderAnnotations(); - return [...bars, ...areas, ...lines, ...annotations] - .sort((elemIdxA, elemIdxB) => elemIdxA.zIndex - elemIdxB.zIndex) - .map((elemIdx) => elemIdx.element); - } - - render() { - const { initialized, chartRotation, chartDimensions, isChartEmpty, debug, chartContainerDimensions } = this.props; - if (!initialized || chartDimensions.width === 0 || chartDimensions.height === 0) { - return null; - } - const { chartTransform } = this.props; - - if (isChartEmpty) { - return ( -
-

No data to display

-
- ); - } - const brushProps = {}; - return ( - - {({ store }) => { - return ( - - - - - - - - - {this.sortAndRenderElements()} - - - - - - - {debug && ( - - {this.renderDebugChartBorders()} - - )} - - ); - }} - - ); - } - - private renderDebugChartBorders = () => { - const { chartDimensions } = this.props; - return ( - - ); - }; -} - -const mapDispatchToProps = (dispatch: Dispatch): ReactiveChartDispatchProps => - bindActionCreators( - { - onChartRendered, - }, - dispatch, - ); - -const DEFAULT_PROPS: ReactiveChartStateProps = { - initialized: false, - theme: LIGHT_THEME, - geometries: { - areas: [], - bars: [], - lines: [], - points: [], - }, - debug: false, - chartContainerDimensions: { - width: 0, - height: 0, - left: 0, - top: 0, - }, - chartRotation: 0 as 0, - chartDimensions: { - width: 0, - height: 0, - left: 0, - top: 0, - }, - chartTransform: { - x: 0, - y: 0, - rotate: 0, - }, - isChartAnimatable: false, - isChartEmpty: true, - annotationDimensions: new Map(), - annotationSpecs: [], - isBrushAvailable: false, - highlightedLegendItem: undefined, -}; - -const mapStateToProps = (state: GlobalChartState): ReactiveChartStateProps => { - if (!isInitialized(state) || state.chartType !== ChartTypes.XYAxis) { - return DEFAULT_PROPS; - } - return { - initialized: true, - theme: getChartThemeSelector(state), - geometries: computeSeriesGeometriesSelector(state).geometries, - chartContainerDimensions: getChartContainerDimensionsSelector(state), - debug: getSettingsSpecSelector(state).debug, - chartRotation: getChartRotationSelector(state), - chartDimensions: computeChartDimensionsSelector(state).chartDimensions, - chartTransform: computeChartTransformSelector(state), - isChartAnimatable: isChartAnimatableSelector(state), - isChartEmpty: isChartEmptySelector(state), - annotationDimensions: computeAnnotationDimensionsSelector(state), - annotationSpecs: getAnnotationSpecsSelector(state), - isBrushAvailable: isBrushAvailableSelector(state), - highlightedLegendItem: getHighlightedSeriesSelector(state), - }; -}; - -export const ReactiveChart = connect(mapStateToProps, mapDispatchToProps)(Chart); diff --git a/src/chart_types/xy_chart/renderer/canvas/rect_annotation.tsx b/src/chart_types/xy_chart/renderer/canvas/rect_annotation.tsx deleted file mode 100644 index 13c32faab2..0000000000 --- a/src/chart_types/xy_chart/renderer/canvas/rect_annotation.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import { Group, Rect } from 'react-konva'; -import { deepEqual } from '../../../../utils/fast_deep_equal'; -import { RectAnnotationStyle } from '../../../../utils/themes/theme'; -import { AnnotationRectProps } from '../../annotations/rect_annotation_tooltip'; - -interface Props { - rects: AnnotationRectProps[]; - rectStyle: RectAnnotationStyle; -} - -export class RectAnnotation extends React.Component { - shouldComponentUpdate(nextProps: Props) { - return !deepEqual(this.props, nextProps); - } - - render() { - const { rects } = this.props; - return {rects.map(this.renderAnnotationRect)}; - } - private renderAnnotationRect = ({ rect }: AnnotationRectProps, index: number) => { - const { x, y, width, height } = rect; - - const rectProps = { - ...this.props.rectStyle, - x, - y, - width, - height, - }; - - return ; - }; -} diff --git a/src/chart_types/xy_chart/renderer/canvas/renderers.ts b/src/chart_types/xy_chart/renderer/canvas/renderers.ts new file mode 100644 index 0000000000..eb883f4c47 --- /dev/null +++ b/src/chart_types/xy_chart/renderer/canvas/renderers.ts @@ -0,0 +1,171 @@ +import { withContext, renderLayers, clearCanvas } from '../../../../renderers/canvas'; +import { renderBars } from './bars'; +import { renderAreas } from './areas'; +import { renderLines } from './lines'; +import { renderAxes } from './axes'; +import { renderGrids } from './grids'; +import { ReactiveChartStateProps } from './xy_chart'; +import { renderAnnotations } from './annotations'; +import { renderBarValues } from './values/bar'; +import { renderDebugRect } from './utils/debug'; +import { stringToRGB } from '../../../partition_chart/layout/utils/d3_utils'; +import { Rect } from '../../../../geoms/types'; + +export function renderXYChartCanvas2d( + ctx: CanvasRenderingContext2D, + dpr: number, + clippings: Rect, + props: ReactiveChartStateProps, +) { + withContext(ctx, (ctx) => { + // let's set the devicePixelRatio once and for all; then we'll never worry about it again + ctx.scale(dpr, dpr); + const { + chartDimensions, + chartTransform, + chartRotation, + geometries, + theme, + highlightedLegendItem, + annotationDimensions, + annotationSpecs, + axisTickPositions, + axesSpecs, + axesTicksDimensions, + axesGridLinesPositions, + debug, + } = props; + const transform = { + x: chartDimensions.left + chartTransform.x, + y: chartDimensions.top + chartTransform.y, + }; + // painter's algorithm, like that of SVG: the sequence determines what overdraws what; first element of the array is drawn first + // (of course, with SVG, it's for ambiguous situations only, eg. when 3D transforms with different Z values aren't used, but + // unlike SVG and esp. WebGL, Canvas2d doesn't support the 3rd dimension well, see ctx.transform / ctx.setTransform). + // The layers are callbacks, because of the need to not bake in the `ctx`, it feels more composable and uncoupled this way. + renderLayers(ctx, [ + // clear the canvas + (ctx: CanvasRenderingContext2D) => clearCanvas(ctx, 200000, 200000 /*, backgroundColor*/), + + (ctx: CanvasRenderingContext2D) => { + renderAxes(ctx, { + axesPositions: axisTickPositions.axisPositions, + axesSpecs, + axesTicksDimensions, + axesVisibleTicks: axisTickPositions.axisVisibleTicks, + chartDimensions, + debug, + axisStyle: theme.axes, + }); + }, + (ctx: CanvasRenderingContext2D) => { + renderGrids(ctx, { + axesSpecs, + chartDimensions, + axesGridLinesPositions, + chartTheme: theme, + }); + }, + // rendering background annotations + (ctx: CanvasRenderingContext2D) => { + withContext(ctx, (ctx) => { + ctx.translate(transform.x, transform.y); + ctx.rotate((chartRotation * Math.PI) / 180); + renderAnnotations( + ctx, + { + annotationDimensions, + annotationSpecs, + }, + true, + ); + }); + }, + + // rendering bars/areas/lines + (ctx: CanvasRenderingContext2D) => { + withContext(ctx, (ctx) => { + ctx.translate(transform.x, transform.y); + ctx.rotate((chartRotation * Math.PI) / 180); + renderBars(ctx, geometries.bars, theme.sharedStyle, clippings, highlightedLegendItem); + }); + }, + (ctx: CanvasRenderingContext2D) => { + withContext(ctx, (ctx) => { + ctx.translate(transform.x, transform.y); + ctx.rotate((chartRotation * Math.PI) / 180); + renderAreas(ctx, { + areas: geometries.areas, + clippings, + highlightedLegendItem: highlightedLegendItem || null, + sharedStyle: theme.sharedStyle, + }); + }); + }, + (ctx: CanvasRenderingContext2D) => { + withContext(ctx, (ctx) => { + ctx.translate(transform.x, transform.y); + ctx.rotate((chartRotation * Math.PI) / 180); + renderLines(ctx, { + lines: geometries.lines, + clippings, + highlightedLegendItem: highlightedLegendItem || null, + sharedStyle: theme.sharedStyle, + }); + }); + }, + (ctx: CanvasRenderingContext2D) => { + withContext(ctx, (ctx) => { + ctx.translate(transform.x, transform.y); + ctx.rotate((chartRotation * Math.PI) / 180); + renderBarValues(ctx, { + bars: geometries.bars, + chartDimensions, + chartRotation, + debug, + theme, + }); + }); + }, + // rendering foreground annotations + (ctx: CanvasRenderingContext2D) => { + withContext(ctx, (ctx) => { + ctx.translate(transform.x, transform.y); + ctx.rotate((chartRotation * Math.PI) / 180); + renderAnnotations( + ctx, + { + annotationDimensions, + annotationSpecs, + }, + false, + ); + }); + }, + (ctx: CanvasRenderingContext2D) => { + if (!debug) { + return; + } + withContext(ctx, (ctx) => { + renderDebugRect( + ctx, + { + x: chartDimensions.left, + y: chartDimensions.top, + width: chartDimensions.width, + height: chartDimensions.height, + }, + { + color: stringToRGB('transparent'), + }, + { + color: stringToRGB('red'), + width: 4, + dash: [4, 4], + }, + ); + }); + }, + ]); + }); +} diff --git a/src/chart_types/xy_chart/renderer/canvas/styles/area.ts b/src/chart_types/xy_chart/renderer/canvas/styles/area.ts new file mode 100644 index 0000000000..de52fe0ea0 --- /dev/null +++ b/src/chart_types/xy_chart/renderer/canvas/styles/area.ts @@ -0,0 +1,22 @@ +import { GeometryStateStyle, AreaStyle } from '../../../../../utils/themes/theme'; +import { stringToRGB } from '../../../../partition_chart/layout/utils/d3_utils'; +import { Fill } from '../../../../../geoms/types'; + +/** + * Return the rendering props for an area. The color of the area will be overwritten + * by the fill color of the themeAreaStyle parameter if present + * @param baseColor the assigned color of the area for this series + * @param themeAreaStyle the theme style for the area series + * @param geometryStateStyle the highlight geometry style + */ +export function buildAreaStyles( + baseColor: string, + themeAreaStyle: AreaStyle, + geometryStateStyle: GeometryStateStyle, +): Fill { + const fillColor = stringToRGB(themeAreaStyle.fill || baseColor); + fillColor.opacity = fillColor.opacity * themeAreaStyle.opacity * geometryStateStyle.opacity; + return { + color: fillColor, + }; +} diff --git a/src/chart_types/xy_chart/renderer/canvas/styles/bar.ts b/src/chart_types/xy_chart/renderer/canvas/styles/bar.ts new file mode 100644 index 0000000000..880f7a7538 --- /dev/null +++ b/src/chart_types/xy_chart/renderer/canvas/styles/bar.ts @@ -0,0 +1,38 @@ +import { GeometryStateStyle, RectStyle, RectBorderStyle } from '../../../../../utils/themes/theme'; +import { stringToRGB } from '../../../../partition_chart/layout/utils/d3_utils'; +import { Stroke, Fill } from '../../../../../geoms/types'; + +/** + * Return the rendering styles (stroke and fill) for a bar. + * The full color of the bar will be overwritten by the fill color + * of the themeRectStyle parameter if present. + * The stroke color of the bar will be overwritten by the stroke color + * of the themeRectBorderStyle parameter if present. + * @param baseColor the assigned color of the bar for this series + * @param themeRectStyle the theme style of the rectangle for the bar series + * @param themeRectBorderStyle the theme style of the rectangle borders for the bar series + * @param geometryStateStyle the highlight geometry style + */ +export function buildBarStyles( + baseColor: string, + themeRectStyle: RectStyle, + themeRectBorderStyle: RectBorderStyle, + geometryStateStyle: GeometryStateStyle, +): { fill: Fill; stroke: Stroke } { + const fillColor = stringToRGB(themeRectStyle.fill || baseColor); + const fillOpacity = themeRectStyle.opacity * geometryStateStyle.opacity; + fillColor.opacity = fillOpacity; + const fill: Fill = { + color: fillColor, + }; + const strokeColor = stringToRGB(themeRectBorderStyle.stroke || baseColor); + const defaultStrokeOpacity = + themeRectBorderStyle.strokeOpacity === undefined ? themeRectStyle.opacity : themeRectBorderStyle.strokeOpacity; + const borderStrokeOpacity = defaultStrokeOpacity * geometryStateStyle.opacity; + strokeColor.opacity = strokeColor.opacity * borderStrokeOpacity; + const stroke: Stroke = { + color: strokeColor, + width: themeRectBorderStyle.visible ? themeRectBorderStyle.strokeWidth : 0, + }; + return { fill, stroke }; +} diff --git a/src/chart_types/xy_chart/renderer/canvas/styles/line.ts b/src/chart_types/xy_chart/renderer/canvas/styles/line.ts new file mode 100644 index 0000000000..a7dd10cb2c --- /dev/null +++ b/src/chart_types/xy_chart/renderer/canvas/styles/line.ts @@ -0,0 +1,24 @@ +import { GeometryStateStyle, LineStyle } from '../../../../../utils/themes/theme'; +import { stringToRGB } from '../../../../partition_chart/layout/utils/d3_utils'; +import { Stroke } from '../../../../../geoms/types'; + +/** + * Return the rendering props for a line. The color of the line will be overwritten + * by the stroke color of the themeLineStyle parameter if present + * @param baseColor the assigned color of the line for this series + * @param themeLineStyle the theme style for the line series + * @param geometryStateStyle the highlight geometry style + */ +export function buildLineStyles( + baseColor: string, + themeLineStyle: LineStyle, + geometryStateStyle: GeometryStateStyle, +): Stroke { + const strokeColor = stringToRGB(themeLineStyle.stroke || baseColor); + strokeColor.opacity = strokeColor.opacity * themeLineStyle.opacity * geometryStateStyle.opacity; + return { + color: strokeColor, + width: themeLineStyle.strokeWidth, + dash: themeLineStyle.dash, + }; +} diff --git a/src/chart_types/xy_chart/renderer/canvas/styles/point.ts b/src/chart_types/xy_chart/renderer/canvas/styles/point.ts new file mode 100644 index 0000000000..960380e775 --- /dev/null +++ b/src/chart_types/xy_chart/renderer/canvas/styles/point.ts @@ -0,0 +1,37 @@ +import { PointStyle, GeometryStateStyle } from '../../../../../utils/themes/theme'; +import { stringToRGB } from '../../../../partition_chart/layout/utils/d3_utils'; +import { Fill, Stroke } from '../../../../../geoms/types'; +import { mergePartial } from '../../../../../utils/commons'; + +/** + * Return the fill, stroke and radius styles for a point geometry. + * The color value is used for stroke or fill if they are undefind in the theme PointStyle. + * If an override style is available it will overrides the style or the radius of the point. + * @param baseColor the series color + * @param themePointStyle the theme style or the merged point style if a custom PointStyle is applied + * @param geometryStateStyle the state style of the geometry + * @param overrides (optional) an override PointStyle + */ +export function buildPointStyles( + baseColor: string, + themePointStyle: PointStyle, + geometryStateStyle: GeometryStateStyle, + overrides?: Partial, +): { fill: Fill; stroke: Stroke; radius: number } { + const pointStyle = mergePartial(themePointStyle, overrides); + const fillColor = stringToRGB(pointStyle.fill || baseColor); + fillColor.opacity = fillColor.opacity * pointStyle.opacity * geometryStateStyle.opacity; + const fill: Fill = { + color: fillColor, + }; + + const strokeColor = stringToRGB(pointStyle.stroke || baseColor); + strokeColor.opacity = strokeColor.opacity * pointStyle.opacity * geometryStateStyle.opacity; + const stroke: Stroke = { + color: strokeColor, + width: pointStyle.strokeWidth, + }; + + const radius = overrides && overrides.radius ? overrides.radius : themePointStyle.radius; + return { fill, stroke, radius }; +} diff --git a/src/chart_types/xy_chart/renderer/canvas/utils/debug.ts b/src/chart_types/xy_chart/renderer/canvas/utils/debug.ts new file mode 100644 index 0000000000..34c1f2117b --- /dev/null +++ b/src/chart_types/xy_chart/renderer/canvas/utils/debug.ts @@ -0,0 +1,72 @@ +import { withContext } from '../../../../../renderers/canvas'; +import { Fill, Stroke, Rect } from '../../../../../geoms/types'; +import { renderRect } from '../primitives/rect'; +import { Point } from '../../../../../utils/point'; + +const DEFAULT_DEBUG_FILL: Fill = { + color: { + r: 238, + g: 130, + b: 238, + opacity: 0.2, + }, +}; +const DEFAULT_DEBUG_STROKE: Stroke = { + color: { + r: 0, + g: 0, + b: 0, + opacity: 0.2, + }, + width: 1, +}; + +export function renderDebugRect( + ctx: CanvasRenderingContext2D, + rect: Rect, + fill = DEFAULT_DEBUG_FILL, // violet + stroke = DEFAULT_DEBUG_STROKE, + rotation: number = 0, +) { + withContext(ctx, (ctx) => { + ctx.translate(rect.x, rect.y); + ctx.rotate((rotation * Math.PI) / 180); + renderRect( + ctx, + { + ...rect, + x: 0, + y: 0, + }, + fill, + stroke, + true, + ); + }); +} +export function renderDebugRectCenterRotated( + ctx: CanvasRenderingContext2D, + center: Point, + rect: Rect, + fill = DEFAULT_DEBUG_FILL, // violet + stroke = DEFAULT_DEBUG_STROKE, + rotation: number = 0, +) { + const { x, y } = center; + + withContext(ctx, (ctx) => { + ctx.translate(x, y); + ctx.rotate((rotation * Math.PI) / 180); + ctx.translate(-x, -y); + renderRect( + ctx, + { + ...rect, + x: x - rect.width / 2, + y: y - rect.height / 2, + }, + fill, + stroke, + ); + }); +} diff --git a/src/chart_types/xy_chart/renderer/canvas/utils/rendering_props_utils.test.ts b/src/chart_types/xy_chart/renderer/canvas/utils/rendering_props_utils.test.ts deleted file mode 100644 index 6ed5cd1264..0000000000 --- a/src/chart_types/xy_chart/renderer/canvas/utils/rendering_props_utils.test.ts +++ /dev/null @@ -1,1088 +0,0 @@ -import { Rotation } from '../../../utils/specs'; -import { - buildAreaRenderProps, - buildBarRenderProps, - buildLineRenderProps, - buildPointRenderProps, - buildPointStyleProps, - buildBarBorderRenderProps, -} from './rendering_props_utils'; -import { RectBorderStyle, RectStyle } from '../../../../../utils/themes/theme'; -import { - buildBarValueProps, - isBarValueOverflow, - getBarValueClipDimensions, - rotateBarValueProps, - Clippings, - clipRanges, -} from '../bar_values_utils'; -import { ClippedRanges } from '../../../../../utils/geometry'; -import { forcedType } from '../../../../../mocks/utils'; - -describe('[canvas] Area Geometries props', () => { - test('can build area point props', () => { - const pointStyleProps = buildPointStyleProps( - 'red', - { - visible: true, - radius: 30, - strokeWidth: 2, - opacity: 0.5, - }, - { - opacity: 0.2, - }, - ); - - const props = buildPointRenderProps(10, 20, pointStyleProps); - expect(props).toEqual({ - x: 10, - y: 20, - radius: 30, - strokeWidth: 2, - strokeEnabled: true, - stroke: 'red', - fill: 'red', - opacity: 0.2 * 0.5, - strokeHitEnabled: false, - perfectDrawEnabled: false, - listening: false, - }); - - const noStrokePointStyleProps = buildPointStyleProps( - 'blue', - { - visible: true, - radius: 30, - stroke: 'red', - strokeWidth: 0, - opacity: 0.5, - }, - { - opacity: 0.2, - }, - ); - - const propsNoStroke = buildPointRenderProps(10, 20, noStrokePointStyleProps); - expect(propsNoStroke).toEqual({ - x: 10, - y: 20, - radius: 30, - strokeWidth: 0, - strokeEnabled: false, - stroke: 'red', - fill: 'blue', - opacity: 0.2 * 0.5, - strokeHitEnabled: false, - perfectDrawEnabled: false, - listening: false, - }); - - const seriesPointStyleProps = buildPointStyleProps( - 'violet', - { - visible: true, - fill: 'pink', - radius: 123, - strokeWidth: 456, - opacity: 789, - }, - { - opacity: 0.2, - }, - ); - const seriesPointStyle = buildPointRenderProps(10, 20, seriesPointStyleProps); - expect(seriesPointStyle).toEqual({ - x: 10, - y: 20, - radius: 123, - strokeWidth: 456, - strokeEnabled: true, - stroke: 'violet', - fill: 'pink', - opacity: 0.2 * 789, - strokeHitEnabled: false, - perfectDrawEnabled: false, - listening: false, - }); - }); - test('can build area path props', () => { - const props = buildAreaRenderProps( - 40, - 'M0,0L10,10Z', - - 'red', - { - opacity: 0.5, - visible: true, - }, - { - opacity: 0.8, - }, - ); - expect(props).toEqual({ - data: 'M0,0L10,10Z', - x: 40, - fill: 'red', - lineCap: 'round', - lineJoin: 'round', - opacity: 0.8 * 0.5, - strokeHitEnabled: false, - perfectDrawEnabled: false, - listening: false, - }); - - const seriesAreaStyle = buildAreaRenderProps( - 0, - 'M0,0L10,10Z', - 'red', - - { - opacity: 123, - fill: 'blue', - visible: true, - }, - { - opacity: 1, - }, - ); - expect(seriesAreaStyle).toEqual({ - data: 'M0,0L10,10Z', - x: 0, - fill: 'blue', - lineCap: 'round', - lineJoin: 'round', - opacity: 1 * 123, - strokeHitEnabled: false, - perfectDrawEnabled: false, - listening: false, - }); - }); - test('can build area line path props', () => { - const props = buildLineRenderProps( - 40, - 'M0,0L10,10Z', - 'red', - - { - visible: true, - opacity: 1, - strokeWidth: 1, - }, - { - opacity: 0.5, - }, - ); - expect(props).toEqual({ - data: 'M0,0L10,10Z', - x: 40, - stroke: 'red', - strokeWidth: 1, - lineCap: 'round', - lineJoin: 'round', - opacity: 0.5, - strokeHitEnabled: false, - perfectDrawEnabled: false, - listening: false, - }); - expect(props.fill).toBeFalsy(); - - const seriesLineStyle = buildLineRenderProps( - 0, - - 'M0,0L10,10Z', - 'red', - - { - opacity: 0.5, - stroke: 'series-stroke', - strokeWidth: 66, - visible: true, - }, - { - opacity: 0.5, - }, - ); - expect(seriesLineStyle).toEqual({ - data: 'M0,0L10,10Z', - x: 0, - stroke: 'series-stroke', - strokeWidth: 66, - lineCap: 'round', - lineJoin: 'round', - opacity: 0.5 * 0.5, - strokeHitEnabled: false, - perfectDrawEnabled: false, - listening: false, - }); - }); -}); - -describe('[canvas] Line Geometries', () => { - test('can build line point props', () => { - const pointStyleProps = buildPointStyleProps( - 'pink', - { - visible: true, - radius: 30, - strokeWidth: 2, - opacity: 0.5, - }, - { - opacity: 0.2, - }, - ); - - const props = buildPointRenderProps(10, 20, pointStyleProps); - expect(props).toEqual({ - x: 10, - y: 20, - radius: 30, - strokeWidth: 2, - strokeEnabled: true, - stroke: 'pink', - fill: 'pink', - opacity: 0.2 * 0.5, - strokeHitEnabled: false, - perfectDrawEnabled: false, - listening: false, - }); - - const noStrokeStyleProps = buildPointStyleProps( - 'pink', - { - visible: true, - radius: 30, - strokeWidth: 0, - opacity: 0.5, - }, - { - opacity: 0.2, - }, - ); - const propsNoStroke = buildPointRenderProps(10, 20, noStrokeStyleProps); - expect(propsNoStroke).toEqual({ - x: 10, - y: 20, - radius: 30, - strokeWidth: 0, - strokeEnabled: false, - stroke: 'pink', - fill: 'pink', - opacity: 0.2 * 0.5, - strokeHitEnabled: false, - perfectDrawEnabled: false, - listening: false, - }); - - const seriesPointStyleProps = buildPointStyleProps( - 'pink', - { - stroke: 'series-stroke', - strokeWidth: 6, - visible: true, - radius: 12, - opacity: 18, - }, - { - opacity: 0.2, - }, - ); - const seriesPointStyle = buildPointRenderProps(10, 20, seriesPointStyleProps); - expect(seriesPointStyle).toEqual({ - x: 10, - y: 20, - radius: 12, - strokeWidth: 6, - strokeEnabled: true, - stroke: 'series-stroke', - fill: 'pink', - opacity: 0.2 * 18, - strokeHitEnabled: false, - perfectDrawEnabled: false, - listening: false, - }); - }); - test('can build line path props', () => { - const props = buildLineRenderProps( - 40, - 'M0,0L10,10Z', - - 'red', - - { - visible: true, - opacity: 1, - strokeWidth: 1, - }, - { - opacity: 0.5, - }, - ); - expect(props).toEqual({ - data: 'M0,0L10,10Z', - x: 40, - stroke: 'red', - strokeWidth: 1, - lineCap: 'round', - lineJoin: 'round', - opacity: 0.5, - strokeHitEnabled: false, - perfectDrawEnabled: false, - listening: false, - }); - expect(props.fill).toBeFalsy(); - - const seriesLineStyleProps = buildLineRenderProps( - 0, - 'M0,0L10,10Z', - - 'red', - - { - opacity: 1, - strokeWidth: 66, - visible: true, - }, - { - opacity: 0.5, - }, - ); - expect(seriesLineStyleProps).toEqual({ - data: 'M0,0L10,10Z', - x: 0, - stroke: 'red', - strokeWidth: 66, - lineCap: 'round', - lineJoin: 'round', - opacity: 0.5, - strokeHitEnabled: false, - perfectDrawEnabled: false, - listening: false, - }); - }); -}); - -describe('[canvas] Bar Geometries', () => { - describe('buildBarValueProps', () => { - test('can build bar props', () => { - const props = buildBarRenderProps( - 10, - 20, - 30, - 40, - 'red', - { - opacity: 1, - }, - { - strokeOpacity: 0.5, - strokeWidth: 2, - visible: true, - stroke: 'blue', - }, - { - opacity: 0.5, - }, - ); - expect(props).toEqual({ - x: 12, - y: 22, - width: 26, - height: 36, - fill: 'red', - strokeEnabled: false, - strokeHitEnabled: false, - perfectDrawEnabled: false, - listening: false, - opacity: 0.5, - }); - - const barWithNoBorder = buildBarRenderProps( - 10, - 20, - 30, - 40, - 'red', - { - opacity: 1, - }, - { - strokeOpacity: 0.5, - strokeWidth: 2, - visible: true, - stroke: 'blue', - }, - { - opacity: 0.5, - }, - ); - expect(barWithNoBorder).toEqual({ - x: 12, - y: 22, - width: 26, - height: 36, - fill: 'red', - strokeEnabled: false, - strokeHitEnabled: false, - perfectDrawEnabled: false, - listening: false, - opacity: 0.5, - }); - }); - - const getProps = (borderStyle: Partial = {}, barStyle: Partial = {}) => { - return [ - 10, - 20, - 30, - 40, - 'red', - { - opacity: 1, - ...barStyle, - }, - { - strokeOpacity: 0.5, - strokeWidth: 2, - visible: true, - stroke: 'blue', - ...borderStyle, - }, - { - opacity: 0.5, - }, - ]; - }; - - it('should build bar props with stroke', () => { - // @ts-ignore - const props = buildBarRenderProps(...getProps()); - expect(props).toEqual({ - x: 12, - y: 22, - width: 26, - height: 36, - strokeEnabled: false, - strokeHitEnabled: false, - perfectDrawEnabled: false, - listening: false, - opacity: 0.5, - fill: 'red', - }); - }); - - it('should return dimensions without offset if visible is false', () => { - // @ts-ignore - const props = buildBarRenderProps(...getProps({ visible: false })); - expect(props).toMatchObject({ - x: 10, - y: 20, - width: 30, - height: 40, - }); - }); - - it('should return dimensions without offset if strokeWidth is 0 or less', () => { - // @ts-ignore - const props = buildBarRenderProps(...getProps({ strokeWidth: 0 })); - expect(props).toMatchObject({ - x: 10, - y: 20, - width: 30, - height: 40, - }); - }); - - it('should return dimensions without offset if no stroke color', () => { - // @ts-ignore - const props = buildBarRenderProps(...getProps({ stroke: undefined })); - expect(props).toMatchObject({ - x: 10, - y: 20, - width: 30, - height: 40, - }); - }); - - it('should return dimensions without offset if no stroke opacity', () => { - // @ts-ignore - const props = buildBarRenderProps(...getProps({ strokeOpacity: undefined })); - expect(props).toMatchObject({ - x: 10, - y: 20, - width: 30, - height: 40, - opacity: 0.5, - }); - }); - - it('should return dimensions without offset if no stroke opacity and bar opacity in 0', () => { - // @ts-ignore - const props = buildBarRenderProps(...getProps({ strokeOpacity: undefined }, { opacity: 0 })); - expect(props).toMatchObject({ - x: 10, - y: 20, - width: 30, - height: 40, - }); - }); - }); - - describe('buildBarValueProps', () => { - test('can build bar value props', () => { - const valueArguments = { - x: 10, - y: 20, - barWidth: 30, - barHeight: 40, - displayValueStyle: { - fill: 'fill', - fontFamily: 'ff', - fontSize: 10, - padding: 5, - offsetX: 0, - offsetY: 0, - }, - displayValue: { - text: 'foo', - width: 10, - height: 10, - isValueContainedInElement: false, - hideClippedValue: false, - }, - chartDimensions: { - width: 10, - height: 10, - top: 0, - left: 0, - }, - chartRotation: 0 as Rotation, - }; - - const basicProps = buildBarValueProps(valueArguments); - expect(basicProps).toEqual({ - ...valueArguments.displayValueStyle, - x: 17.5, - y: 20, - height: 15, - width: 15, - align: 'center', - verticalAlign: 'top', - text: 'foo', - }); - - valueArguments.barHeight = 2; - const insufficientHeightBarProps = buildBarValueProps(valueArguments); - expect(insufficientHeightBarProps).toEqual({ - ...valueArguments.displayValueStyle, - x: 17.5, - y: 5, - height: 15, - width: 15, - align: 'center', - verticalAlign: 'top', - text: 'foo', - }); - - valueArguments.y = 4; - valueArguments.barHeight = 20; - valueArguments.chartDimensions = { - left: 0, - top: 0, - width: 0, - height: 0, - }; - const chartOverflowBarProps = buildBarValueProps(valueArguments); - expect(chartOverflowBarProps).toEqual({ - ...valueArguments.displayValueStyle, - x: 17.5, - y: 4, - width: 15, - height: 15, - align: 'center', - verticalAlign: 'top', - text: 'foo', - }); - - valueArguments.displayValue.isValueContainedInElement = true; - const containedBarProps = buildBarValueProps(valueArguments); - expect(containedBarProps).toEqual({ - ...valueArguments.displayValueStyle, - x: 17.5, - y: -21, - height: 25, - width: 15, - align: 'center', - verticalAlign: 'top', - text: 'foo', - }); - - valueArguments.displayValue.isValueContainedInElement = false; - valueArguments.barWidth = 0; - const containedXBarProps = buildBarValueProps(valueArguments); - expect(containedXBarProps).toEqual({ - ...valueArguments.displayValueStyle, - x: 2.5, - y: 4, - height: 15, - width: 15, - align: 'center', - verticalAlign: 'top', - text: 'foo', - }); - - valueArguments.displayValue.hideClippedValue = true; - valueArguments.barWidth = 0; - const clippedBarProps = buildBarValueProps(valueArguments); - expect(clippedBarProps).toEqual({ - ...valueArguments.displayValueStyle, - x: 2.5, - y: 4, - height: 0, - width: 0, - align: 'center', - verticalAlign: 'top', - text: 'foo', - }); - }); - }); - - test('shouold get clipDimensions for bar values', () => { - const chartDimensions = { - width: 100, - height: 100, - top: 0, - left: 0, - }; - - const clip = { - width: 10, - height: 20, - offsetX: 0, - offsetY: 0, - }; - - const displayValue = { - x: 0, - y: 0, - offsetX: 2, - offsetY: 4, - }; - - const overflowVisible = isBarValueOverflow(chartDimensions, clip, displayValue); - expect(overflowVisible).toBe(false); - - clip.offsetX = -15; - clip.offsetY = 0; - const overflowXHidden = isBarValueOverflow(chartDimensions, clip, displayValue, true); - expect(overflowXHidden).toBe(true); - - clip.offsetX = 10; - clip.offsetY = -25; - const overflowYHidden = isBarValueOverflow(chartDimensions, clip, displayValue, true); - expect(overflowYHidden).toBe(true); - }); - test('can get bar value clip dimensions', () => { - const barHeight = 30; - const computedDimensions = { - width: 20, - height: 10, - }; - const displayValue = { - width: 15, - height: 5, - isValueContainedInElement: false, - }; - - const unrotatedClipDimensions = getBarValueClipDimensions(displayValue, computedDimensions, barHeight, 0); - expect(unrotatedClipDimensions).toEqual({ - width: 20, - height: 10, - offsetX: 0, - offsetY: 0, - }); - - const horizontalRotatedClipDimensions = getBarValueClipDimensions(displayValue, computedDimensions, barHeight, 180); - expect(horizontalRotatedClipDimensions).toEqual({ - width: 20, - height: 10, - offsetX: 0, - offsetY: 25, - }); - - const verticalRotatedClipDimensions = getBarValueClipDimensions(displayValue, computedDimensions, barHeight, 90); - expect(verticalRotatedClipDimensions).toEqual({ - width: 20, - height: 10, - offsetX: 15, - offsetY: 0, - }); - }); - test('can compute props for rotated bar values', () => { - const chartDimensions = { - width: 100, - height: 100, - top: 0, - left: 0, - }; - const barDimensions = { - width: 10, - height: 20, - top: 0, - left: 0, - }; - const displayValueDimensions = { - width: 15, - height: 25, - top: 0, - left: 0, - }; - const displayValue = { - text: 'foo', - width: 10, - height: 20, - isValueContainedInElement: false, - }; - const displayValueStyle = { - fill: 'fill', - fontFamily: 'ff', - fontSize: 10, - padding: 0, - offsetX: 0, - offsetY: 0, - }; - const props = { - ...displayValueStyle, - x: 33, - y: 66, - align: 'center', - verticalAlign: 'top', - text: 'foo', - width: 10, - height: 20, - }; - - // 0 rotation - const defaultProps = rotateBarValueProps( - 0, - chartDimensions, - barDimensions, - displayValueDimensions, - displayValue, - props, - ); - expect(defaultProps).toEqual(props); - - // 180 rotation - const rotatedHorizontalProps = rotateBarValueProps( - 180, - chartDimensions, - barDimensions, - displayValueDimensions, - displayValue, - props, - ); - const expectedRotatedHorizontalProps = { - ...props, - verticalAlign: 'bottom', - x: 85, - y: 75, - }; - expect(rotatedHorizontalProps).toEqual(expectedRotatedHorizontalProps); - - // 90 rotation - const verticalProps = rotateBarValueProps( - 90, - chartDimensions, - barDimensions, - displayValueDimensions, - displayValue, - props, - ); - const expectedVerticalProps = { - ...props, - verticalAlign: 'middle', - x: 85, - y: -7.5, - }; - expect(verticalProps).toEqual(expectedVerticalProps); - - const verticalOverflowProps = rotateBarValueProps( - 90, - chartDimensions, - { ...barDimensions, height: 0 }, - displayValueDimensions, - displayValue, - props, - ); - - expectedVerticalProps.x = 100; - expect(verticalOverflowProps).toEqual(expectedVerticalProps); - - const verticalContainedProps = rotateBarValueProps( - 90, - chartDimensions, - barDimensions, - displayValueDimensions, - { ...displayValue, isValueContainedInElement: true }, - props, - ); - - expectedVerticalProps.x = 80; - expectedVerticalProps.y = 0; - expectedVerticalProps.height = 0; - expectedVerticalProps.width = 20; - expectedVerticalProps.align = 'right'; - expect(verticalContainedProps).toEqual(expectedVerticalProps); - - const verticalContainedOverflowProps = rotateBarValueProps( - 90, - chartDimensions, - { ...barDimensions, height: 0, width: 50 }, - displayValueDimensions, - { ...displayValue, isValueContainedInElement: true }, - props, - ); - expectedVerticalProps.width = 0; - expectedVerticalProps.height = 50; - expectedVerticalProps.x = 100; - expectedVerticalProps.y = 0; - expect(verticalContainedOverflowProps).toEqual(expectedVerticalProps); - - // -90 rotation - const rotatedVerticalProps = rotateBarValueProps( - -90, - chartDimensions, - barDimensions, - displayValueDimensions, - displayValue, - props, - ); - const expectedRotatedVerticalProps = { - ...props, - verticalAlign: 'middle', - x: 0, - y: 82.5, - height: 50, - width: 0, - }; - expect(rotatedVerticalProps).toEqual(expectedRotatedVerticalProps); - - const rotatedVerticalOverflowProps = rotateBarValueProps( - -90, - chartDimensions, - { ...barDimensions, height: 0 }, - displayValueDimensions, - displayValue, - props, - ); - expectedRotatedVerticalProps.x = -15; - expect(rotatedVerticalOverflowProps).toEqual(expectedRotatedVerticalProps); - - const rotatedVerticalOverflowContainedProps = rotateBarValueProps( - -90, - chartDimensions, - { ...barDimensions, height: 0, width: 50 }, - displayValueDimensions, - { ...displayValue, isValueContainedInElement: true }, - props, - ); - - expectedRotatedVerticalProps.x = 0; - expectedRotatedVerticalProps.y = 50; - expectedRotatedVerticalProps.align = 'left'; - expect(rotatedVerticalOverflowContainedProps).toEqual(expectedRotatedVerticalProps); - - const rotatedVerticalContainedProps = rotateBarValueProps( - -90, - chartDimensions, - barDimensions, - displayValueDimensions, - { ...displayValue, isValueContainedInElement: true }, - props, - ); - - expectedRotatedVerticalProps.width = 20; - expectedRotatedVerticalProps.height = 0; - expectedRotatedVerticalProps.y = 90; - expect(rotatedVerticalContainedProps).toEqual(expectedRotatedVerticalProps); - }); - - describe('buildBarBorderRenderProps', () => { - const getProps = (borderStyle: Partial = {}, barStyle: Partial = {}) => { - return [ - 10, - 20, - 30, - 40, - { - opacity: 1, - ...barStyle, - }, - { - strokeOpacity: 0.5, - strokeWidth: 2, - visible: true, - stroke: 'blue', - ...borderStyle, - }, - { - opacity: 0.5, - }, - ]; - }; - it('should build bar props with stroke', () => { - // @ts-ignore - const props = buildBarBorderRenderProps(...getProps()); - expect(props).toEqual({ - x: 11, - y: 21, - width: 28, - height: 38, - fillEnabled: false, - strokeEnabled: true, - strokeWidth: 2, - stroke: 'blue', - strokeHitEnabled: false, - perfectDrawEnabled: false, - listening: false, - opacity: 0.5 * 0.5, - }); - }); - - it('should return null if visible is false', () => { - // @ts-ignore - const props = buildBarBorderRenderProps(...getProps({ visible: false })); - expect(props).toBeNull(); - }); - - it('should return null if strokeWidth is 0 or less', () => { - // @ts-ignore - const props = buildBarBorderRenderProps(...getProps({ strokeWidth: 0 })); - expect(props).toBeNull(); - }); - - it('should return null if no stroke color', () => { - // @ts-ignore - const props = buildBarBorderRenderProps(...getProps({ stroke: undefined })); - expect(props).toBeNull(); - }); - - it('should return props with bar opacity if no stroke opacity', () => { - // @ts-ignore - const props = buildBarBorderRenderProps(...getProps({ strokeOpacity: undefined })); - expect(props).toMatchObject({ - opacity: 1 * 0.5, - }); - }); - - it('should return null if no stroke opacity and bar opacity in 0', () => { - // @ts-ignore - const props = buildBarBorderRenderProps(...getProps({ strokeOpacity: undefined }, { opacity: 0 })); - expect(props).toBeNull(); - }); - }); - - describe('clipRanges', () => { - const clippedRanges: ClippedRanges = [ - [0, 1], - [2, 4], - [4, 6], - [7, 11], - [11, 12], - ]; - const singleRange: ClippedRanges = [[0, 1]]; - const clippings: Clippings = { - clipHeight: 111, - clipWidth: 222, - }; - const mockCtx = forcedType({ - rect: jest.fn(), - }); - - describe('clipping is NOT negated', () => { - it('should call ctx with correct args - empty range', () => { - clipRanges([], clippings, false)(mockCtx); - - expect(mockCtx.rect).not.toBeCalled(); - }); - - describe('length equal to 1', () => { - it('should call ctx with correct args for start range - single range', () => { - clipRanges(singleRange, clippings, false)(mockCtx); - - expect(mockCtx.rect).toHaveBeenNthCalledWith(1, 0, 0, singleRange[0][0], clippings.clipHeight); - }); - - it('should call ctx with correct args for end range - single range', () => { - clipRanges(singleRange, clippings, false)(mockCtx); - const lastX = singleRange[singleRange.length - 1][1]; - - expect(mockCtx.rect).toHaveBeenNthCalledWith(2, lastX, 0, clippings.clipWidth - lastX, clippings.clipHeight); - }); - - it('should only call ctx twice', () => { - clipRanges(singleRange, clippings, false)(mockCtx); - - expect(mockCtx.rect).toBeCalledTimes(2); - }); - }); - - describe('length greater than 1', () => { - it('should call ctx with correct args for start range - single range', () => { - clipRanges(clippedRanges, clippings, false)(mockCtx); - - expect(mockCtx.rect).toHaveBeenNthCalledWith(1, 0, 0, clippedRanges[0][0], clippings.clipHeight); - }); - - it('should call ctx with correct args for end range - single range', () => { - clipRanges(clippedRanges, clippings, false)(mockCtx); - const lastX = clippedRanges[clippedRanges.length - 1][1]; - - expect(mockCtx.rect).toHaveBeenNthCalledWith(2, lastX, 0, clippings.clipWidth - lastX, clippings.clipHeight); - }); - - it('should call ctx with correct args', () => { - clipRanges(clippedRanges, clippings, false)(mockCtx); - - for (let i = 1; i < length; i++) { - const [, x0] = clippedRanges[i - 1]; - const [x1] = clippedRanges[i]; - - expect(mockCtx.rect).toHaveBeenNthCalledWith(i + 3, x0, 0, x1 - x0, clippings.clipHeight); - } - }); - - it('should only call ctx for (n - 1) range plus 2 for ends', () => { - clipRanges(clippedRanges, clippings, false)(mockCtx); - - expect(mockCtx.rect).toBeCalledTimes(clippedRanges.length - 1 + 2); - }); - }); - }); - - describe('clipping is negated', () => { - it('should call ctx with correct args - empty range', () => { - clipRanges([], clippings, true)(mockCtx); - - expect(mockCtx.rect).not.toBeCalled(); - }); - - it('should call ctx with correct args - single range', () => { - clipRanges(singleRange, clippings, true)(mockCtx); - const [x0, x1] = clippedRanges[0]; - - expect(mockCtx.rect).toHaveBeenNthCalledWith(1, x0, 0, x1 - x0, clippings.clipHeight); - }); - - it('should call ctx with correct args', () => { - clipRanges(clippedRanges, clippings, true)(mockCtx); - - clippedRanges.forEach(([x0, x1], i) => { - expect(mockCtx.rect).toHaveBeenNthCalledWith(i + 1, x0, 0, x1 - x0, clippings.clipHeight); - }); - }); - }); - }); -}); diff --git a/src/chart_types/xy_chart/renderer/canvas/utils/rendering_props_utils.ts b/src/chart_types/xy_chart/renderer/canvas/utils/rendering_props_utils.ts deleted file mode 100644 index 9b2ab21b06..0000000000 --- a/src/chart_types/xy_chart/renderer/canvas/utils/rendering_props_utils.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { RectConfig } from 'konva/types/shapes/Rect'; -import { PathConfig } from 'konva/types/shapes/Path'; -import { CircleConfig } from 'konva/types/shapes/Circle'; -import { - AreaStyle, - LineStyle, - PointStyle, - RectBorderStyle, - RectStyle, - GeometryStateStyle, -} from '../../../../../utils/themes/theme'; -import { GlobalKonvaElementProps } from '../../../../../components/react_canvas/globals'; - -export interface PointStyleProps { - radius: number; - stroke: string; - strokeWidth: number; - strokeEnabled: boolean; - fill: string; - opacity: number; -} - -/** - * Return the style of a point. - * The color value is used for stroke or fill if they are undefind in the PointStyle - * @param color the series color - * @param pointStyle the merged point style - */ -export function buildPointStyleProps( - color: string, - pointStyle: PointStyle, - geometryStateStyle: GeometryStateStyle, -): PointStyleProps { - const { strokeWidth, opacity } = pointStyle; - const stroke = pointStyle.stroke || color; - const fill = pointStyle.fill || color; - return { - radius: pointStyle.radius, - stroke, - strokeWidth, - strokeEnabled: strokeWidth !== 0, - fill: fill, - ...geometryStateStyle, - opacity: opacity * geometryStateStyle.opacity, - }; -} - -/** - * Return the rendering props for a point - * @param x the x position of the point - * @param y the y position of the point - * @param pointStyleProps the style props of the point - */ -export function buildPointRenderProps(x: number, y: number, pointStyleProps: PointStyleProps): CircleConfig { - return { - x, - y, - ...pointStyleProps, - ...GlobalKonvaElementProps, - }; -} - -/** - * Return the rendering props for a line. The color of the line will be overwritten - * by the stroke color of the lineStyle parameter if present - * @param x the horizontal offset to place the line - * @param linePath the SVG line path - * @param color the computed color of the line for this series - * @param lineStyle the line style - * @param geometryStateStyle the highlight geometry style - */ -export function buildLineRenderProps( - x: number, - linePath: string, - color: string, - lineStyle: LineStyle, - geometryStateStyle: GeometryStateStyle, -): PathConfig { - const opacity = lineStyle.opacity * geometryStateStyle.opacity; - - return { - x, - data: linePath, - stroke: lineStyle.stroke || color, - strokeWidth: lineStyle.strokeWidth, - lineCap: 'round', - lineJoin: 'round', - ...geometryStateStyle, - opacity, // want to override opactiy of geometryStateStyle - ...GlobalKonvaElementProps, - }; -} - -/** - * Return the rendering props for an area. The color of the area will be overwritten - * by the fill color of the areaStyle parameter if present - * @param areaPath the SVG area path - * @param x the horizontal offset to place the area - * @param color the computed color of the line for this series - * @param areaStyle the area style - * @param geometryStateStyle the highlight geometry style - */ -export function buildAreaRenderProps( - xTransform: number, - areaPath: string, - color: string, - areaStyle: AreaStyle, - geometryStateStyle: GeometryStateStyle, -): PathConfig { - const opacity = areaStyle.opacity * geometryStateStyle.opacity; - - return { - x: xTransform, - data: areaPath, - fill: areaStyle.fill || color, - lineCap: 'round', - lineJoin: 'round', - ...geometryStateStyle, - opacity, // want to override opactiy of geometryStateStyle - ...GlobalKonvaElementProps, - }; -} - -/** - * Return the rendering props for a bar. The color of the bar will be overwritten - * by the fill color of the rectStyle parameter if present - * @param x the x position of the rect - * @param y the y position of the rect - * @param width the width of the rect - * @param height the height of the rect - * @param color the computed color of the rect for this series - * @param rectStyle the rect style - * @param geometryStateStyle the highlight geometry style - */ -export function buildBarRenderProps( - x: number, - y: number, - width: number, - height: number, - color: string, - rectStyle: RectStyle, - borderStyle: RectBorderStyle, - geometryStateStyle: GeometryStateStyle, -): RectConfig { - const opacity = rectStyle.opacity * geometryStateStyle.opacity; - const { stroke, visible, strokeWidth, strokeOpacity = 0 } = borderStyle; - const offset = !visible || strokeWidth <= 0 || !stroke || strokeOpacity <= 0 || opacity <= 0 ? 0 : strokeWidth; - - return { - x: x + offset, - y: y + offset, - width: width - 2 * offset, - height: height - 2 * offset, - fill: rectStyle.fill || color, - strokeEnabled: false, - ...geometryStateStyle, - opacity, // want to override opactiy of geometryStateStyle - ...GlobalKonvaElementProps, - }; -} - -/** - * Return the rendering props for a bar. The color of the bar will be overwritten - * by the fill color of the rectStyle parameter if present - * @param x the x position of the rect - * @param y the y position of the rect - * @param width the width of the rect - * @param height the height of the rect - * @param color the computed color of the rect for this series - * @param rectStyle the rect style - * @param borderStyle the border rect style - * @param geometryStyle the highlight geometry style - */ -export function buildBarBorderRenderProps( - x: number, - y: number, - width: number, - height: number, - rectStyle: RectStyle, - borderStyle: RectBorderStyle, - geometryStateStyle: GeometryStateStyle, -): RectConfig | null { - const { stroke, visible, strokeWidth, strokeOpacity = rectStyle.opacity } = borderStyle; - const opacity = strokeOpacity * geometryStateStyle.opacity; - - if (!visible || strokeWidth <= 0 || !stroke || opacity <= 0) { - return null; - } - - return { - x: x + strokeWidth / 2, - y: y + strokeWidth / 2, - width: width - strokeWidth, - height: height - strokeWidth, - fillEnabled: false, - strokeEnabled: true, - strokeWidth, - stroke, - ...geometryStateStyle, - opacity, // want to override opactiy of geometryStateStyle - ...GlobalKonvaElementProps, - }; -} diff --git a/src/chart_types/xy_chart/renderer/canvas/values/bar.ts b/src/chart_types/xy_chart/renderer/canvas/values/bar.ts new file mode 100644 index 0000000000..9f48cd3bb1 --- /dev/null +++ b/src/chart_types/xy_chart/renderer/canvas/values/bar.ts @@ -0,0 +1,173 @@ +import { Rotation } from '../../../utils/specs'; +import { Dimensions } from '../../../../../utils/dimensions'; +import { Theme } from '../../../../../utils/themes/theme'; +import { BarGeometry } from '../../../../../utils/geometry'; +import { renderText, wrapLines } from '../primitives/text'; +import { renderDebugRect } from '../utils/debug'; +import { Font, FontStyle, TextBaseline, TextAlign } from '../../../../partition_chart/layout/types/types'; +import { Point } from '../../../../../utils/point'; +import { Rect } from '../../../../../geoms/types'; + +interface BarValuesProps { + theme: Theme; + chartDimensions: Dimensions; + chartRotation: Rotation; + debug: boolean; + bars: BarGeometry[]; +} +export function renderBarValues(ctx: CanvasRenderingContext2D, props: BarValuesProps) { + const { bars, debug, chartRotation, chartDimensions, theme } = props; + const { fontFamily, fontStyle, fill, fontSize } = theme.barSeriesStyle.displayValue; + const barsLength = bars.length; + for (let i = 0; i < barsLength; i++) { + const { displayValue } = bars[i]; + if (!displayValue) { + continue; + } + const { text } = displayValue; + let textLines = { + lines: [text], + width: displayValue.width, + height: displayValue.height, + }; + const font: Font = { + fontFamily: fontFamily, + fontStyle: fontStyle ? (fontStyle as FontStyle) : 'normal', + fontVariant: 'normal', + fontWeight: 'normal', + }; + + const { x, y, align, baseline, rect } = positionText( + bars[i], + displayValue, + chartRotation, + theme.barSeriesStyle.displayValue, + ); + + if (displayValue.isValueContainedInElement) { + const width = chartRotation === 0 || chartRotation === 180 ? bars[i].width : bars[i].height; + textLines = wrapLines(ctx, textLines.lines[0], font, fontSize, width, 100); + } + if (displayValue.hideClippedValue && isOverflow(rect, chartDimensions, chartRotation)) { + continue; + } + if (debug) { + renderDebugRect(ctx, rect); + } + const { width, height } = textLines; + const linesLength = textLines.lines.length; + + for (let i = 0; i < linesLength; i++) { + const text = textLines.lines[i]; + const origin = repositionTextLine({ x, y }, chartRotation, i, linesLength, { height, width }); + renderText( + ctx, + origin, + text, + { + ...font, + fill, + fontSize, + align, + baseline, + }, + -chartRotation, + ); + } + } +} +function repositionTextLine( + origin: Point, + chartRotation: Rotation, + i: number, + max: number, + box: { height: number; width: number }, +) { + const { x, y } = origin; + const { width, height } = box; + let lineX = x; + let lineY = y + i * height; + switch (chartRotation) { + case 180: + lineX = x; + lineY = y - (i - max + 1) * height; + break; + case -90: + lineX = x; + lineY = y; + case 90: + lineX = x; + lineY = y - (i - max + 1) * width; + } + + return { x: lineX, y: lineY }; +} + +function positionText( + geom: BarGeometry, + valueBox: { width: number; height: number }, + chartRotation: Rotation, + offsets: { offsetX: number; offsetY: number }, +) { + const { offsetX, offsetY } = offsets; + let baseline: TextBaseline = 'top'; + let align: TextAlign = 'center'; + + let x = geom.x + geom.width / 2 - offsetX; + let y = geom.y - offsetY; + const rect: Rect = { + x: x - valueBox.width / 2, + y, + width: valueBox.width, + height: valueBox.height, + }; + if (chartRotation === 180) { + baseline = 'bottom'; + x = geom.x + geom.width / 2 + offsetX; + y = geom.y + offsetY; + rect.x = x - valueBox.width / 2; + rect.y = y; + } + if (chartRotation === 90) { + x = geom.x - offsetY; + y = geom.y + offsetX; + align = 'right'; + rect.x = x; + rect.y = y; + rect.width = valueBox.height; + rect.height = valueBox.width; + } + if (chartRotation === -90) { + x = geom.x + geom.width + offsetY; + y = geom.y - offsetX; + align = 'left'; + rect.x = x - valueBox.height; + rect.y = y; + rect.width = valueBox.height; + rect.height = valueBox.width; + } + return { + x, + y, + align, + baseline, + rect, + }; +} +function isOverflow(rect: Rect, chartDimensions: Dimensions, chartRotation: Rotation) { + let cWidth = chartDimensions.width; + let cHeight = chartDimensions.height; + if (chartRotation === 90 || chartRotation === -90) { + cWidth = chartDimensions.height; + cHeight = chartDimensions.width; + } + + if (rect.x < 0 || rect.x + rect.width > cWidth) { + return true; + } + if (rect.y < 0 || rect.y + rect.height > cHeight) { + return true; + } + + return false; +} diff --git a/src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx b/src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx new file mode 100644 index 0000000000..65d4a71f9c --- /dev/null +++ b/src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx @@ -0,0 +1,221 @@ +import React, { RefObject } from 'react'; +import { connect } from 'react-redux'; +import { bindActionCreators, Dispatch } from 'redux'; +import { onChartRendered } from '../../../../state/actions/chart'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { getChartContainerDimensionsSelector } from '../../../../state/selectors/get_chart_container_dimensions'; +import { getChartRotationSelector } from '../../../../state/selectors/get_chart_rotation'; +import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { isInitialized } from '../../../../state/selectors/is_initialized'; +import { Dimensions } from '../../../../utils/dimensions'; +import { AnnotationId, AxisId } from '../../../../utils/ids'; +import { LIGHT_THEME } from '../../../../utils/themes/light_theme'; +import { Theme } from '../../../../utils/themes/theme'; +import { AnnotationDimensions } from '../../annotations/annotation_utils'; +import { LegendItem } from '../../legend/legend'; +import { computeAnnotationDimensionsSelector } from '../../state/selectors/compute_annotations'; +import { computeAxisTicksDimensionsSelector } from '../../state/selectors/compute_axis_ticks_dimensions'; +import { AxisVisibleTicks, computeAxisVisibleTicksSelector } from '../../state/selectors/compute_axis_visible_ticks'; +import { computeChartDimensionsSelector } from '../../state/selectors/compute_chart_dimensions'; +import { computeChartTransformSelector } from '../../state/selectors/compute_chart_transform'; +import { computeSeriesGeometriesSelector } from '../../state/selectors/compute_series_geometries'; +import { getHighlightedSeriesSelector } from '../../state/selectors/get_highlighted_series'; +import { getAnnotationSpecsSelector, getAxisSpecsSelector } from '../../state/selectors/get_specs'; +import { Geometries, Transform } from '../../state/utils'; +import { AxisLinePosition, AxisTicksDimensions } from '../../utils/axis_utils'; +import { AxisSpec, Rotation, AnnotationSpec } from '../../utils/specs'; +import { renderXYChartCanvas2d } from './renderers'; +import { isChartEmptySelector } from '../../state/selectors/is_chart_empty'; +import { deepEqual } from '../../../../utils/fast_deep_equal'; + +export interface ReactiveChartStateProps { + initialized: boolean; + debug: boolean; + isChartEmpty: boolean; + geometries: Geometries; + theme: Theme; + chartContainerDimensions: Dimensions; + chartRotation: Rotation; + chartDimensions: Dimensions; + chartTransform: Transform; + highlightedLegendItem?: LegendItem; + axesSpecs: AxisSpec[]; + axesTicksDimensions: Map; + axisTickPositions: AxisVisibleTicks; + axesGridLinesPositions: Map; + annotationDimensions: Map; + annotationSpecs: AnnotationSpec[]; +} + +interface ReactiveChartDispatchProps { + onChartRendered: typeof onChartRendered; +} +interface ReactiveChartOwnProps { + forwardStageRef: RefObject; +} + +type XYChartProps = ReactiveChartStateProps & ReactiveChartDispatchProps & ReactiveChartOwnProps; +class XYChartComponent extends React.Component { + static displayName = 'Partition'; + private ctx: CanvasRenderingContext2D | null; + // see example https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio#Example + private readonly devicePixelRatio: number; // fixme this be no constant: multi-monitor window drag may necessitate modifying the `` dimensions + constructor(props: Readonly) { + super(props); + this.ctx = null; + this.devicePixelRatio = window.devicePixelRatio; + } + + private drawCanvas() { + if (this.ctx) { + const { chartDimensions, chartRotation } = this.props; + const clippings = { + x: 0, + y: 0, + width: [90, -90].includes(chartRotation) ? chartDimensions.height : chartDimensions.width, + height: [90, -90].includes(chartRotation) ? chartDimensions.width : chartDimensions.height, + }; + renderXYChartCanvas2d(this.ctx, this.devicePixelRatio, clippings, this.props); + } + } + + private tryCanvasContext() { + const canvas = this.props.forwardStageRef.current; + this.ctx = canvas && canvas.getContext('2d'); + } + shouldComponentUpdate(nextProps: ReactiveChartStateProps) { + return !deepEqual(this.props, nextProps); + } + + componentDidUpdate() { + if (!this.ctx) { + this.tryCanvasContext(); + } + if (this.props.initialized) { + this.drawCanvas(); + this.props.onChartRendered(); + } + } + + componentDidMount() { + // the DOM element has just been appended, and getContext('2d') is always non-null, + // so we could use a couple of ! non-null assertions but no big plus + this.tryCanvasContext(); + if (this.props.initialized) { + this.drawCanvas(); + this.props.onChartRendered(); + } + } + + render() { + const { + forwardStageRef, + initialized, + isChartEmpty, + chartContainerDimensions: { width, height }, + } = this.props; + if (!initialized || width === 0 || height === 0) { + this.ctx = null; + return null; + } + + if (isChartEmpty) { + this.ctx = null; + return ( +
+

No data to display

+
+ ); + } + return ( + + ); + } +} + +const mapDispatchToProps = (dispatch: Dispatch): ReactiveChartDispatchProps => + bindActionCreators( + { + onChartRendered, + }, + dispatch, + ); + +const DEFAULT_PROPS: ReactiveChartStateProps = { + initialized: false, + debug: false, + isChartEmpty: true, + geometries: { + areas: [], + bars: [], + lines: [], + points: [], + }, + theme: LIGHT_THEME, + chartContainerDimensions: { + width: 0, + height: 0, + left: 0, + top: 0, + }, + chartRotation: 0 as 0, + chartDimensions: { + width: 0, + height: 0, + left: 0, + top: 0, + }, + chartTransform: { + x: 0, + y: 0, + rotate: 0, + }, + + axesSpecs: [], + axisTickPositions: { + axisGridLinesPositions: new Map(), + axisPositions: new Map(), + axisTicks: new Map(), + axisVisibleTicks: new Map(), + }, + axesTicksDimensions: new Map(), + axesGridLinesPositions: new Map(), + annotationDimensions: new Map(), + annotationSpecs: [], +}; + +const mapStateToProps = (state: GlobalChartState): ReactiveChartStateProps => { + if (!isInitialized(state)) { + return DEFAULT_PROPS; + } + return { + initialized: true, + isChartEmpty: isChartEmptySelector(state), + debug: getSettingsSpecSelector(state).debug, + geometries: computeSeriesGeometriesSelector(state).geometries, + theme: getChartThemeSelector(state), + chartContainerDimensions: getChartContainerDimensionsSelector(state), + highlightedLegendItem: getHighlightedSeriesSelector(state), + chartRotation: getChartRotationSelector(state), + chartDimensions: computeChartDimensionsSelector(state).chartDimensions, + chartTransform: computeChartTransformSelector(state), + axesSpecs: getAxisSpecsSelector(state), + axisTickPositions: computeAxisVisibleTicksSelector(state), + axesTicksDimensions: computeAxisTicksDimensionsSelector(state), + axesGridLinesPositions: computeAxisVisibleTicksSelector(state).axisGridLinesPositions, + annotationDimensions: computeAnnotationDimensionsSelector(state), + annotationSpecs: getAnnotationSpecsSelector(state), + }; +}; + +export const XYChart = connect(mapStateToProps, mapDispatchToProps)(XYChartComponent); diff --git a/src/chart_types/xy_chart/renderer/dom/brush.tsx b/src/chart_types/xy_chart/renderer/dom/brush.tsx index 42484297ea..ac0ab6be52 100644 --- a/src/chart_types/xy_chart/renderer/dom/brush.tsx +++ b/src/chart_types/xy_chart/renderer/dom/brush.tsx @@ -1,5 +1,4 @@ -import React from 'react'; -import { Layer, Rect, Stage } from 'react-konva'; +import React, { RefObject } from 'react'; import { connect } from 'react-redux'; import { Dimensions } from '../../../../utils/dimensions'; import { isInitialized } from '../../../../state/selectors/is_initialized'; @@ -8,10 +7,14 @@ import { getBrushAreaSelector } from '../../state/selectors/get_brush_area'; import { isBrushAvailableSelector } from '../../state/selectors/is_brush_available'; import { computeChartDimensionsSelector } from '../../state/selectors/compute_chart_dimensions'; import { isBrushingSelector } from '../../state/selectors/is_brushing'; +import { renderRect } from '../canvas/primitives/rect'; +import { clearCanvas, withContext, withClip } from '../../../../renderers/canvas'; +import { getChartContainerDimensionsSelector } from '../../../../state/selectors/get_chart_container_dimensions'; interface Props { initialized: boolean; chartDimensions: Dimensions; + chartContainerDimensions: Dimensions; isBrushing: boolean | undefined; isBrushAvailable: boolean | undefined; brushArea: Dimensions | null; @@ -19,37 +22,94 @@ interface Props { class BrushToolComponent extends React.Component { static displayName = 'BrushToolComponent'; + private readonly devicePixelRatio: number; + private ctx: CanvasRenderingContext2D | null; + private canvasRef: RefObject; - renderBrushTool = (brushArea: Dimensions | null) => { - if (!brushArea) { - return null; + constructor(props: Readonly) { + super(props); + this.ctx = null; + this.devicePixelRatio = window.devicePixelRatio; + this.canvasRef = React.createRef(); + } + private tryCanvasContext() { + const canvas = this.canvasRef.current; + this.ctx = canvas && canvas.getContext('2d'); + } + componentDidUpdate() { + if (!this.ctx) { + this.tryCanvasContext(); + } + if (this.props.initialized) { + this.drawCanvas(); + } + } + + componentDidMount() { + // the DOM element has just been appended, and getContext('2d') is always non-null, + // so we could use a couple of ! non-null assertions but no big plus + this.tryCanvasContext(); + this.drawCanvas(); + } + private drawCanvas = () => { + const { brushArea, chartDimensions } = this.props; + if (!this.ctx || !brushArea) { + return; } const { top, left, width, height } = brushArea; - return ; + withContext(this.ctx, (ctx) => { + ctx.scale(this.devicePixelRatio, this.devicePixelRatio); + withClip( + ctx, + { + x: chartDimensions.left, + y: chartDimensions.top, + width: chartDimensions.width, + height: chartDimensions.height, + }, + (ctx) => { + clearCanvas(ctx, 200000, 200000); + ctx.translate(chartDimensions.left, chartDimensions.top); + renderRect( + ctx, + { + x: left, + y: top, + width, + height, + }, + { + color: { + r: 128, + g: 128, + b: 128, + opacity: 0.6, + }, + }, + ); + }, + ); + }); }; render() { - const { initialized, isBrushAvailable, isBrushing, chartDimensions, brushArea } = this.props; + const { initialized, isBrushAvailable, isBrushing, chartContainerDimensions } = this.props; if (!initialized || !isBrushAvailable || !isBrushing) { + this.ctx = null; return null; } - + const { width, height } = chartContainerDimensions; return ( - - - {this.renderBrushTool(brushArea)} - - + /> ); } } @@ -61,6 +121,12 @@ const mapStateToProps = (state: GlobalChartState): Props => { isBrushing: false, isBrushAvailable: false, brushArea: null, + chartContainerDimensions: { + width: 0, + height: 0, + left: 0, + top: 0, + }, chartDimensions: { top: 0, left: 0, @@ -71,6 +137,7 @@ const mapStateToProps = (state: GlobalChartState): Props => { } return { initialized: state.specsInitialized, + chartContainerDimensions: getChartContainerDimensionsSelector(state), brushArea: getBrushAreaSelector(state), isBrushAvailable: isBrushAvailableSelector(state), chartDimensions: computeChartDimensionsSelector(state).chartDimensions, diff --git a/src/chart_types/xy_chart/state/chart_state.tsx b/src/chart_types/xy_chart/state/chart_state.tsx index 0ede503cf0..83932a45ae 100644 --- a/src/chart_types/xy_chart/state/chart_state.tsx +++ b/src/chart_types/xy_chart/state/chart_state.tsx @@ -9,13 +9,12 @@ import { AnnotationTooltip } from '../renderer/dom/annotation_tooltips'; import { isBrushAvailableSelector } from './selectors/is_brush_available'; import { BrushTool } from '../renderer/dom/brush'; import { isChartEmptySelector } from './selectors/is_chart_empty'; -import { ReactiveChart } from '../renderer/canvas/reactive_chart'; import { computeLegendSelector } from './selectors/compute_legend'; import { getLegendTooltipValuesSelector } from './selectors/get_legend_tooltip_values'; import { TooltipLegendValue } from '../tooltip/tooltip'; import { getPointerCursorSelector } from './selectors/get_cursor_pointer'; -import { Stage } from 'react-konva'; import { isBrushingSelector } from './selectors/is_brushing'; +import { XYChart } from '../renderer/canvas/xy_chart'; export class XYAxisChartState implements InternalChartState { chartType = ChartTypes.XYAxis; @@ -35,11 +34,11 @@ export class XYAxisChartState implements InternalChartState { getLegendItemsValues(globalState: GlobalChartState): Map { return getLegendTooltipValuesSelector(globalState); } - chartRenderer(containerRef: BackwardRef, forwardStageRef: RefObject) { + chartRenderer(containerRef: BackwardRef, forwardStageRef: RefObject) { return ( - + diff --git a/src/chart_types/xy_chart/state/selectors/compute_axis_visible_ticks.ts b/src/chart_types/xy_chart/state/selectors/compute_axis_visible_ticks.ts index 59bfa4358a..33dfe69554 100644 --- a/src/chart_types/xy_chart/state/selectors/compute_axis_visible_ticks.ts +++ b/src/chart_types/xy_chart/state/selectors/compute_axis_visible_ticks.ts @@ -13,7 +13,7 @@ import { AxisId } from '../../../../utils/ids'; import { Dimensions } from '../../../../utils/dimensions'; import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; -interface AxisVisibleTicks { +export interface AxisVisibleTicks { axisPositions: Map; axisTicks: Map; axisVisibleTicks: Map; diff --git a/src/chart_types/xy_chart/state/utils.ts b/src/chart_types/xy_chart/state/utils.ts index 34b1bd9364..3073bcc9a0 100644 --- a/src/chart_types/xy_chart/state/utils.ts +++ b/src/chart_types/xy_chart/state/utils.ts @@ -550,10 +550,10 @@ function renderGeometries( ? mergePartial(chartTheme.areaSeriesStyle, spec.areaSeriesStyle, { mergeOptionalPartialValues: true }) : chartTheme.areaSeriesStyle; const xScaleOffset = computeXScaleOffset(xScale, enableHistogramMode, spec.histogramModeAlignment); - const renderedAreas = renderArea( // move the point on half of the bandwidth if we have mixed bars/lines (xScale.bandwidth * areaShift) / 2, + ds, xScale, yScale, diff --git a/src/chart_types/xy_chart/utils/axis_utils.test.ts b/src/chart_types/xy_chart/utils/axis_utils.test.ts index c93bbeca4c..f90041dd1b 100644 --- a/src/chart_types/xy_chart/utils/axis_utils.test.ts +++ b/src/chart_types/xy_chart/utils/axis_utils.test.ts @@ -644,7 +644,11 @@ describe('Axis computational utils', () => { top: 0, left: 0, width: 100, - height: 100, + height: 10, + }; + const axisDimensions = { + maxLabelBboxWidth: 100, + maxLabelBboxHeight: 100, }; const unrotatedLabelProps = getTickLabelProps( tickLabelRotation, @@ -653,12 +657,14 @@ describe('Axis computational utils', () => { tickPosition, Position.Left, axisPosition, - axis1Dims, + axisDimensions, ); expect(unrotatedLabelProps).toEqual({ - x: 75, - y: -5, + offsetX: -50, + offsetY: 0, + x: 85, + y: 0, align: 'right', verticalAlign: 'middle', }); @@ -671,12 +677,14 @@ describe('Axis computational utils', () => { tickPosition, Position.Left, axisPosition, - axis1Dims, + axisDimensions, ); expect(rotatedLabelProps).toEqual({ - x: 75, - y: -5, + offsetX: -50, + offsetY: 0, + x: 85, + y: 0, align: 'center', verticalAlign: 'middle', }); @@ -688,12 +696,14 @@ describe('Axis computational utils', () => { tickPosition, Position.Right, axisPosition, - axis1Dims, + axisDimensions, ); expect(rightRotatedLabelProps).toEqual({ + offsetX: 50, + offsetY: 0, x: 15, - y: -5, + y: 0, align: 'center', verticalAlign: 'middle', }); @@ -706,12 +716,14 @@ describe('Axis computational utils', () => { tickPosition, Position.Right, axisPosition, - axis1Dims, + axisDimensions, ); expect(rightUnrotatedLabelProps).toEqual({ + offsetX: 50, + offsetY: 0, x: 15, - y: -5, + y: 0, align: 'left', verticalAlign: 'middle', }); @@ -726,9 +738,12 @@ describe('Axis computational utils', () => { top: 0, left: 0, width: 100, - height: 100, + height: 10, + }; + const axisDimensions = { + maxLabelBboxWidth: 100, + maxLabelBboxHeight: 100, }; - const unrotatedLabelProps = getTickLabelProps( tickLabelRotation, tickSize, @@ -736,12 +751,14 @@ describe('Axis computational utils', () => { tickPosition, Position.Top, axisPosition, - axis1Dims, + axisDimensions, ); expect(unrotatedLabelProps).toEqual({ - x: -5, - y: 75, + offsetX: 0, + offsetY: -50, + x: 0, + y: -5, align: 'center', verticalAlign: 'bottom', }); @@ -754,12 +771,14 @@ describe('Axis computational utils', () => { tickPosition, Position.Top, axisPosition, - axis1Dims, + axisDimensions, ); expect(rotatedLabelProps).toEqual({ - x: -5, - y: 75, + offsetX: 0, + offsetY: -50, + x: 0, + y: -5, align: 'center', verticalAlign: 'middle', }); @@ -771,11 +790,13 @@ describe('Axis computational utils', () => { tickPosition, Position.Bottom, axisPosition, - axis1Dims, + axisDimensions, ); expect(bottomRotatedLabelProps).toEqual({ - x: -5, + offsetX: 0, + offsetY: 50, + x: 0, y: 15, align: 'center', verticalAlign: 'middle', @@ -789,11 +810,13 @@ describe('Axis computational utils', () => { tickPosition, Position.Bottom, axisPosition, - axis1Dims, + axisDimensions, ); expect(bottomUnrotatedLabelProps).toEqual({ - x: -5, + offsetX: 0, + offsetY: 50, + x: 0, y: 15, align: 'center', verticalAlign: 'top', diff --git a/src/chart_types/xy_chart/utils/axis_utils.ts b/src/chart_types/xy_chart/utils/axis_utils.ts index d1f2b2c1dd..6a378a64d4 100644 --- a/src/chart_types/xy_chart/utils/axis_utils.ts +++ b/src/chart_types/xy_chart/utils/axis_utils.ts @@ -40,6 +40,8 @@ export interface AxisTicksDimensions { export interface TickLabelProps { x: number; y: number; + offsetX: number; + offsetY: number; align: string; verticalAlign: string; } @@ -286,39 +288,33 @@ export function getTickLabelProps( tickPosition: number, position: Position, axisPosition: Dimensions, - axisTicksDimensions: AxisTicksDimensions, + axisTickDimensions: Pick, ): TickLabelProps { - const { maxLabelBboxWidth, maxLabelBboxHeight } = axisTicksDimensions; + const { maxLabelBboxWidth, maxLabelBboxHeight } = axisTickDimensions; const isRotated = tickLabelRotation !== 0; - let align = 'center'; - let verticalAlign = 'middle'; - if (isVerticalAxis(position)) { const isLeftAxis = position === Position.Left; - - if (!isRotated) { - align = isLeftAxis ? 'right' : 'left'; - } - + const x = isLeftAxis ? axisPosition.width - tickSize - tickPadding : tickSize + tickPadding; + const offsetX = isLeftAxis ? -maxLabelBboxWidth / 2 : maxLabelBboxWidth / 2; return { - x: isLeftAxis ? axisPosition.width - tickSize - tickPadding - maxLabelBboxWidth : tickSize + tickPadding, - y: tickPosition - maxLabelBboxHeight / 2, - align, - verticalAlign, + x, + y: tickPosition, + offsetX, + offsetY: 0, + align: isRotated ? 'center' : isLeftAxis ? 'right' : 'left', + verticalAlign: 'middle', }; } const isAxisTop = position === Position.Top; - if (!isRotated) { - verticalAlign = isAxisTop ? 'bottom' : 'top'; - } - return { - x: tickPosition - maxLabelBboxWidth / 2, - y: isAxisTop ? axisPosition.height - tickSize - tickPadding - maxLabelBboxHeight : tickSize + tickPadding, - align, - verticalAlign, + x: tickPosition, + y: isAxisTop ? axisPosition.height - tickSize - tickPadding : tickSize + tickPadding, + offsetX: 0, + offsetY: isAxisTop ? -maxLabelBboxHeight / 2 : maxLabelBboxHeight / 2, + align: 'center', + verticalAlign: isRotated ? 'middle' : isAxisTop ? 'bottom' : 'top', }; } diff --git a/src/components/chart.tsx b/src/components/chart.tsx index 139268012a..af2edba66d 100644 --- a/src/components/chart.tsx +++ b/src/components/chart.tsx @@ -2,8 +2,6 @@ import React, { CSSProperties, createRef } from 'react'; import classNames from 'classnames'; import { Provider } from 'react-redux'; import { createStore, Store } from 'redux'; -import Konva from 'konva'; -import { Stage } from 'react-konva'; import uuid from 'uuid'; import { SpecsParser } from '../specs/specs_parser'; @@ -56,7 +54,7 @@ export class Chart extends React.Component { }; private chartStore: Store; private chartContainerRef: React.RefObject; - private chartStageRef: React.RefObject; + private chartStageRef: React.RefObject; constructor(props: any) { super(props); @@ -119,38 +117,29 @@ export class Chart extends React.Component { if (!this.chartStageRef.current) { return null; } - const stage = this.chartStageRef.current.getStage().clone(null); - const width = stage.getWidth(); - const height = stage.getHeight(); - const backgroundLayer = new Konva.Layer(); - const backgroundRect = new Konva.Rect({ - fill: options.backgroundColor, - x: 0, - y: 0, - width, - height, - }); + const canvas = this.chartStageRef.current; + const backgroundCanvas = document.createElement('canvas'); + backgroundCanvas.width = canvas.width; + backgroundCanvas.height = canvas.height; + const bgCtx = backgroundCanvas.getContext('2d'); + if (!bgCtx) { + return null; + } + bgCtx.fillStyle = options.backgroundColor; + bgCtx.fillRect(0, 0, canvas.width, canvas.height); + bgCtx.drawImage(canvas, 0, 0); - backgroundLayer.add(backgroundRect); - stage.add(backgroundLayer); - backgroundLayer.moveToBottom(); - stage.draw(); - const canvasStage = stage.toCanvas({ - width, - height, - callback: () => undefined, - }); // @ts-ignore - if (canvasStage.msToBlob) { + if (bgCtx.msToBlob) { // @ts-ignore - const blobOrDataUrl = canvasStage.msToBlob(); + const blobOrDataUrl = bgCtx.msToBlob(); return { blobOrDataUrl, browser: 'IE11', }; } else { return { - blobOrDataUrl: stage.toDataURL({ pixelRatio: options.pixelRatio }), + blobOrDataUrl: backgroundCanvas.toDataURL(), browser: 'other', }; } diff --git a/src/components/chart_container.tsx b/src/components/chart_container.tsx index 2ec5c8ca10..970ee7ab6a 100644 --- a/src/components/chart_container.tsx +++ b/src/components/chart_container.tsx @@ -11,7 +11,6 @@ import { isInternalChartEmptySelector } from '../state/selectors/is_chart_empty' import { isInitialized } from '../state/selectors/is_initialized'; import { getSettingsSpecSelector } from '../state/selectors/get_settings_specs'; import { SettingsSpec } from '../specs'; -import { Stage } from 'react-konva'; import { getInternalIsBrushingSelector } from '../state/selectors/get_internal_is_brushing'; interface ReactiveChartStateProps { @@ -21,7 +20,10 @@ interface ReactiveChartStateProps { isBrushing: boolean; isBrushingAvailable: boolean; settings?: SettingsSpec; - internalChartRenderer: (containerRef: BackwardRef, forwardStageRef: React.RefObject) => JSX.Element | null; + internalChartRenderer: ( + containerRef: BackwardRef, + forwardStageRef: React.RefObject, + ) => JSX.Element | null; } interface ReactiveChartDispatchProps { onPointerMove: typeof onPointerMove; @@ -31,7 +33,7 @@ interface ReactiveChartDispatchProps { interface ReactiveChartOwnProps { getChartContainerRef: BackwardRef; - forwardStageRef: React.RefObject; + forwardStageRef: React.RefObject; } type ReactiveChartProps = ReactiveChartStateProps & ReactiveChartDispatchProps & ReactiveChartOwnProps; diff --git a/src/components/react_canvas/globals.ts b/src/components/react_canvas/globals.ts deleted file mode 100644 index dcd15df01c..0000000000 --- a/src/components/react_canvas/globals.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { ShapeConfig } from 'konva/types/Shape'; - -export const GlobalKonvaElementProps: ShapeConfig = { - strokeHitEnabled: false, - perfectDrawEnabled: false, - listening: false, -}; diff --git a/src/components/react_canvas/grid.tsx b/src/components/react_canvas/grid.tsx deleted file mode 100644 index b1544153b1..0000000000 --- a/src/components/react_canvas/grid.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; -import { Group, Line } from 'react-konva'; -import { deepEqual } from '../../utils/fast_deep_equal'; -import { AxisLinePosition } from '../../chart_types/xy_chart/utils/axis_utils'; -import { GridLineConfig } from '../../utils/themes/theme'; -import { Dimensions } from '../../utils/dimensions'; - -interface GridProps { - chartDimensions: Dimensions; - debug: boolean; - gridLineStyle: GridLineConfig; - linesPositions: AxisLinePosition[]; -} - -export class Grid extends React.Component { - shouldComponentUpdate(nextProps: GridProps) { - return !deepEqual(this.props, nextProps); - } - - render() { - return this.renderGrid(); - } - private renderGridLine = (linePosition: AxisLinePosition, i: number) => { - const { gridLineStyle } = this.props; - - return ; - }; - - private renderGrid = () => { - const { chartDimensions, linesPositions } = this.props; - - return ( - - {linesPositions.map(this.renderGridLine)} - - ); - }; -} diff --git a/src/geoms/types.ts b/src/geoms/types.ts new file mode 100644 index 0000000000..cf83a62055 --- /dev/null +++ b/src/geoms/types.ts @@ -0,0 +1,62 @@ +import { RgbObject } from '../chart_types/partition_chart/layout/utils/d3_utils'; +import { Radian } from '../chart_types/partition_chart/layout/types/geometry_types'; +export interface Text { + text: string; + x: number; + y: number; +} +export interface Line { + x1: number; + y1: number; + x2: number; + y2: number; +} + +export interface Rect { + x: number; + y: number; + width: number; + height: number; +} + +export interface Arc { + x: number; + y: number; + radius: number; + startAngle: Radian; + endAngle: Radian; +} + +export interface Circle { + x: number; + y: number; + radius: number; +} + +/** + * Fill style for every geometry + */ +export interface Fill { + /** + * fill color in rgba + */ + color: RgbObject; +} + +/** + * Stroke style for every geometry + */ +export interface Stroke { + /** + * stroke rgba + */ + color: RgbObject; + /** + * stroke width + */ + width: number; + /** + * stroke dash array + */ + dash?: number[]; +} diff --git a/src/index.ts b/src/index.ts index c2a2667cc9..e7d04cf272 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,7 @@ export * from './utils/themes/theme_commons'; export { RecursivePartial } from './utils/commons'; export { CurveType } from './utils/curves'; export { timeFormatter, niceTimeFormatter, niceTimeFormatByDay } from './utils/data/formatters'; +import 'path2d-polyfill'; export { DataGenerator } from './utils/data_generators/data_generator'; export { SeriesCollectionValue } from './chart_types/xy_chart/utils/series'; export { ChartTypes } from './chart_types'; diff --git a/src/renderers/canvas/index.ts b/src/renderers/canvas/index.ts new file mode 100644 index 0000000000..b590427324 --- /dev/null +++ b/src/renderers/canvas/index.ts @@ -0,0 +1,110 @@ +import { Coordinate } from '../../chart_types/partition_chart/layout/types/geometry_types'; +import { ClippedRanges } from '../../utils/geometry'; +import { Rect } from '../../geoms/types'; +import { Point } from '../../utils/point'; + +/** + * withContext abstracts out the otherwise error-prone save/restore pairing; it can be nested and/or put into sequence + * The idea is that you just set what's needed for the enclosed snippet, which may temporarily override values in the + * outer withContext. Example: we use a +y = top convention, so when doing text rendering, y has to be flipped (ctx.scale) + * otherwise the text will render upside down. + * @param ctx + * @param fun + */ +export function withContext(ctx: CanvasRenderingContext2D, fun: (ctx: CanvasRenderingContext2D) => void) { + ctx.save(); + fun(ctx); + ctx.restore(); +} + +export function clearCanvas( + ctx: CanvasRenderingContext2D, + width: Coordinate, + height: Coordinate /*, backgroundColor: string*/, +) { + withContext(ctx, (ctx) => { + // two steps, as the backgroundColor may have a non-one opacity + // todo we should avoid `fillRect` by setting the element background via CSS + ctx.clearRect(-width, -height, 2 * width, 2 * height); // remove past contents + // ctx.fillStyle = backgroundColor; + // ctx.fillRect(-width, -height, 2 * width, 2 * height); // new background + }); +} + +// order of rendering is important; determined by the order of layers in the array +export function renderLayers(ctx: CanvasRenderingContext2D, layers: Array<(ctx: CanvasRenderingContext2D) => void>) { + layers.forEach((renderLayer) => renderLayer(ctx)); +} + +export function withClip( + ctx: CanvasRenderingContext2D, + clip: { x: number; y: number; width: number; height: number }, + fun: (ctx: CanvasRenderingContext2D) => void, +) { + withContext(ctx, (ctx) => { + const { x, y, width, height } = clip; + ctx.beginPath(); + ctx.rect(x, y, width, height); + ctx.clip(); + withContext(ctx, (ctx) => { + fun(ctx); + }); + }); +} + +/** + * Create clip from a set of clipped ranges + * + * @param clippedRanges ranges to be clipped from rendering + * @param clippings the general clipping + * @param negate show, rather than exclude, only selected ranges + */ +export function withClipRanges( + ctx: CanvasRenderingContext2D, + clippedRanges: ClippedRanges, + clippings: Rect, + negate = false, + fun: (ctx: CanvasRenderingContext2D) => void, +) { + withContext(ctx, (ctx) => { + const length = clippedRanges.length; + const { width, height } = clippings; + ctx.beginPath(); + if (negate) { + clippedRanges.forEach(([x0, x1]) => { + ctx.rect(x0, 0, x1 - x0, height); + }); + } else { + if (length > 0) { + ctx.rect(0, 0, clippedRanges[0][0], height); + const lastX = clippedRanges[length - 1][1]; + ctx.rect(lastX, 0, width - lastX, height); + } + + if (length > 1) { + for (let i = 1; i < length; i++) { + const [, x0] = clippedRanges[i - 1]; + const [x1] = clippedRanges[i]; + ctx.rect(x0, 0, x1 - x0, height); + } + } + } + ctx.clip(); + fun(ctx); + }); +} + +export function withRotatedOrigin( + ctx: CanvasRenderingContext2D, + origin: Point, + rotation: number = 0, + fn: (ctx: CanvasRenderingContext2D) => void, +) { + withContext(ctx, (ctx) => { + const { x, y } = origin; + ctx.translate(x, y); + ctx.rotate((rotation * Math.PI) / 180); + ctx.translate(-x, -y); + fn(ctx); + }); +} diff --git a/src/state/chart_state.ts b/src/state/chart_state.ts index d1df1ffa07..ce5f3769aa 100644 --- a/src/state/chart_state.ts +++ b/src/state/chart_state.ts @@ -14,7 +14,6 @@ import { CHART_RENDERED } from './actions/chart'; import { UPDATE_PARENT_DIMENSION } from './actions/chart_settings'; import { EXTERNAL_POINTER_EVENT } from './actions/events'; import { RefObject } from 'react'; -import { Stage } from 'react-konva'; import { PartitionState } from '../chart_types/partition_chart/state/chart_state'; export type BackwardRef = () => React.RefObject; @@ -27,7 +26,7 @@ export interface InternalChartState { // the chart type chartType: ChartTypes; // returns a JSX element with the chart rendered (lenged excluded) - chartRenderer(containerRef: BackwardRef, forwardStageRef: RefObject): JSX.Element | null; + chartRenderer(containerRef: BackwardRef, forwardStageRef: RefObject): JSX.Element | null; // true if the brush is available for this chart type isBrushAvailable(globalState: GlobalChartState): boolean; // true if the brush is available for this chart type diff --git a/src/state/selectors/get_chart_type_components.ts b/src/state/selectors/get_chart_type_components.ts index 0f87771481..b446608f3a 100644 --- a/src/state/selectors/get_chart_type_components.ts +++ b/src/state/selectors/get_chart_type_components.ts @@ -1,7 +1,9 @@ import { GlobalChartState, BackwardRef } from '../chart_state'; -import { Stage } from 'react-konva'; -type ChartRendererFn = (containerRef: BackwardRef, forwardStageRef: React.RefObject) => JSX.Element | null; +type ChartRendererFn = ( + containerRef: BackwardRef, + forwardStageRef: React.RefObject, +) => JSX.Element | null; export const getInternalChartRendererSelector = (state: GlobalChartState): ChartRendererFn => { if (state.internalChartState) { diff --git a/src/utils/themes/theme.ts b/src/utils/themes/theme.ts index 2782e4a958..7f7a40fc6b 100644 --- a/src/utils/themes/theme.ts +++ b/src/utils/themes/theme.ts @@ -190,6 +190,8 @@ export interface LineStyle { strokeWidth: number; /** the opacity of each line on the theme/series */ opacity: number; + /** the dash array */ + dash?: number[]; } export interface AreaStyle { diff --git a/tsconfig.lib.json b/tsconfig.lib.json index d4f45dd2c9..b997b4ec4f 100644 --- a/tsconfig.lib.json +++ b/tsconfig.lib.json @@ -1,6 +1,7 @@ { "compilerOptions": { - "noUnusedLocals": true + "noUnusedLocals": true, + "removeComments": true }, "extends": "./tsconfig", "include": ["src/**/*"], diff --git a/yarn.lock b/yarn.lock index a4347e3c9a..0aa8f9cf49 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2394,13 +2394,20 @@ dependencies: regenerator-runtime "^0.13.2" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3": version "7.6.3" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.6.3.tgz#935122c74c73d2240cafd32ddb5fc2a6cd35cf1f" integrity sha512-kq6anf9JGjW8Nt5rYfEuGRaEAaH1mkv3Bbu6rYvLOpPh/RusSJXuKPEAoZ7L7gybZkchE8+NV5g9vKF4AGAtsA== dependencies: regenerator-runtime "^0.13.2" +"@babel/runtime@^7.3.1": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.8.4.tgz#d79f5a2040f7caa24d53e563aad49cbc05581308" + integrity sha512-neAp3zt80trRVBI1x0azq6c57aNBqYZH8KhMm3TaB7wEI5Q4A2SHfBHE8w9gOhI/lrqxtEbXZgQIrHP+wvSGwQ== + dependencies: + regenerator-runtime "^0.13.2" + "@babel/runtime@^7.4.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.7.4.tgz#b23a856751e4bf099262f867767889c0e3fe175b" @@ -12201,11 +12208,6 @@ kleur@^3.0.3: resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== -konva@^4.0.18: - version "4.0.18" - resolved "https://registry.yarnpkg.com/konva/-/konva-4.0.18.tgz#43e614c9b22827183506d4a6b3b474f90187b469" - integrity sha512-Tlq0v7QHr8q73xr1cKjHdQl41oHC06IOldPO+ukjt99G74NgoU0TVouvPIFpW2whA9t3xNk/+/VJcc3XPcboOw== - kuler@1.0.x: version "1.0.1" resolved "https://registry.yarnpkg.com/kuler/-/kuler-1.0.1.tgz#ef7c784f36c9fb6e16dd3150d152677b2b0228a6" @@ -14714,6 +14716,11 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== +path2d-polyfill@^0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/path2d-polyfill/-/path2d-polyfill-0.4.2.tgz#594d3103838ef6b9dd4a7fd498fe9a88f1f28531" + integrity sha512-JSeAnUfkFjl+Ml/EZL898ivMSbGHrOH63Mirx5EQ1ycJiryHDmj1Q7Are+uEPvenVGCUN9YbolfGfyUewJfJEg== + pbkdf2@^3.0.3: version "3.0.17" resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.17.tgz#976c206530617b14ebb32114239f7b09336e93a6" @@ -15797,14 +15804,6 @@ react-is@~16.3.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.3.2.tgz#f4d3d0e2f5fbb6ac46450641eb2e25bf05d36b22" integrity sha512-ybEM7YOr4yBgFd6w8dJqwxegqZGJNBZl6U27HnGKuTZmDvVrD5quWOK/wAnMywiZzW+Qsk+l4X2c70+thp/A8Q== -react-konva@16.10.1-0: - version "16.10.1-0" - resolved "https://registry.yarnpkg.com/react-konva/-/react-konva-16.10.1-0.tgz#f8cc2c95374933069e891a6c714c70d0fdc77e68" - integrity sha512-N0Zi3TcWmUxb2d7y1DUDQhRA+WIcqk54DQmmUmJSadj+fS0bg6iZDebQSEQC8dMbjnLHc/338xRT4a4718PEiw== - dependencies: - react-reconciler "^0.22.1" - scheduler "^0.16.1" - react-lifecycles-compat@^3.0.0, react-lifecycles-compat@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" @@ -15830,16 +15829,6 @@ react-popper@^1.3.4: typed-styles "^0.0.7" warning "^4.0.2" -react-reconciler@^0.22.1: - version "0.22.2" - resolved "https://registry.yarnpkg.com/react-reconciler/-/react-reconciler-0.22.2.tgz#e8a10374fec8fee7c5cd0cf3cd05626f1b134d3e" - integrity sha512-MLX5Y2pNLsdXzWz/GLNhhYkdLOvxEtw2IGqVCzkiRdSFSHRjujI9gfTOQ3rV5z8toTBxSZ2qrRkRUo97mmEdhA== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - prop-types "^15.6.2" - scheduler "^0.16.2" - react-redux@^5.0.7: version "5.1.2" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.1.2.tgz#b19cf9e21d694422727bf798e934a916c4080f57" @@ -15889,14 +15878,6 @@ react-sizeme@^2.6.7: shallowequal "^1.1.0" throttle-debounce "^2.1.0" -react-spring@^8.0.8: - version "8.0.27" - resolved "https://registry.yarnpkg.com/react-spring/-/react-spring-8.0.27.tgz#97d4dee677f41e0b2adcb696f3839680a3aa356a" - integrity sha512-nDpWBe3ZVezukNRandTeLSPcwwTMjNVu1IDq9qA/AMiUqHuRN4BeSWvKr3eIxxg1vtiYiOLy4FqdfCP5IoP77g== - dependencies: - "@babel/runtime" "^7.3.1" - prop-types "^15.5.8" - react-syntax-highlighter@^11.0.2: version "11.0.2" resolved "https://registry.yarnpkg.com/react-syntax-highlighter/-/react-syntax-highlighter-11.0.2.tgz#4e3f376e752b20d2f54e4c55652fd663149e4029" @@ -16946,7 +16927,7 @@ saxes@^3.1.9: dependencies: xmlchars "^2.1.1" -scheduler@^0.16.1, scheduler@^0.16.2: +scheduler@^0.16.2: version "0.16.2" resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.16.2.tgz#f74cd9d33eff6fc554edfb79864868e4819132c1" integrity sha512-BqYVWqwz6s1wZMhjFvLfVR5WXP7ZY32M/wYPo04CcuPM7XZEbV2TBNW7Z0UkguPTl0dWMA59VbNXxK6q+pHItg==