diff --git a/.eslintrc.js b/.eslintrc.js index ee6d95107d..9aba28b983 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -69,7 +69,7 @@ module.exports = { '@typescript-eslint/no-unsafe-call': 0, // seems to have issues with default import types '@typescript-eslint/unbound-method': 1, '@typescript-eslint/no-redeclare': 0, // we use to declare enum type and object with the same name - '@typescript-eslint/no-shadow': 0, // we use shadow mostly within the canvas renderer function when we need a new context + '@typescript-eslint/no-shadow': 0, '@typescript-eslint/quotes': 0, '@typescript-eslint/no-unsafe-argument': 1, 'unicorn/consistent-function-scoping': 1, @@ -89,7 +89,7 @@ module.exports = { 'unicorn/number-literal-case': 0, // use prettier lower case preference 'global-require': 1, 'import/no-dynamic-require': 1, - 'no-shadow': 1, + 'no-shadow': ['warn', { allow: ['ctx'] }], // allow replacing ctx in canvas renderer functions, too tedious to rename at each level 'react/no-array-index-key': 1, 'react/prefer-stateless-function': 1, 'react/require-default-props': 0, diff --git a/e2e/helpers.ts b/e2e/helpers.ts index 47d3717381..f5ef67e9d5 100644 --- a/e2e/helpers.ts +++ b/e2e/helpers.ts @@ -37,7 +37,7 @@ export interface StoryGroupInfo { /** * Groups to skip in all vrt. */ -const groupsToSkip: Set = new Set(['Components/Tooltip']); +const groupsToSkip: Set = new Set(['Components/Tooltip', 'Bullet Graph']); /** * Stories to skip in all vrt based by group. diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-horizontal/bullet-as-metric/should-render-with-negative-values-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-horizontal/bullet-as-metric/should-render-with-negative-values-chrome-linux.png new file mode 100644 index 0000000000..d5604c7bf5 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-horizontal/bullet-as-metric/should-render-with-negative-values-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-horizontal/bullet-as-metric/should-render-with-negative-values-reversed-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-horizontal/bullet-as-metric/should-render-with-negative-values-reversed-chrome-linux.png new file mode 100644 index 0000000000..63a52119b4 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-horizontal/bullet-as-metric/should-render-with-negative-values-reversed-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-horizontal/bullet-as-metric/should-render-with-positive-negative-values-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-horizontal/bullet-as-metric/should-render-with-positive-negative-values-chrome-linux.png new file mode 100644 index 0000000000..78fcbc3cc2 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-horizontal/bullet-as-metric/should-render-with-positive-negative-values-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-horizontal/bullet-as-metric/should-render-with-positive-negative-values-reversed-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-horizontal/bullet-as-metric/should-render-with-positive-negative-values-reversed-chrome-linux.png new file mode 100644 index 0000000000..3285bb5b09 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-horizontal/bullet-as-metric/should-render-with-positive-negative-values-reversed-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-horizontal/bullet-as-metric/should-render-with-positive-values-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-horizontal/bullet-as-metric/should-render-with-positive-values-chrome-linux.png new file mode 100644 index 0000000000..5caa16ffa9 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-horizontal/bullet-as-metric/should-render-with-positive-values-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-horizontal/bullet-as-metric/should-render-with-positive-values-reversed-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-horizontal/bullet-as-metric/should-render-with-positive-values-reversed-chrome-linux.png new file mode 100644 index 0000000000..4187475dc7 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-horizontal/bullet-as-metric/should-render-with-positive-values-reversed-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-two-thirds-circle/bullet-as-metric/should-render-with-negative-values-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-two-thirds-circle/bullet-as-metric/should-render-with-negative-values-chrome-linux.png new file mode 100644 index 0000000000..f64892a453 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-two-thirds-circle/bullet-as-metric/should-render-with-negative-values-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-two-thirds-circle/bullet-as-metric/should-render-with-negative-values-reversed-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-two-thirds-circle/bullet-as-metric/should-render-with-negative-values-reversed-chrome-linux.png new file mode 100644 index 0000000000..3d24fdf42b Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-two-thirds-circle/bullet-as-metric/should-render-with-negative-values-reversed-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-two-thirds-circle/bullet-as-metric/should-render-with-positive-negative-values-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-two-thirds-circle/bullet-as-metric/should-render-with-positive-negative-values-chrome-linux.png new file mode 100644 index 0000000000..50780fe0fd Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-two-thirds-circle/bullet-as-metric/should-render-with-positive-negative-values-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-two-thirds-circle/bullet-as-metric/should-render-with-positive-negative-values-reversed-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-two-thirds-circle/bullet-as-metric/should-render-with-positive-negative-values-reversed-chrome-linux.png new file mode 100644 index 0000000000..017b0cc4e3 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-two-thirds-circle/bullet-as-metric/should-render-with-positive-negative-values-reversed-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-two-thirds-circle/bullet-as-metric/should-render-with-positive-values-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-two-thirds-circle/bullet-as-metric/should-render-with-positive-values-chrome-linux.png new file mode 100644 index 0000000000..95b61974b7 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-two-thirds-circle/bullet-as-metric/should-render-with-positive-values-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-two-thirds-circle/bullet-as-metric/should-render-with-positive-values-reversed-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-two-thirds-circle/bullet-as-metric/should-render-with-positive-values-reversed-chrome-linux.png new file mode 100644 index 0000000000..8a463c54ab Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-two-thirds-circle/bullet-as-metric/should-render-with-positive-values-reversed-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-vertical/bullet-as-metric/should-render-with-negative-values-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-vertical/bullet-as-metric/should-render-with-negative-values-chrome-linux.png new file mode 100644 index 0000000000..0ce850c87f Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-vertical/bullet-as-metric/should-render-with-negative-values-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-vertical/bullet-as-metric/should-render-with-negative-values-reversed-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-vertical/bullet-as-metric/should-render-with-negative-values-reversed-chrome-linux.png new file mode 100644 index 0000000000..c236370210 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-vertical/bullet-as-metric/should-render-with-negative-values-reversed-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-vertical/bullet-as-metric/should-render-with-positive-negative-values-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-vertical/bullet-as-metric/should-render-with-positive-negative-values-chrome-linux.png new file mode 100644 index 0000000000..2169268bb7 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-vertical/bullet-as-metric/should-render-with-positive-negative-values-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-vertical/bullet-as-metric/should-render-with-positive-negative-values-reversed-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-vertical/bullet-as-metric/should-render-with-positive-negative-values-reversed-chrome-linux.png new file mode 100644 index 0000000000..c5b6261396 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-vertical/bullet-as-metric/should-render-with-positive-negative-values-reversed-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-vertical/bullet-as-metric/should-render-with-positive-values-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-vertical/bullet-as-metric/should-render-with-positive-values-chrome-linux.png new file mode 100644 index 0000000000..c860f07f96 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-vertical/bullet-as-metric/should-render-with-positive-values-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-vertical/bullet-as-metric/should-render-with-positive-values-reversed-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-vertical/bullet-as-metric/should-render-with-positive-values-reversed-chrome-linux.png new file mode 100644 index 0000000000..971db405f9 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/bullet-as-metric-vertical/bullet-as-metric/should-render-with-positive-values-reversed-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/renders-angular-bullet-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/renders-angular-bullet-chrome-linux.png new file mode 100644 index 0000000000..c5f3c275cd Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/renders-angular-bullet-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/renders-grid-bullet-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/renders-grid-bullet-chrome-linux.png new file mode 100644 index 0000000000..9cf843ebe6 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/renders-grid-bullet-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/renders-single-bullet-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/renders-single-bullet-chrome-linux.png new file mode 100644 index 0000000000..047cae2378 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/renders-single-bullet-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/renders-single-column-bullet-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/renders-single-column-bullet-chrome-linux.png new file mode 100644 index 0000000000..ece0f31a9e Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/renders-single-column-bullet-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/renders-single-row-bullet-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/renders-single-row-bullet-chrome-linux.png new file mode 100644 index 0000000000..0120b81622 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/renders-single-row-bullet-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/nice-domain-false/should-render-with-negative-values-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/nice-domain-false/should-render-with-negative-values-chrome-linux.png new file mode 100644 index 0000000000..8345ecd44c Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/nice-domain-false/should-render-with-negative-values-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/nice-domain-false/should-render-with-negative-values-reversed-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/nice-domain-false/should-render-with-negative-values-reversed-chrome-linux.png new file mode 100644 index 0000000000..90cf22bac4 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/nice-domain-false/should-render-with-negative-values-reversed-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/nice-domain-false/should-render-with-positive-negative-values-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/nice-domain-false/should-render-with-positive-negative-values-chrome-linux.png new file mode 100644 index 0000000000..05b51901b6 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/nice-domain-false/should-render-with-positive-negative-values-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/nice-domain-false/should-render-with-positive-negative-values-reversed-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/nice-domain-false/should-render-with-positive-negative-values-reversed-chrome-linux.png new file mode 100644 index 0000000000..50d905e8d9 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/nice-domain-false/should-render-with-positive-negative-values-reversed-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/nice-domain-false/should-render-with-positive-values-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/nice-domain-false/should-render-with-positive-values-chrome-linux.png new file mode 100644 index 0000000000..1a4f0bb488 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/nice-domain-false/should-render-with-positive-values-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/nice-domain-false/should-render-with-positive-values-reversed-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/nice-domain-false/should-render-with-positive-values-reversed-chrome-linux.png new file mode 100644 index 0000000000..685ff710d5 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/nice-domain-false/should-render-with-positive-values-reversed-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/nice-domain-true/should-render-with-negative-values-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/nice-domain-true/should-render-with-negative-values-chrome-linux.png new file mode 100644 index 0000000000..c79b96b921 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/nice-domain-true/should-render-with-negative-values-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/nice-domain-true/should-render-with-negative-values-reversed-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/nice-domain-true/should-render-with-negative-values-reversed-chrome-linux.png new file mode 100644 index 0000000000..911a5c0828 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/nice-domain-true/should-render-with-negative-values-reversed-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/nice-domain-true/should-render-with-positive-negative-values-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/nice-domain-true/should-render-with-positive-negative-values-chrome-linux.png new file mode 100644 index 0000000000..5f91237c67 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/nice-domain-true/should-render-with-positive-negative-values-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/nice-domain-true/should-render-with-positive-negative-values-reversed-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/nice-domain-true/should-render-with-positive-negative-values-reversed-chrome-linux.png new file mode 100644 index 0000000000..1b3731922a Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/nice-domain-true/should-render-with-positive-negative-values-reversed-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/nice-domain-true/should-render-with-positive-values-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/nice-domain-true/should-render-with-positive-values-chrome-linux.png new file mode 100644 index 0000000000..3798b95d2e Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/nice-domain-true/should-render-with-positive-values-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/nice-domain-true/should-render-with-positive-values-reversed-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/nice-domain-true/should-render-with-positive-values-reversed-chrome-linux.png new file mode 100644 index 0000000000..70cb4d6e41 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/nice-domain-true/should-render-with-positive-values-reversed-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/should-render-colors-with-config-1-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/should-render-colors-with-config-1-chrome-linux.png new file mode 100644 index 0000000000..a464171c60 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/should-render-colors-with-config-1-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/should-render-colors-with-config-2-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/should-render-colors-with-config-2-chrome-linux.png new file mode 100644 index 0000000000..d2242a56fa Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/should-render-colors-with-config-2-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/should-render-colors-with-config-3-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/should-render-colors-with-config-3-chrome-linux.png new file mode 100644 index 0000000000..0446398747 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/should-render-colors-with-config-3-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/should-render-colors-with-config-4-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/should-render-colors-with-config-4-chrome-linux.png new file mode 100644 index 0000000000..d1c3c805d0 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/should-render-colors-with-config-4-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/should-render-colors-with-discrete-classes-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/should-render-colors-with-discrete-classes-chrome-linux.png new file mode 100644 index 0000000000..d56734ee06 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/should-render-colors-with-discrete-classes-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/should-render-in-dark-theme-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/should-render-in-dark-theme-chrome-linux.png new file mode 100644 index 0000000000..51f54c740e Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/should-render-in-dark-theme-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/ticks/should-render-with-auto-ticks-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/ticks/should-render-with-auto-ticks-chrome-linux.png new file mode 100644 index 0000000000..5d24184216 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/ticks/should-render-with-auto-ticks-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/ticks/should-render-with-explicit-tick-count-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/ticks/should-render-with-explicit-tick-count-chrome-linux.png new file mode 100644 index 0000000000..5d24184216 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/ticks/should-render-with-explicit-tick-count-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/ticks/should-render-with-explicit-tick-placements-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/ticks/should-render-with-explicit-tick-placements-chrome-linux.png new file mode 100644 index 0000000000..619b85e233 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-circle/ticks/should-render-with-explicit-tick-placements-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/nice-domain-false/should-render-with-negative-values-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/nice-domain-false/should-render-with-negative-values-chrome-linux.png new file mode 100644 index 0000000000..e8c9b60496 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/nice-domain-false/should-render-with-negative-values-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/nice-domain-false/should-render-with-negative-values-reversed-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/nice-domain-false/should-render-with-negative-values-reversed-chrome-linux.png new file mode 100644 index 0000000000..e33c36a389 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/nice-domain-false/should-render-with-negative-values-reversed-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/nice-domain-false/should-render-with-positive-negative-values-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/nice-domain-false/should-render-with-positive-negative-values-chrome-linux.png new file mode 100644 index 0000000000..16785477dd Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/nice-domain-false/should-render-with-positive-negative-values-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/nice-domain-false/should-render-with-positive-negative-values-reversed-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/nice-domain-false/should-render-with-positive-negative-values-reversed-chrome-linux.png new file mode 100644 index 0000000000..1ae0da51f0 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/nice-domain-false/should-render-with-positive-negative-values-reversed-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/nice-domain-false/should-render-with-positive-values-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/nice-domain-false/should-render-with-positive-values-chrome-linux.png new file mode 100644 index 0000000000..fd298cf2ee Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/nice-domain-false/should-render-with-positive-values-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/nice-domain-false/should-render-with-positive-values-reversed-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/nice-domain-false/should-render-with-positive-values-reversed-chrome-linux.png new file mode 100644 index 0000000000..a6f9729844 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/nice-domain-false/should-render-with-positive-values-reversed-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/nice-domain-true/should-render-with-negative-values-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/nice-domain-true/should-render-with-negative-values-chrome-linux.png new file mode 100644 index 0000000000..5f1ceddb2e Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/nice-domain-true/should-render-with-negative-values-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/nice-domain-true/should-render-with-negative-values-reversed-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/nice-domain-true/should-render-with-negative-values-reversed-chrome-linux.png new file mode 100644 index 0000000000..095e6119ce Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/nice-domain-true/should-render-with-negative-values-reversed-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/nice-domain-true/should-render-with-positive-negative-values-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/nice-domain-true/should-render-with-positive-negative-values-chrome-linux.png new file mode 100644 index 0000000000..05659c6aa8 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/nice-domain-true/should-render-with-positive-negative-values-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/nice-domain-true/should-render-with-positive-negative-values-reversed-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/nice-domain-true/should-render-with-positive-negative-values-reversed-chrome-linux.png new file mode 100644 index 0000000000..729dfa9de5 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/nice-domain-true/should-render-with-positive-negative-values-reversed-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/nice-domain-true/should-render-with-positive-values-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/nice-domain-true/should-render-with-positive-values-chrome-linux.png new file mode 100644 index 0000000000..c04ef98a51 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/nice-domain-true/should-render-with-positive-values-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/nice-domain-true/should-render-with-positive-values-reversed-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/nice-domain-true/should-render-with-positive-values-reversed-chrome-linux.png new file mode 100644 index 0000000000..8f0a9e158d Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/nice-domain-true/should-render-with-positive-values-reversed-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/should-render-colors-with-config-1-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/should-render-colors-with-config-1-chrome-linux.png new file mode 100644 index 0000000000..bf00527277 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/should-render-colors-with-config-1-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/should-render-colors-with-config-2-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/should-render-colors-with-config-2-chrome-linux.png new file mode 100644 index 0000000000..56ec5e01ce Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/should-render-colors-with-config-2-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/should-render-colors-with-config-3-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/should-render-colors-with-config-3-chrome-linux.png new file mode 100644 index 0000000000..7ec713fc79 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/should-render-colors-with-config-3-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/should-render-colors-with-config-4-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/should-render-colors-with-config-4-chrome-linux.png new file mode 100644 index 0000000000..12efc2f0b6 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/should-render-colors-with-config-4-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/should-render-colors-with-discrete-classes-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/should-render-colors-with-discrete-classes-chrome-linux.png new file mode 100644 index 0000000000..8c0a85d7e0 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/should-render-colors-with-discrete-classes-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/should-render-in-dark-theme-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/should-render-in-dark-theme-chrome-linux.png new file mode 100644 index 0000000000..66f1f6c987 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/should-render-in-dark-theme-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/ticks/should-render-with-auto-ticks-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/ticks/should-render-with-auto-ticks-chrome-linux.png new file mode 100644 index 0000000000..34b117ebde Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/ticks/should-render-with-auto-ticks-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/ticks/should-render-with-explicit-tick-count-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/ticks/should-render-with-explicit-tick-count-chrome-linux.png new file mode 100644 index 0000000000..aec0bc7271 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/ticks/should-render-with-explicit-tick-count-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/ticks/should-render-with-explicit-tick-placements-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/ticks/should-render-with-explicit-tick-placements-chrome-linux.png new file mode 100644 index 0000000000..15bcf673b6 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-half-circle/ticks/should-render-with-explicit-tick-placements-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/nice-domain-false/should-render-with-negative-values-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/nice-domain-false/should-render-with-negative-values-chrome-linux.png new file mode 100644 index 0000000000..e61fad6eee Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/nice-domain-false/should-render-with-negative-values-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/nice-domain-false/should-render-with-negative-values-reversed-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/nice-domain-false/should-render-with-negative-values-reversed-chrome-linux.png new file mode 100644 index 0000000000..b857195741 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/nice-domain-false/should-render-with-negative-values-reversed-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/nice-domain-false/should-render-with-positive-negative-values-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/nice-domain-false/should-render-with-positive-negative-values-chrome-linux.png new file mode 100644 index 0000000000..bb77d48173 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/nice-domain-false/should-render-with-positive-negative-values-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/nice-domain-false/should-render-with-positive-negative-values-reversed-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/nice-domain-false/should-render-with-positive-negative-values-reversed-chrome-linux.png new file mode 100644 index 0000000000..f89a4ecbb5 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/nice-domain-false/should-render-with-positive-negative-values-reversed-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/nice-domain-false/should-render-with-positive-values-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/nice-domain-false/should-render-with-positive-values-chrome-linux.png new file mode 100644 index 0000000000..b5c153a094 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/nice-domain-false/should-render-with-positive-values-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/nice-domain-false/should-render-with-positive-values-reversed-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/nice-domain-false/should-render-with-positive-values-reversed-chrome-linux.png new file mode 100644 index 0000000000..f423642041 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/nice-domain-false/should-render-with-positive-values-reversed-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/nice-domain-true/should-render-with-negative-values-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/nice-domain-true/should-render-with-negative-values-chrome-linux.png new file mode 100644 index 0000000000..94a0cb5047 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/nice-domain-true/should-render-with-negative-values-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/nice-domain-true/should-render-with-negative-values-reversed-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/nice-domain-true/should-render-with-negative-values-reversed-chrome-linux.png new file mode 100644 index 0000000000..28ef6da630 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/nice-domain-true/should-render-with-negative-values-reversed-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/nice-domain-true/should-render-with-positive-negative-values-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/nice-domain-true/should-render-with-positive-negative-values-chrome-linux.png new file mode 100644 index 0000000000..a58ed4e670 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/nice-domain-true/should-render-with-positive-negative-values-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/nice-domain-true/should-render-with-positive-negative-values-reversed-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/nice-domain-true/should-render-with-positive-negative-values-reversed-chrome-linux.png new file mode 100644 index 0000000000..49d696587b Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/nice-domain-true/should-render-with-positive-negative-values-reversed-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/nice-domain-true/should-render-with-positive-values-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/nice-domain-true/should-render-with-positive-values-chrome-linux.png new file mode 100644 index 0000000000..9e0a94ceb7 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/nice-domain-true/should-render-with-positive-values-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/nice-domain-true/should-render-with-positive-values-reversed-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/nice-domain-true/should-render-with-positive-values-reversed-chrome-linux.png new file mode 100644 index 0000000000..a4765b518d Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/nice-domain-true/should-render-with-positive-values-reversed-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/should-render-colors-with-config-1-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/should-render-colors-with-config-1-chrome-linux.png new file mode 100644 index 0000000000..c065112732 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/should-render-colors-with-config-1-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/should-render-colors-with-config-2-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/should-render-colors-with-config-2-chrome-linux.png new file mode 100644 index 0000000000..01b64f6b63 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/should-render-colors-with-config-2-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/should-render-colors-with-config-3-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/should-render-colors-with-config-3-chrome-linux.png new file mode 100644 index 0000000000..dd1213aa1e Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/should-render-colors-with-config-3-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/should-render-colors-with-config-4-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/should-render-colors-with-config-4-chrome-linux.png new file mode 100644 index 0000000000..a47e5d1d53 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/should-render-colors-with-config-4-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/should-render-colors-with-discrete-classes-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/should-render-colors-with-discrete-classes-chrome-linux.png new file mode 100644 index 0000000000..0f625cc41e Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/should-render-colors-with-discrete-classes-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/should-render-in-dark-theme-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/should-render-in-dark-theme-chrome-linux.png new file mode 100644 index 0000000000..add00c058b Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/should-render-in-dark-theme-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/ticks/should-render-with-auto-ticks-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/ticks/should-render-with-auto-ticks-chrome-linux.png new file mode 100644 index 0000000000..8d7d60d064 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/ticks/should-render-with-auto-ticks-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/ticks/should-render-with-explicit-tick-count-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/ticks/should-render-with-explicit-tick-count-chrome-linux.png new file mode 100644 index 0000000000..4fc7b5c234 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/ticks/should-render-with-explicit-tick-count-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/ticks/should-render-with-explicit-tick-placements-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/ticks/should-render-with-explicit-tick-placements-chrome-linux.png new file mode 100644 index 0000000000..64aa3e5d98 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-horizontal/ticks/should-render-with-explicit-tick-placements-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/nice-domain-false/should-render-with-negative-values-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/nice-domain-false/should-render-with-negative-values-chrome-linux.png new file mode 100644 index 0000000000..bcaba22ec6 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/nice-domain-false/should-render-with-negative-values-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/nice-domain-false/should-render-with-negative-values-reversed-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/nice-domain-false/should-render-with-negative-values-reversed-chrome-linux.png new file mode 100644 index 0000000000..45cfd34d47 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/nice-domain-false/should-render-with-negative-values-reversed-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/nice-domain-false/should-render-with-positive-negative-values-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/nice-domain-false/should-render-with-positive-negative-values-chrome-linux.png new file mode 100644 index 0000000000..54de2e1174 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/nice-domain-false/should-render-with-positive-negative-values-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/nice-domain-false/should-render-with-positive-negative-values-reversed-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/nice-domain-false/should-render-with-positive-negative-values-reversed-chrome-linux.png new file mode 100644 index 0000000000..c928fa4684 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/nice-domain-false/should-render-with-positive-negative-values-reversed-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/nice-domain-false/should-render-with-positive-values-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/nice-domain-false/should-render-with-positive-values-chrome-linux.png new file mode 100644 index 0000000000..3cec8c08e6 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/nice-domain-false/should-render-with-positive-values-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/nice-domain-false/should-render-with-positive-values-reversed-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/nice-domain-false/should-render-with-positive-values-reversed-chrome-linux.png new file mode 100644 index 0000000000..4a62f6f5e8 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/nice-domain-false/should-render-with-positive-values-reversed-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/nice-domain-true/should-render-with-negative-values-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/nice-domain-true/should-render-with-negative-values-chrome-linux.png new file mode 100644 index 0000000000..d2f26a76d4 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/nice-domain-true/should-render-with-negative-values-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/nice-domain-true/should-render-with-negative-values-reversed-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/nice-domain-true/should-render-with-negative-values-reversed-chrome-linux.png new file mode 100644 index 0000000000..64abe1bed7 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/nice-domain-true/should-render-with-negative-values-reversed-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/nice-domain-true/should-render-with-positive-negative-values-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/nice-domain-true/should-render-with-positive-negative-values-chrome-linux.png new file mode 100644 index 0000000000..f84ecc5722 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/nice-domain-true/should-render-with-positive-negative-values-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/nice-domain-true/should-render-with-positive-negative-values-reversed-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/nice-domain-true/should-render-with-positive-negative-values-reversed-chrome-linux.png new file mode 100644 index 0000000000..073914144f Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/nice-domain-true/should-render-with-positive-negative-values-reversed-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/nice-domain-true/should-render-with-positive-values-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/nice-domain-true/should-render-with-positive-values-chrome-linux.png new file mode 100644 index 0000000000..a0e9d87dff Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/nice-domain-true/should-render-with-positive-values-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/nice-domain-true/should-render-with-positive-values-reversed-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/nice-domain-true/should-render-with-positive-values-reversed-chrome-linux.png new file mode 100644 index 0000000000..dbad42f10d Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/nice-domain-true/should-render-with-positive-values-reversed-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/should-render-colors-with-config-1-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/should-render-colors-with-config-1-chrome-linux.png new file mode 100644 index 0000000000..5759c13500 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/should-render-colors-with-config-1-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/should-render-colors-with-config-2-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/should-render-colors-with-config-2-chrome-linux.png new file mode 100644 index 0000000000..dd49eaf143 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/should-render-colors-with-config-2-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/should-render-colors-with-config-3-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/should-render-colors-with-config-3-chrome-linux.png new file mode 100644 index 0000000000..adde888770 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/should-render-colors-with-config-3-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/should-render-colors-with-config-4-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/should-render-colors-with-config-4-chrome-linux.png new file mode 100644 index 0000000000..4a2443e999 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/should-render-colors-with-config-4-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/should-render-colors-with-discrete-classes-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/should-render-colors-with-discrete-classes-chrome-linux.png new file mode 100644 index 0000000000..74694b88ac Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/should-render-colors-with-discrete-classes-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/should-render-in-dark-theme-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/should-render-in-dark-theme-chrome-linux.png new file mode 100644 index 0000000000..aec5134343 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/should-render-in-dark-theme-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/ticks/should-render-with-auto-ticks-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/ticks/should-render-with-auto-ticks-chrome-linux.png new file mode 100644 index 0000000000..97869903c8 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/ticks/should-render-with-auto-ticks-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/ticks/should-render-with-explicit-tick-count-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/ticks/should-render-with-explicit-tick-count-chrome-linux.png new file mode 100644 index 0000000000..97869903c8 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/ticks/should-render-with-explicit-tick-count-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/ticks/should-render-with-explicit-tick-placements-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/ticks/should-render-with-explicit-tick-placements-chrome-linux.png new file mode 100644 index 0000000000..8cc6965b73 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-two-thirds-circle/ticks/should-render-with-explicit-tick-placements-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/nice-domain-false/should-render-with-negative-values-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/nice-domain-false/should-render-with-negative-values-chrome-linux.png new file mode 100644 index 0000000000..9e5402340b Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/nice-domain-false/should-render-with-negative-values-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/nice-domain-false/should-render-with-negative-values-reversed-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/nice-domain-false/should-render-with-negative-values-reversed-chrome-linux.png new file mode 100644 index 0000000000..d2547c3f23 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/nice-domain-false/should-render-with-negative-values-reversed-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/nice-domain-false/should-render-with-positive-negative-values-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/nice-domain-false/should-render-with-positive-negative-values-chrome-linux.png new file mode 100644 index 0000000000..09bd4c77f2 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/nice-domain-false/should-render-with-positive-negative-values-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/nice-domain-false/should-render-with-positive-negative-values-reversed-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/nice-domain-false/should-render-with-positive-negative-values-reversed-chrome-linux.png new file mode 100644 index 0000000000..123223834e Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/nice-domain-false/should-render-with-positive-negative-values-reversed-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/nice-domain-false/should-render-with-positive-values-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/nice-domain-false/should-render-with-positive-values-chrome-linux.png new file mode 100644 index 0000000000..7507e2cd68 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/nice-domain-false/should-render-with-positive-values-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/nice-domain-false/should-render-with-positive-values-reversed-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/nice-domain-false/should-render-with-positive-values-reversed-chrome-linux.png new file mode 100644 index 0000000000..6c91be4521 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/nice-domain-false/should-render-with-positive-values-reversed-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/nice-domain-true/should-render-with-negative-values-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/nice-domain-true/should-render-with-negative-values-chrome-linux.png new file mode 100644 index 0000000000..c71283981e Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/nice-domain-true/should-render-with-negative-values-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/nice-domain-true/should-render-with-negative-values-reversed-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/nice-domain-true/should-render-with-negative-values-reversed-chrome-linux.png new file mode 100644 index 0000000000..890f50b836 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/nice-domain-true/should-render-with-negative-values-reversed-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/nice-domain-true/should-render-with-positive-negative-values-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/nice-domain-true/should-render-with-positive-negative-values-chrome-linux.png new file mode 100644 index 0000000000..4450388bd0 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/nice-domain-true/should-render-with-positive-negative-values-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/nice-domain-true/should-render-with-positive-negative-values-reversed-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/nice-domain-true/should-render-with-positive-negative-values-reversed-chrome-linux.png new file mode 100644 index 0000000000..7f16837b20 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/nice-domain-true/should-render-with-positive-negative-values-reversed-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/nice-domain-true/should-render-with-positive-values-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/nice-domain-true/should-render-with-positive-values-chrome-linux.png new file mode 100644 index 0000000000..fe94a65d52 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/nice-domain-true/should-render-with-positive-values-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/nice-domain-true/should-render-with-positive-values-reversed-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/nice-domain-true/should-render-with-positive-values-reversed-chrome-linux.png new file mode 100644 index 0000000000..d5b8a6f997 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/nice-domain-true/should-render-with-positive-values-reversed-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/should-render-colors-with-config-1-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/should-render-colors-with-config-1-chrome-linux.png new file mode 100644 index 0000000000..47efe09dee Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/should-render-colors-with-config-1-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/should-render-colors-with-config-2-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/should-render-colors-with-config-2-chrome-linux.png new file mode 100644 index 0000000000..1356eb1bf6 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/should-render-colors-with-config-2-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/should-render-colors-with-config-3-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/should-render-colors-with-config-3-chrome-linux.png new file mode 100644 index 0000000000..96a6511abe Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/should-render-colors-with-config-3-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/should-render-colors-with-config-4-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/should-render-colors-with-config-4-chrome-linux.png new file mode 100644 index 0000000000..5ccb774625 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/should-render-colors-with-config-4-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/should-render-colors-with-discrete-classes-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/should-render-colors-with-discrete-classes-chrome-linux.png new file mode 100644 index 0000000000..362c44d050 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/should-render-colors-with-discrete-classes-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/should-render-in-dark-theme-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/should-render-in-dark-theme-chrome-linux.png new file mode 100644 index 0000000000..57bb132717 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/should-render-in-dark-theme-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/ticks/should-render-with-auto-ticks-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/ticks/should-render-with-auto-ticks-chrome-linux.png new file mode 100644 index 0000000000..95ec6e2ae5 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/ticks/should-render-with-auto-ticks-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/ticks/should-render-with-explicit-tick-count-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/ticks/should-render-with-explicit-tick-count-chrome-linux.png new file mode 100644 index 0000000000..3764619274 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/ticks/should-render-with-explicit-tick-count-chrome-linux.png differ diff --git a/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/ticks/should-render-with-explicit-tick-placements-chrome-linux.png b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/ticks/should-render-with-explicit-tick-placements-chrome-linux.png new file mode 100644 index 0000000000..8e6f549a06 Binary files /dev/null and b/e2e/screenshots/bullet_stories.test.ts-snapshots/bullet-stories/subtype-vertical/ticks/should-render-with-explicit-tick-placements-chrome-linux.png differ diff --git a/e2e/screenshots/chart.test.ts-snapshots/chart/sizing/should-accommodate-chart-title-and-description-metric-sm-chrome-linux.png b/e2e/screenshots/chart.test.ts-snapshots/chart/sizing/should-accommodate-chart-title-and-description-metric-sm-chrome-linux.png index 534ece25ce..5219395a41 100644 Binary files a/e2e/screenshots/chart.test.ts-snapshots/chart/sizing/should-accommodate-chart-title-and-description-metric-sm-chrome-linux.png and b/e2e/screenshots/chart.test.ts-snapshots/chart/sizing/should-accommodate-chart-title-and-description-metric-sm-chrome-linux.png differ diff --git a/e2e/tests/bullet_stories.test.ts b/e2e/tests/bullet_stories.test.ts new file mode 100644 index 0000000000..3345385298 --- /dev/null +++ b/e2e/tests/bullet_stories.test.ts @@ -0,0 +1,141 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { test } from '@playwright/test'; + +import { pwEach } from '../helpers'; +import { common } from '../page_objects/common'; + +export const BulletGraphSubtype = ['vertical', 'horizontal', 'circle', 'half-circle', 'two-thirds-circle']; +const testCases: [string, { start: number; end: number; value: number; target: number }][] = [ + ['positive values', { start: 4, end: 167, value: 50, target: 100 }], + ['positive values - reversed', { start: 167, end: 4, value: 50, target: 100 }], + ['positive/negative values', { start: -57, end: 97, value: -12, target: 50 }], + ['positive/negative values - reversed', { start: 97, end: -57, value: -12, target: 50 }], + ['negative values', { start: -194, end: -5, value: -50, target: -150 }], + ['negative values - reversed', { start: -5, end: -194, value: -50, target: -150 }], +]; + +test.describe('Bullet stories', () => { + test('renders single bullet', async ({ page }) => { + await common.expectChartAtUrlToMatchScreenshot(page)('http://localhost:9001/?path=/story/bullet-graph--single'); + }); + + test('renders angular bullet', async ({ page }) => { + await page.setViewportSize({ width: 785, height: 800 }); + await common.expectChartAtUrlToMatchScreenshot(page)('http://localhost:9001/?path=/story/bullet-graph--angular'); + }); + + test('renders single row bullet', async ({ page }) => { + await page.setViewportSize({ width: 850, height: 800 }); + await common.expectChartAtUrlToMatchScreenshot(page)('http://localhost:9001/?path=/story/bullet-graph--single-row'); + }); + + test('renders single column bullet', async ({ page }) => { + await common.expectChartAtUrlToMatchScreenshot(page)( + 'http://localhost:9001/?path=/story/bullet-graph--single-column', + ); + }); + + test('renders grid bullet', async ({ page }) => { + await page.setViewportSize({ width: 785, height: 1000 }); + await common.expectChartAtUrlToMatchScreenshot(page)('http://localhost:9001/?path=/story/bullet-graph--grid'); + }); + + pwEach.describe(BulletGraphSubtype)( + (subtype) => `subtype - ${subtype}`, + (subtype) => { + test('should render in dark theme', async ({ page }) => { + await page.setViewportSize({ width: 785, height: 800 }); + await common.expectChartAtUrlToMatchScreenshot(page)( + `http://localhost:9001/?path=/story/bullet-graph--single&globals=toggles.showHeader:true;toggles.showChartTitle:false;toggles.showChartDescription:false;toggles.showChartBoundary:false;theme:dark&knob-Config 1 - Color_Color Bands=RGBA(70, 130, 96, 1)&knob-Config 2 - Steps_Color Bands=5&knob-Config 3 - json_Color Bands={"classes":[0,20,40,100],"colors":["&knob-debug=&knob-title_General=Error rate title&knob-subtitle_General=Here is the subtitle&knob-value_General=56&knob-target_General=75&knob-start_General=0&knob-end_General=100&knob-format (numeraljs)_General=0.[0]&knob-subtype_General=${subtype}&knob-niceDomain_Ticks=&knob-tick strategy_Ticks=auto&knob-ticks(approx. count)_Ticks=5&knob-ticks(placements)_Ticks[0]=-200&knob-ticks(placements)_Ticks[1]=-100&knob-ticks(placements)_Ticks[2]=0&knob-ticks(placements)_Ticks[3]=5&knob-ticks(placements)_Ticks[4]=10&knob-ticks(placements)_Ticks[5]=15&knob-ticks(placements)_Ticks[6]=20&knob-ticks(placements)_Ticks[7]=25&knob-ticks(placements)_Ticks[8]=50&knob-ticks(placements)_Ticks[9]=100&knob-ticks(placements)_Ticks[10]=200`, + ); + }); + + test.describe('Ticks', () => { + test('should render with auto ticks', async ({ page }) => { + await page.setViewportSize({ width: 785, height: 800 }); + await common.expectChartAtUrlToMatchScreenshot(page)( + `http://localhost:9001/?path=/story/bullet-graph--single&globals=toggles.showHeader:true;toggles.showChartTitle:false;toggles.showChartDescription:false;toggles.showChartBoundary:false;theme:light&knob-end_General=100&knob-format (numeraljs)_General=0.[0]&knob-niceDomain_Ticks=true&knob-start_General=0&knob-subtype_General=${subtype}&knob-target_General=81&knob-tick strategy_Ticks=auto&knob-ticks(approx. count)_Ticks=10&knob-ticks(placements)_Ticks[0]=-200&knob-ticks(placements)_Ticks[1]=-100&knob-ticks(placements)_Ticks[2]=0&knob-ticks(placements)_Ticks[3]=5&knob-ticks(placements)_Ticks[4]=10&knob-ticks(placements)_Ticks[5]=15&knob-ticks(placements)_Ticks[6]=20&knob-ticks(placements)_Ticks[7]=25&knob-ticks(placements)_Ticks[8]=50&knob-ticks(placements)_Ticks[9]=100&knob-ticks(placements)_Ticks[10]=200&knob-title_General=Error rate&knob-value_General=57&knob-debug=&knob-subtitle_General=`, + ); + }); + + test('should render with explicit tick count', async ({ page }) => { + await page.setViewportSize({ width: 785, height: 800 }); + await common.expectChartAtUrlToMatchScreenshot(page)( + `http://localhost:9002/?path=/story/bullet-graph--single&globals=toggles.showHeader:true;toggles.showChartTitle:false;toggles.showChartDescription:false;toggles.showChartBoundary:false;theme:light&knob-end_General=100&knob-format%20(numeraljs)_General=0.[0]&knob-niceDomain_Ticks=true&knob-start_General=0&knob-subtype_General=${subtype}&knob-target_General=81&knob-tick%20strategy_Ticks[0]=count&knob-tick%20strategy_Ticks[1]=auto&knob-ticks(approx.%20count)_Ticks=14&knob-ticks(placements)_Ticks[0]=-200&knob-ticks(placements)_Ticks[1]=-100&knob-ticks(placements)_Ticks[2]=0&knob-ticks(placements)_Ticks[3]=5&knob-ticks(placements)_Ticks[4]=10&knob-ticks(placements)_Ticks[5]=15&knob-ticks(placements)_Ticks[6]=20&knob-ticks(placements)_Ticks[7]=25&knob-ticks(placements)_Ticks[8]=50&knob-ticks(placements)_Ticks[9]=100&knob-ticks(placements)_Ticks[10]=200&knob-title_General=Error%20rate&knob-value_General=57&knob-debug=&knob-subtitle_General=`, + ); + }); + + test('should render with explicit tick placements', async ({ page }) => { + await page.setViewportSize({ width: 785, height: 800 }); + await common.expectChartAtUrlToMatchScreenshot(page)( + `http://localhost:9001/?path=/story/bullet-graph--single&globals=toggles.showHeader:true;toggles.showChartTitle:false;toggles.showChartDescription:false;toggles.showChartBoundary:false;theme:light&knob-end_General=100&knob-format (numeraljs)_General=0.[0]&knob-niceDomain_Ticks=true&knob-start_General=0&knob-subtype_General=${subtype}&knob-target_General=81&knob-tick strategy_Ticks=placements&knob-ticks(approx. count)_Ticks=16&knob-ticks(placements)_Ticks[0]=-200&knob-ticks(placements)_Ticks[1]=-100&knob-ticks(placements)_Ticks[2]=0&knob-ticks(placements)_Ticks[3]=5&knob-ticks(placements)_Ticks[4]=10&knob-ticks(placements)_Ticks[5]=15&knob-ticks(placements)_Ticks[6]=20&knob-ticks(placements)_Ticks[7]=25&knob-ticks(placements)_Ticks[8]=50&knob-ticks(placements)_Ticks[9]=100&knob-ticks(placements)_Ticks[10]=200&knob-title_General=Error rate&knob-value_General=57&knob-debug=&knob-subtitle_General=`, + ); + }); + }); + + // Each color config type + pwEach.test([1, 2, 3, 4])( + (v) => `should render colors with config - ${v}`, + async (page, configIndex) => { + await page.setViewportSize({ width: 785, height: 800 }); + await common.expectChartAtUrlToMatchScreenshot(page)( + `http://localhost:9001/?path=/story/bullet-graph--color-bands&globals=toggles.showHeader:true;toggles.showChartTitle:false;toggles.showChartDescription:false;toggles.showChartBoundary:false;theme:light&knob-color config_Color Bands=${configIndex}&knob-Config 1 - Color_Color Bands=RGBA(70, 130, 96, 1)&knob-Config 2 - Palette_Color Bands=0&knob-Config 2 - Steps_Color Bands=5&knob-Config 2 - Reverse_Color Bands=&knob-Config 3 - json_Color Bands={"classes":5,"colors":["pink","yellow","blue"]}&knob-Config 4 - json_Color Bands=[{"color":"red","gte":0,"lt":20},{"color":"green","gte":20,"lte":40},{"color":"blue","gt":40,"lte":{"type":"percentage","value":100}}]&knob-start_Domain=0&knob-end_Domain=100&knob-value_Domain=56&knob-target_Domain=75&knob-niceDomain_Ticks=&knob-tick strategy_Ticks=auto&knob-ticks(approx. count)_Ticks=5&knob-ticks(placements)_Ticks[0]=-200&knob-ticks(placements)_Ticks[1]=-100&knob-ticks(placements)_Ticks[2]=0&knob-ticks(placements)_Ticks[3]=5&knob-ticks(placements)_Ticks[4]=10&knob-ticks(placements)_Ticks[5]=15&knob-ticks(placements)_Ticks[6]=20&knob-ticks(placements)_Ticks[7]=25&knob-ticks(placements)_Ticks[8]=50&knob-ticks(placements)_Ticks[9]=100&knob-ticks(placements)_Ticks[10]=200&knob-debug=&knob-subtype=${subtype}`, + ); + }, + ); + + test('should render colors with discrete classes', async ({ page }) => { + await page.setViewportSize({ width: 785, height: 800 }); + await common.expectChartAtUrlToMatchScreenshot(page)( + `http://localhost:9001/?path=/story/bullet-graph--color-bands&globals=toggles.showHeader:true;toggles.showChartTitle:false;toggles.showChartDescription:false;toggles.showChartBoundary:false;theme:light&knob-Config 1 - Color_Color Bands=RGBA(70, 130, 96, 1)&knob-Config 2 - Steps_Color Bands=5&knob-Config 3 - json_Color Bands={"classes":[0,20,40,100],"colors":["pink","yellow","blue"]}&knob-Config 4 - json_Color Bands=[]&knob-color config_Color Bands=3&knob-end_Domain=100&knob-start_Domain=0&knob-subtype=${subtype}&knob-target_Domain=75&knob-tick strategy_Ticks=auto&knob-ticks(approx. count)_Ticks=5&knob-ticks(placements)_Ticks[0]=-200&knob-ticks(placements)_Ticks[1]=-100&knob-ticks(placements)_Ticks[2]=0&knob-ticks(placements)_Ticks[3]=5&knob-ticks(placements)_Ticks[4]=10&knob-ticks(placements)_Ticks[5]=15&knob-ticks(placements)_Ticks[6]=20&knob-ticks(placements)_Ticks[7]=25&knob-ticks(placements)_Ticks[8]=50&knob-ticks(placements)_Ticks[9]=100&knob-ticks(placements)_Ticks[10]=200&knob-value_Domain=56&knob-Config 2 - Palette_Color Bands=0&knob-Config 2 - Reverse_Color Bands=&knob-niceDomain_Ticks=&knob-debug=`, + ); + }); + + pwEach.describe([true, false])( + (d) => `Nice domain - ${d}`, + (niceDomain) => { + pwEach.test(testCases)( + ([v]) => `should render with ${v}`, + async (page, [, { start, end, target, value }]) => { + await page.setViewportSize({ width: 785, height: 800 }); + await common.expectChartAtUrlToMatchScreenshot(page)( + `http://localhost:9001/?path=/story/bullet-graph--single&globals=toggles.showHeader:true;toggles.showChartTitle:false;toggles.showChartDescription:false;toggles.showChartBoundary:false;theme:light&knob-debug=&knob-title_General=Error rate&knob-subtitle_General=&knob-value_General=${value}&knob-target_General=${target}&knob-start_General=${start}&knob-end_General=${end}&knob-format (numeraljs)_General=0.[0]&knob-subtype_General=${subtype}&knob-niceDomain_Ticks=${niceDomain}&knob-tick strategy_Ticks=auto&knob-ticks(approx. count)_Ticks=10&knob-ticks(placements)_Ticks[0]=-200&knob-ticks(placements)_Ticks[1]=-100&knob-ticks(placements)_Ticks[2]=0&knob-ticks(placements)_Ticks[3]=5&knob-ticks(placements)_Ticks[4]=10&knob-ticks(placements)_Ticks[5]=15&knob-ticks(placements)_Ticks[6]=20&knob-ticks(placements)_Ticks[7]=25&knob-ticks(placements)_Ticks[8]=50&knob-ticks(placements)_Ticks[9]=100&knob-ticks(placements)_Ticks[10]=200`, + ); + }, + ); + }, + ); + }, + ); + + pwEach.describe([ + { subtype: 'vertical', width: '140px' }, + { subtype: 'horizontal', height: '85px' }, + { subtype: 'two-thirds-circle', height: '195px' }, + ])( + ({ subtype }) => `Bullet as Metric - ${subtype}`, + ({ subtype, height, width }) => { + test.describe('Bullet as Metric', () => { + pwEach.test(testCases)( + ([v]) => `should render with ${v}`, + async (page, [, { start, end, target, value }]) => { + await page.setViewportSize({ width: 785, height: 800 }); + await common.expectChartAtUrlToMatchScreenshot(page)( + `http://localhost:9001/?path=/story/bullet-graph--single&globals=toggles.showHeader:true;toggles.showChartTitle:false;toggles.showChartDescription:false;toggles.showChartBoundary:false;theme:light&knob-debug=&knob-title_General=Error rate&knob-subtitle_General=&knob-value_General=${value}&knob-target_General=${target}&knob-start_General=${start}&knob-end_General=${end}&knob-format (numeraljs)_General=0.[0]&knob-subtype_General=${subtype}&knob-niceDomain_Ticks=false&knob-tick strategy_Ticks=auto&knob-ticks(approx. count)_Ticks=10&knob-ticks(placements)_Ticks[0]=-200&knob-ticks(placements)_Ticks[1]=-100&knob-ticks(placements)_Ticks[2]=0&knob-ticks(placements)_Ticks[3]=5&knob-ticks(placements)_Ticks[4]=10&knob-ticks(placements)_Ticks[5]=15&knob-ticks(placements)_Ticks[6]=20&knob-ticks(placements)_Ticks[7]=25&knob-ticks(placements)_Ticks[8]=50&knob-ticks(placements)_Ticks[9]=100&knob-ticks(placements)_Ticks[10]=200`, + { + action: async () => await common.setResizeDimensions(page)({ height, width }), + }, + ); + }, + ); + }); + }, + ); +}); diff --git a/e2e_server/server/mocks/@storybook/addon-knobs/index.ts b/e2e_server/server/mocks/@storybook/addon-knobs/index.ts index 6e81757e06..4f52a18e53 100644 --- a/e2e_server/server/mocks/@storybook/addon-knobs/index.ts +++ b/e2e_server/server/mocks/@storybook/addon-knobs/index.ts @@ -59,7 +59,7 @@ export function array(name: string, dftValues: unknown[], options: any, groupId? return values; } -export function object(name: string, dftValue: unknown, options: any, groupId?: string) { +export function object(name: string, dftValue: unknown, groupId?: string) { const params = getParams(); const key = getKnobKey(name, groupId); const value = params.get(key); @@ -67,6 +67,14 @@ export function object(name: string, dftValue: unknown, options: any, groupId?: } export function optionsKnob(name: string, values: unknown, dftValues: unknown[], options: any, groupId?: string) { + const params = getParams(); + const knobName = getKnobKey(name, groupId); + + // Check for single values first + const paramValues = params.getAll(knobName); + + if (paramValues.length > 0) return paramValues; + return array(name, dftValues, options, groupId); } diff --git a/package.json b/package.json index f611a4eae2..77caff7fbe 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "@semantic-release/npm": "^9.0.1", "@semantic-release/release-notes-generator": "^10.0.3", "@storybook/react": "^6.3.7", - "@types/chroma-js": "^2.0.0", + "@types/chroma-js": "^2.4.2", "@types/classnames": "^2.2.7", "@types/color": "^3.0.1", "@types/core-js": "^2.5.2", diff --git a/packages/charts/api/charts.api.md b/packages/charts/api/charts.api.md index c1f4855648..698d3ea8bf 100644 --- a/packages/charts/api/charts.api.md +++ b/packages/charts/api/charts.api.md @@ -8,17 +8,20 @@ import { $Keys } from 'utility-types'; import { $Values } from 'utility-types'; +import { Assign } from 'utility-types'; import { ComponentProps } from 'react'; import { ComponentType } from 'react'; import { CSSProperties } from 'react'; import { FC } from 'react'; import { LegacyRef } from 'react'; +import { Optional } from 'utility-types'; import { OptionalKeys } from 'utility-types'; import { PropsWithChildren as PropsWithChildren_2 } from 'react'; import { default as React_2 } from 'react'; import { ReactChild } from 'react'; import { ReactElement } from 'react'; import { ReactNode } from 'react'; +import { Required as Required_2 } from 'utility-types'; import { RequiredKeys } from 'utility-types'; // @public (undocumented) @@ -177,7 +180,7 @@ export type AreaFitStyle = Visible & Opacity & { // Warning: (ae-forgotten-export) The symbol "SFProps" needs to be exported by the entry point index.d.ts // // @public -export const AreaSeries: (props: SFProps, "chartType" | "specType" | "seriesType", "groupId" | "xScaleType" | "yScaleType" | "hideInLegend" | "histogramModeAlignment", "name" | "color" | "fit" | "curve" | "timeZone" | "areaSeriesStyle" | "xNice" | "yNice" | "useDefaultGroupDomain" | "displayValueSettings" | "y0AccessorFormat" | "y1AccessorFormat" | "filterSeriesInTooltip" | "tickFormat" | "y0Accessors" | "splitSeriesAccessors" | "stackAccessors" | "markSizeAccessor" | "markFormat" | "stackMode" | "pointStyleAccessor", "id" | "data" | "xAccessor" | "yAccessors">) => null; +export const AreaSeries: (props: SFProps, "chartType" | "specType" | "seriesType", "groupId" | "hideInLegend" | "xScaleType" | "yScaleType" | "histogramModeAlignment", "name" | "color" | "fit" | "curve" | "timeZone" | "useDefaultGroupDomain" | "displayValueSettings" | "y0AccessorFormat" | "y1AccessorFormat" | "filterSeriesInTooltip" | "tickFormat" | "y0Accessors" | "splitSeriesAccessors" | "stackAccessors" | "markSizeAccessor" | "xNice" | "yNice" | "stackMode" | "areaSeriesStyle" | "markFormat" | "pointStyleAccessor", "id" | "data" | "xAccessor" | "yAccessors">) => null; // @public (undocumented) export type AreaSeriesProps = ComponentProps; @@ -233,7 +236,7 @@ export interface ArrayNode extends NodeDescriptor { } // @public -export const Axis: FC>; +export const Axis: FC>; // @public (undocumented) export type AxisId = string; @@ -329,7 +332,7 @@ export interface BandFillColorAccessorInput { } // @public -export const BarSeries: (props: SFProps, "chartType" | "specType" | "seriesType", "groupId" | "enableHistogramMode" | "xScaleType" | "yScaleType" | "hideInLegend", "name" | "color" | "timeZone" | "barSeriesStyle" | "xNice" | "yNice" | "useDefaultGroupDomain" | "displayValueSettings" | "y0AccessorFormat" | "y1AccessorFormat" | "filterSeriesInTooltip" | "tickFormat" | "y0Accessors" | "splitSeriesAccessors" | "stackAccessors" | "markSizeAccessor" | "stackMode" | "styleAccessor" | "minBarHeight", "id" | "data" | "xAccessor" | "yAccessors">) => null; +export const BarSeries: (props: SFProps, "chartType" | "specType" | "seriesType", "groupId" | "hideInLegend" | "xScaleType" | "yScaleType" | "enableHistogramMode", "name" | "color" | "timeZone" | "useDefaultGroupDomain" | "displayValueSettings" | "y0AccessorFormat" | "y1AccessorFormat" | "filterSeriesInTooltip" | "tickFormat" | "y0Accessors" | "splitSeriesAccessors" | "stackAccessors" | "markSizeAccessor" | "xNice" | "yNice" | "barSeriesStyle" | "stackMode" | "styleAccessor" | "minBarHeight", "id" | "data" | "xAccessor" | "yAccessors">) => null; // @public (undocumented) export type BarSeriesProps = ComponentProps; @@ -422,7 +425,7 @@ export type BrushEvent = XYBrushEvent | HeatmapBrushEvent; // Warning: (ae-incompatible-release-tags) The symbol "BubbleSeries" is marked as @public, but its signature references "BubbleSeriesSpec" which is marked as @alpha // // @public -export const BubbleSeries: (props: SFProps, "chartType" | "specType" | "seriesType", "groupId" | "xScaleType" | "yScaleType" | "hideInLegend", "name" | "color" | "timeZone" | "bubbleSeriesStyle" | "xNice" | "yNice" | "useDefaultGroupDomain" | "displayValueSettings" | "y0AccessorFormat" | "y1AccessorFormat" | "filterSeriesInTooltip" | "tickFormat" | "y0Accessors" | "splitSeriesAccessors" | "stackAccessors" | "markSizeAccessor" | "markFormat" | "pointStyleAccessor", "id" | "data" | "xAccessor" | "yAccessors">) => null; +export const BubbleSeries: (props: SFProps, "chartType" | "specType" | "seriesType", "groupId" | "hideInLegend" | "xScaleType" | "yScaleType", "name" | "color" | "timeZone" | "useDefaultGroupDomain" | "displayValueSettings" | "y0AccessorFormat" | "y1AccessorFormat" | "filterSeriesInTooltip" | "tickFormat" | "y0Accessors" | "splitSeriesAccessors" | "stackAccessors" | "markSizeAccessor" | "xNice" | "yNice" | "bubbleSeriesStyle" | "markFormat" | "pointStyleAccessor", "id" | "data" | "xAccessor" | "yAccessors">) => null; // @public (undocumented) export type BubbleSeriesProps = ComponentProps; @@ -440,6 +443,98 @@ export interface BubbleSeriesStyle { point: PointStyle; } +// @public +export type BulletColorConfig = Color[] | ColorBandSimpleConfig | ColorBandComplexConfig; + +// @public (undocumented) +export interface BulletDatum { + // (undocumented) + domain: GenericDomain; + // (undocumented) + niceDomain?: boolean; + // (undocumented) + subtitle?: string; + // (undocumented) + syncCursor?: boolean; + // (undocumented) + target?: number; + // (undocumented) + targetFormatter?: ValueFormatter; + // (undocumented) + tickFormatter: ValueFormatter; + ticks?: number | ((domain: GenericDomain) => number[]); + // (undocumented) + title: string; + // (undocumented) + value: number; + // (undocumented) + valueFormatter: ValueFormatter; +} + +// Warning: (ae-forgotten-export) The symbol "buildProps" needs to be exported by the entry point index.d.ts +// +// @alpha +export const BulletGraph: (props: SFProps) => null; + +// @alpha (undocumented) +export interface BulletGraphSpec extends Spec { + // (undocumented) + chartType: typeof ChartType.BulletGraph; + // (undocumented) + colorBands?: BulletColorConfig; + // (undocumented) + data: (BulletDatum | undefined)[][]; + // (undocumented) + specType: typeof SpecType.Series; + // (undocumented) + subtype: BulletGraphSubtype; + // (undocumented) + tickSnapStep?: number; + // (undocumented) + valueLabels?: Optional; +} + +// @public (undocumented) +export interface BulletGraphStyle { + // (undocumented) + angularTickLabelPadding: Pixels; + // (undocumented) + barBackground: Color; + // (undocumented) + border: Color; + colorBands: BulletColorConfig; + // (undocumented) + fallbackBandColor: Color; + // (undocumented) + minHeight: Pixels; + // (undocumented) + nonFiniteText: string; + // (undocumented) + textColor: Color; +} + +// @public (undocumented) +export const BulletGraphSubtype: Readonly<{ + vertical: "vertical"; + horizontal: "horizontal"; + circle: "circle"; + halfCircle: "half-circle"; + twoThirdsCircle: "two-thirds-circle"; +}>; + +// @public (undocumented) +export type BulletGraphSubtype = $Values; + +// @public (undocumented) +export interface BulletValueLabels { + // (undocumented) + active: string; + // (undocumented) + target: string; + // (undocumented) + value: string; +} + // @public (undocumented) export type CategoryKey = string; @@ -568,6 +663,7 @@ export const ChartType: Readonly<{ Heatmap: "heatmap"; Wordcloud: "wordcloud"; Metric: "metric"; + BulletGraph: "bullet_graph"; }>; // @public (undocumented) @@ -590,6 +686,31 @@ export type ColorBand = { label?: string; }; +// @public (undocumented) +export type ColorBandComplexConfig = ColorBandConfig[]; + +// Warning: (ae-forgotten-export) The symbol "OpenClosedBoundsConfig" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export type ColorBandConfig = OpenClosedBoundsConfig & { + color: Color; +}; + +// @public (undocumented) +export interface ColorBandSimpleConfig { + classes?: number | number[]; + // (undocumented) + colors: Color[]; +} + +// @public (undocumented) +export interface ColorBandValue { + // Warning: (ae-forgotten-export) The symbol "ColorBandValueType" needs to be exported by the entry point index.d.ts + type: ColorBandValueType; + // (undocumented) + value: number; +} + // @public (undocumented) export interface ColorConfig { // (undocumented) @@ -1152,6 +1273,9 @@ export const FONT_STYLES: readonly ["normal", "italic", "oblique", "inherit", "i // @public (undocumented) export type FontStyle = (typeof FONT_STYLES)[number]; +// @public (undocumented) +export type GenericDomain = [start: number, end: number]; + // @public export interface GeometryStateStyle { opacity: number; @@ -1184,10 +1308,10 @@ export type GetData = (dataDemand: DataDemand) => TimeslipDataRows; // @public (undocumented) export function getNodeName(node: ArrayNode): string; -// Warning: (ae-forgotten-export) The symbol "buildProps" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "buildProps_2" needs to be exported by the entry point index.d.ts // -// @alpha -export const Goal: (props: SFProps) => null; +// @alpha @deprecated +export const Goal: (props: SFProps) => null; // @alpha (undocumented) export interface GoalDomainRange { @@ -1338,7 +1462,7 @@ export type GroupId = string; export type GroupKeysOrKeyFn = Array | GroupByKeyFn; // @alpha -export const Heatmap: (props: SFProps, "chartType" | "specType", "data" | "timeZone" | "valueFormatter" | "valueAccessor" | "xAccessor" | "yAccessor" | "xScale" | "xSortPredicate" | "ySortPredicate" | "xAxisTitle" | "xAxisLabelName" | "xAxisLabelFormatter" | "yAxisTitle" | "yAxisLabelName" | "yAxisLabelFormatter", "name" | "highlightedData", "id" | "colorScale">) => null; +export const Heatmap: (props: SFProps, "chartType" | "specType", "data" | "timeZone" | "xAccessor" | "valueFormatter" | "valueAccessor" | "yAccessor" | "xSortPredicate" | "ySortPredicate" | "xScale" | "xAxisTitle" | "xAxisLabelName" | "xAxisLabelFormatter" | "yAxisTitle" | "yAxisLabelName" | "yAxisLabelFormatter", "name" | "highlightedData", "id" | "colorScale">) => null; // @alpha (undocumented) export interface HeatmapBandsColorScale { @@ -1372,7 +1496,7 @@ export interface HeatmapCellDatum extends SmallMultiplesDatum { } // @public (undocumented) -export type HeatmapElementEvent = [Cell, SeriesIdentifier]; +export type HeatmapElementEvent = [cell: Cell, seriesIdentifier: SeriesIdentifier]; // @public (undocumented) export interface HeatmapHighlightedData extends SmallMultiplesDatum { @@ -1512,7 +1636,7 @@ export interface HighlighterStyle { } // @public -export const HistogramBarSeries: (props: SFProps, "chartType" | "specType" | "seriesType", "groupId" | "enableHistogramMode" | "xScaleType" | "yScaleType" | "hideInLegend", "name" | "color" | "timeZone" | "barSeriesStyle" | "xNice" | "yNice" | "useDefaultGroupDomain" | "displayValueSettings" | "y0AccessorFormat" | "y1AccessorFormat" | "filterSeriesInTooltip" | "tickFormat" | "y0Accessors" | "splitSeriesAccessors" | "stackAccessors" | "markSizeAccessor" | "stackMode" | "styleAccessor" | "minBarHeight", "id" | "data" | "xAccessor" | "yAccessors">) => null; +export const HistogramBarSeries: (props: SFProps, "chartType" | "specType" | "seriesType", "groupId" | "hideInLegend" | "xScaleType" | "yScaleType" | "enableHistogramMode", "name" | "color" | "timeZone" | "useDefaultGroupDomain" | "displayValueSettings" | "y0AccessorFormat" | "y1AccessorFormat" | "filterSeriesInTooltip" | "tickFormat" | "y0Accessors" | "splitSeriesAccessors" | "stackAccessors" | "markSizeAccessor" | "xNice" | "yNice" | "barSeriesStyle" | "stackMode" | "styleAccessor" | "minBarHeight", "id" | "data" | "xAccessor" | "yAccessors">) => null; // @public (undocumented) export type HistogramBarSeriesProps = ComponentProps; @@ -1717,7 +1841,7 @@ export const LIGHT_BASE_COLORS: ChartBaseColors; export const LIGHT_THEME: Theme; // @public -export const LineAnnotation: (props: SFProps, "chartType" | "specType", "style" | "zIndex" | "groupId" | "hideLines" | "hideLinesTooltips" | "annotationType" | "hideTooltips", "marker" | "fallbackPlacements" | "placement" | "offset" | "boundary" | "boundaryPadding" | "markerBody" | "markerDimensions" | "markerPosition" | "customTooltip" | "customTooltipDetails" | "animations", "id" | "domainType" | "dataValues">) => null; +export const LineAnnotation: (props: SFProps, "chartType" | "specType", "style" | "zIndex" | "groupId" | "annotationType" | "hideTooltips" | "hideLines" | "hideLinesTooltips", "marker" | "fallbackPlacements" | "placement" | "offset" | "boundary" | "boundaryPadding" | "animations" | "markerBody" | "markerDimensions" | "markerPosition" | "customTooltip" | "customTooltipDetails", "id" | "dataValues" | "domainType">) => null; // @public export interface LineAnnotationDatum { @@ -1767,7 +1891,7 @@ export type LineFitStyle = Visible & Opacity & StrokeDashArray & { }; // @public -export const LineSeries: (props: SFProps, "chartType" | "specType" | "seriesType", "groupId" | "xScaleType" | "yScaleType" | "hideInLegend" | "histogramModeAlignment", "name" | "color" | "fit" | "curve" | "timeZone" | "lineSeriesStyle" | "xNice" | "yNice" | "useDefaultGroupDomain" | "displayValueSettings" | "y0AccessorFormat" | "y1AccessorFormat" | "filterSeriesInTooltip" | "tickFormat" | "y0Accessors" | "splitSeriesAccessors" | "stackAccessors" | "markSizeAccessor" | "markFormat" | "pointStyleAccessor", "id" | "data" | "xAccessor" | "yAccessors">) => null; +export const LineSeries: (props: SFProps, "chartType" | "specType" | "seriesType", "groupId" | "hideInLegend" | "xScaleType" | "yScaleType" | "histogramModeAlignment", "name" | "color" | "fit" | "curve" | "timeZone" | "useDefaultGroupDomain" | "displayValueSettings" | "y0AccessorFormat" | "y1AccessorFormat" | "filterSeriesInTooltip" | "tickFormat" | "y0Accessors" | "splitSeriesAccessors" | "stackAccessors" | "markSizeAccessor" | "xNice" | "yNice" | "lineSeriesStyle" | "markFormat" | "pointStyleAccessor", "id" | "data" | "xAccessor" | "yAccessors">) => null; // @public (undocumented) export type LineSeriesProps = ComponentProps; @@ -1897,7 +2021,9 @@ export type MetricTrendShape = $Values; // @alpha (undocumented) export type MetricWNumber = MetricBase & { value: number; - valueFormatter: (d: number) => string; + target?: number; + valueFormatter: ValueFormatter; + targetFormatter?: ValueFormatter; }; // @alpha (undocumented) @@ -2003,7 +2129,7 @@ export type PartialTheme = RecursivePartial; export const Partition: (props: SFProps, "chartType" | "specType", "animation" | "layout" | "layers" | "valueFormatter" | "valueGetter" | "fillOutside" | "radiusOutside" | "fillRectangleWidth" | "fillRectangleHeight" | "topGroove" | "percentFormatter" | "clockwiseSectors" | "maxRowCount" | "specialFirstInnermostSector" | "valueAccessor" | "smallMultiples" | "drilldown", never, "id" | "data">) => null; // @public (undocumented) -export type PartitionElementEvent = [Array, SeriesIdentifier]; +export type PartitionElementEvent = [layers: Array, seriesIdentifier: SeriesIdentifier]; // Warning: (ae-forgotten-export) The symbol "LabelConfig" needs to be exported by the entry point index.d.ts // @@ -2274,6 +2400,10 @@ export type PropsWithoutChildren = Record< children?: never | undefined; } & Neverify & Props & ExtraProps; +// @public (undocumented) +type Range_2 = [min: number, max: number]; +export { Range_2 as Range } + // @public (undocumented) export interface RasterTimeScale extends TimeScale { // (undocumented) @@ -2287,7 +2417,7 @@ export type Ratio = number; export type RawTextGetter = (node: ShapeTreeNode) => string; // @public (undocumented) -export const RectAnnotation: FC>; +export const RectAnnotation: FC>; // @public export interface RectAnnotationDatum { @@ -2792,6 +2922,7 @@ export interface Theme { background: BackgroundStyle; barSeriesStyle: BarSeriesStyle; bubbleSeriesStyle: BubbleSeriesStyle; + bulletGraph: BulletGraphStyle; chartMargins: Margins; chartPaddings: Margins; // (undocumented) @@ -2855,10 +2986,10 @@ export interface TimeScale { type: typeof ScaleType.Time; } -// Warning: (ae-forgotten-export) The symbol "buildProps_2" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "buildProps_3" needs to be exported by the entry point index.d.ts // // @public -export const Timeslip: (props: SFProps) => null; +export const Timeslip: (props: SFProps) => null; // @public export interface TimeslipSpec extends Spec { @@ -3223,7 +3354,7 @@ export type WillRenderListener = () => void; export const Wordcloud: FC>; // @public (undocumented) -export type WordCloudElementEvent = [WordModel, SeriesIdentifier]; +export type WordCloudElementEvent = [model: WordModel, seriesIdentifier: SeriesIdentifier]; // Warning: (ae-incompatible-release-tags) The symbol "WordcloudProps" is marked as @public, but its signature references "Wordcloud" which is marked as @alpha // @@ -3292,7 +3423,7 @@ export interface XYBrushEvent { } // @public (undocumented) -export type XYChartElementEvent = [GeometryValue, XYChartSeriesIdentifier]; +export type XYChartElementEvent = [geometry: GeometryValue, seriesIdentifier: XYChartSeriesIdentifier]; // @public (undocumented) export interface XYChartSeriesIdentifier extends SeriesIdentifier, SmallMultiplesDatum { diff --git a/packages/charts/package.json b/packages/charts/package.json index 8ed90f3d08..7b532c727d 100644 --- a/packages/charts/package.json +++ b/packages/charts/package.json @@ -34,7 +34,7 @@ "dependencies": { "@popperjs/core": "^2.11.8", "bezier-easing": "^2.1.0", - "chroma-js": "^2.1.0", + "chroma-js": "^2.4.2", "classnames": "^2.2.6", "d3-array": "^1.2.4", "d3-cloud": "^1.2.5", diff --git a/packages/charts/src/_reset.scss b/packages/charts/src/_reset.scss index b82f6aa906..928846cd25 100644 --- a/packages/charts/src/_reset.scss +++ b/packages/charts/src/_reset.scss @@ -9,3 +9,8 @@ svg text { letter-spacing: normal !important; } + +html, +body { + font-family: Inter, BlinkMacSystemFont, Helvetica, Arial, sans-serif !important; +} diff --git a/packages/charts/src/chart_types/bullet_graph/chart_state.tsx b/packages/charts/src/chart_types/bullet_graph/chart_state.tsx new file mode 100644 index 0000000000..98386abb66 --- /dev/null +++ b/packages/charts/src/chart_types/bullet_graph/chart_state.tsx @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { RefObject } from 'react'; + +import { BulletGraphRenderer } from './renderer/canvas'; +import { canDisplayChartTitles } from './selectors/can_display_chart_titles'; +import { getTooltipAnchor } from './selectors/get_tooltip_anchor'; +import { getTooltipInfo } from './selectors/get_tooltip_info'; +import { isTooltipVisible } from './selectors/is_tooltip_visible'; +import { ChartType } from '../../chart_types'; +import { DEFAULT_CSS_CURSOR } from '../../common/constants'; +import { LegendItem } from '../../common/legend'; +import { Tooltip } from '../../components/tooltip/tooltip'; +import { BackwardRef, GlobalChartState, InternalChartState } from '../../state/chart_state'; +import { InitStatus } from '../../state/selectors/get_internal_is_intialized'; +import { LegendItemLabel } from '../../state/selectors/get_legend_items_labels'; + +const EMPTY_MAP = new Map(); +const EMPTY_LEGEND_LIST: LegendItem[] = []; +const EMPTY_LEGEND_ITEM_LIST: LegendItemLabel[] = []; + +/** @internal */ +export class BulletGraphState implements InternalChartState { + chartType = ChartType.BulletGraph; + getChartTypeDescription = () => 'Bullet Graph'; + chartRenderer = (containerRef: BackwardRef, forwardStageRef: RefObject) => ( + <> + + + + ); + + isInitialized = () => InitStatus.Initialized; + isBrushAvailable = () => false; + isBrushing = () => false; + isChartEmpty = () => false; + getLegendItems = () => EMPTY_LEGEND_LIST; + getLegendItemsLabels = () => EMPTY_LEGEND_ITEM_LIST; + getLegendExtraValues = () => EMPTY_MAP; + getPointerCursor = () => DEFAULT_CSS_CURSOR; + isTooltipVisible(globalState: GlobalChartState) { + return isTooltipVisible(globalState); + } + + getTooltipInfo(globalState: GlobalChartState) { + return getTooltipInfo(globalState); + } + + getTooltipAnchor(globalState: GlobalChartState) { + return getTooltipAnchor(globalState); + } + + eventCallbacks = () => {}; + getProjectionContainerArea = () => ({ width: 0, height: 0, top: 0, left: 0 }); + getMainProjectionArea = () => ({ width: 0, height: 0, top: 0, left: 0 }); + getBrushArea = () => null; + getDebugState = () => ({}); + getSmallMultiplesDomains() { + return { + smHDomain: [], + smVDomain: [], + }; + } + + canDisplayChartTitles(globalState: GlobalChartState) { + return canDisplayChartTitles(globalState); + } +} diff --git a/packages/charts/src/chart_types/bullet_graph/renderer/canvas/bullet_graph.ts b/packages/charts/src/chart_types/bullet_graph/renderer/canvas/bullet_graph.ts new file mode 100644 index 0000000000..65de595d0d --- /dev/null +++ b/packages/charts/src/chart_types/bullet_graph/renderer/canvas/bullet_graph.ts @@ -0,0 +1,184 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { angularBullet, horizontalBullet, verticalBullet } from './sub_types'; +import { Color, Colors } from '../../../../common/colors'; +import { Ratio } from '../../../../common/geometry'; +import { cssFontShorthand } from '../../../../common/text_utils'; +import { withContext, clearCanvas } from '../../../../renderers/canvas'; +import { A11ySettings } from '../../../../state/selectors/get_accessibility_config'; +import { renderDebugPoint, renderDebugRect } from '../../../xy_chart/renderer/canvas/utils/debug'; +import { ActiveValue } from '../../selectors/get_active_values'; +import { BulletDimensions } from '../../selectors/get_panel_dimensions'; +import { BulletGraphSpec, BulletGraphSubtype } from '../../spec'; +import { + BulletGraphStyle, + HEADER_PADDING, + SUBTITLE_FONT, + SUBTITLE_FONT_SIZE, + SUBTITLE_LINE_HEIGHT, + TARGET_FONT, + TARGET_FONT_SIZE, + TITLE_FONT, + TITLE_FONT_SIZE, + TITLE_LINE_HEIGHT, + VALUE_FONT, + VALUE_FONT_SIZE, +} from '../../theme'; + +/** @internal */ +export function renderBulletGraph( + ctx: CanvasRenderingContext2D, + dpr: Ratio, + props: { + debug: boolean; + spec?: BulletGraphSpec; + a11y: A11ySettings; + dimensions: BulletDimensions; + activeValues: (ActiveValue | null)[][]; + style: BulletGraphStyle; + backgroundColor: Color; + }, +) { + const { debug, style, dimensions, activeValues, spec, backgroundColor } = props; + withContext(ctx, (ctx) => { + ctx.scale(dpr, dpr); + clearCanvas(ctx, backgroundColor); + + // clear only if need to render metric or no spec available + if (!spec || dimensions.shouldRenderMetric) { + return; + } + + // render each Small multiple + ctx.fillStyle = backgroundColor; + + // layout.headerLayout.forEach((row, rowIndex) => + dimensions.rows.forEach((row, rowIndex) => + row.forEach((bulletGraph, columnIndex) => { + if (!bulletGraph) return; + const { panel, multiline } = bulletGraph; + withContext(ctx, (ctx) => { + const verticalAlignment = dimensions.layoutAlignment[rowIndex]!; + const activeValue = activeValues?.[rowIndex]?.[columnIndex]; + + if (debug) { + renderDebugRect(ctx, panel); + } + + // move into the panel position + ctx.translate(panel.x, panel.y); + + // paint right border + ctx.strokeStyle = style.border; + // TODO: check paddings + if (row.length > 1 && columnIndex < row.length - 1) { + ctx.beginPath(); + ctx.moveTo(panel.width, 0); + ctx.lineTo(panel.width, panel.height); + ctx.stroke(); + } + + if (dimensions.rows.length > 1 && columnIndex < dimensions.rows.length) { + ctx.beginPath(); + ctx.moveTo(0, panel.height); + ctx.lineTo(panel.width, panel.height); + ctx.stroke(); + } + + // this helps render the header without considering paddings + ctx.translate(HEADER_PADDING.left, HEADER_PADDING.top); + + // Title + ctx.fillStyle = props.style.textColor; + ctx.textBaseline = 'top'; + ctx.textAlign = 'start'; + ctx.font = cssFontShorthand(TITLE_FONT, TITLE_FONT_SIZE); + bulletGraph.title.forEach((titleLine, lineIndex) => { + const y = lineIndex * TITLE_LINE_HEIGHT; + ctx.fillText(titleLine, 0, y); + }); + + // Subtitle + if (bulletGraph.subtitle) { + const y = verticalAlignment.maxTitleRows * TITLE_LINE_HEIGHT; + ctx.font = cssFontShorthand(SUBTITLE_FONT, SUBTITLE_FONT_SIZE); + ctx.fillText(bulletGraph.subtitle, 0, y); + } + + // Value + ctx.textBaseline = 'alphabetic'; + ctx.font = cssFontShorthand(VALUE_FONT, VALUE_FONT_SIZE); + if (!multiline) ctx.textAlign = 'end'; + { + const y = + verticalAlignment.maxTitleRows * TITLE_LINE_HEIGHT + + verticalAlignment.maxSubtitleRows * SUBTITLE_LINE_HEIGHT + + (multiline ? TARGET_FONT_SIZE : 0); + const x = multiline ? 0 : bulletGraph.header.width - bulletGraph.targetWidth; + ctx.fillText(bulletGraph.value, x, y); + } + + // Target + ctx.font = cssFontShorthand(TARGET_FONT, TARGET_FONT_SIZE); + if (!multiline) ctx.textAlign = 'end'; + { + const x = multiline ? bulletGraph.valueWidth : bulletGraph.header.width; + const y = + verticalAlignment.maxTitleRows * TITLE_LINE_HEIGHT + + verticalAlignment.maxSubtitleRows * SUBTITLE_LINE_HEIGHT + + (multiline ? TARGET_FONT_SIZE : 0); + ctx.fillText(bulletGraph.target, x, y); + } + + ctx.translate(-HEADER_PADDING.left, -HEADER_PADDING.top); + + const { graphArea } = bulletGraph; + + if (spec.subtype !== BulletGraphSubtype.horizontal) { + ctx.strokeStyle = style.border; + ctx.beginPath(); + ctx.moveTo(HEADER_PADDING.left, graphArea.origin.y); + ctx.lineTo(panel.width - HEADER_PADDING.right, graphArea.origin.y); + ctx.stroke(); + } + + withContext(ctx, (ctx) => { + ctx.translate(graphArea.origin.x, graphArea.origin.y); + + if (spec.subtype === BulletGraphSubtype.horizontal) { + horizontalBullet(ctx, bulletGraph, style, backgroundColor, activeValue); + } else if (spec.subtype === BulletGraphSubtype.vertical) { + verticalBullet(ctx, bulletGraph, style, backgroundColor, activeValue); + } else { + angularBullet(ctx, bulletGraph, style, backgroundColor, spec, debug, activeValue); + } + }); + + if (debug) { + withContext(ctx, (ctx) => { + ctx.translate(graphArea.origin.x, graphArea.origin.y); + renderDebugRect( + ctx, + { + ...graphArea.size, + x: 0, + y: 0, + }, + 0, + { color: Colors.Transparent.rgba }, + ); + renderDebugPoint(ctx, 0, 0); + renderDebugPoint(ctx, graphArea.size.width / 2, graphArea.size.height / 2); + }); + } + }); + }), + ); + }); +} diff --git a/packages/charts/src/chart_types/bullet_graph/renderer/canvas/constants.ts b/packages/charts/src/chart_types/bullet_graph/renderer/canvas/constants.ts new file mode 100644 index 0000000000..3b506566a8 --- /dev/null +++ b/packages/charts/src/chart_types/bullet_graph/renderer/canvas/constants.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/** @internal */ +export const TARGET_SIZE = 40; + +/** @internal */ +export const TARGET_STROKE_WIDTH = 3; + +/** @internal */ +export const BULLET_SIZE = 32; + +/** @internal */ +export const BAR_SIZE = 12; + +/** @internal */ +export const TICK_WIDTH = 1; + +/** @internal */ +export const MIN_TICK_COUNT = 3; + +/** @internal */ +export const MAX_TICK_COUNT = 8; + +/** @internal */ +export const HOVER_SLOP = 20; + +/** @internal */ +export const TICK_INTERVAL = 100; + +/** @internal */ +export const ANGULAR_TICK_INTERVAL = 120; diff --git a/packages/charts/src/chart_types/bullet_graph/renderer/canvas/index.tsx b/packages/charts/src/chart_types/bullet_graph/renderer/canvas/index.tsx new file mode 100644 index 0000000000..1b0c8fb087 --- /dev/null +++ b/packages/charts/src/chart_types/bullet_graph/renderer/canvas/index.tsx @@ -0,0 +1,286 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/* eslint-disable-next-line eslint-comments/disable-enable-pair */ +/* eslint-disable react/no-array-index-key */ + +import chroma from 'chroma-js'; +import React, { RefObject } from 'react'; +import { connect } from 'react-redux'; +import { bindActionCreators, Dispatch } from 'redux'; + +import { renderBulletGraph } from './bullet_graph'; +import { ColorContrastOptions } from '../../../../common/color_calcs'; +import { colorToRgba } from '../../../../common/color_library_wrappers'; +import { Color } from '../../../../common/colors'; +import { AlignedGrid } from '../../../../components/grid/aligned_grid'; +import { ElementOverListener, settingsBuildProps } from '../../../../specs'; +import { onChartRendered } from '../../../../state/actions/chart'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { + A11ySettings, + DEFAULT_A11Y_SETTINGS, + getA11ySettingsSelector, +} from '../../../../state/selectors/get_accessibility_config'; +import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; +import { getInternalIsInitializedSelector, InitStatus } from '../../../../state/selectors/get_internal_is_intialized'; +import { getResolvedBackgroundColorSelector } from '../../../../state/selectors/get_resolved_background_color'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_spec'; +import { Size } from '../../../../utils/dimensions'; +import { deepEqual } from '../../../../utils/fast_deep_equal'; +import { Point } from '../../../../utils/point'; +import { LIGHT_THEME } from '../../../../utils/themes/light_theme'; +import { Metric } from '../../../metric/renderer/dom/metric'; +import { BulletMetricWProgress, MetricDatum } from '../../../metric/specs'; +import { ActiveValue, getActiveValues } from '../../selectors/get_active_values'; +import { getBulletSpec } from '../../selectors/get_bullet_spec'; +import { getChartSize } from '../../selectors/get_chart_size'; +import { BulletDimensions, getPanelDimensions } from '../../selectors/get_panel_dimensions'; +import { hasChartTitles } from '../../selectors/has_chart_titles'; +import { BulletDatum, BulletGraphSpec, BulletGraphSubtype, mergeValueLabels } from '../../spec'; +import { BulletGraphStyle, LIGHT_THEME_BULLET_STYLE } from '../../theme'; +import { BulletColorConfig } from '../../utils/color'; + +interface StateProps { + initialized: boolean; + debug: boolean; + chartId: string; + hasTitles: boolean; + spec?: BulletGraphSpec; + a11y: A11ySettings; + size: Size; + dimensions: BulletDimensions; + activeValues: (ActiveValue | null)[][]; + style: BulletGraphStyle; + backgroundColor: Color; + locale: string; + pointerPosition?: Point; + colorBands: BulletColorConfig; + contrastOptions: ColorContrastOptions; + onElementOver?: ElementOverListener; +} + +interface DispatchProps { + onChartRendered: typeof onChartRendered; +} + +interface OwnProps { + forwardStageRef: RefObject; +} + +type Props = DispatchProps & StateProps & OwnProps; + +class Component extends React.Component { + static displayName = 'BulletGraph'; + private ctx: CanvasRenderingContext2D | null; + private readonly devicePixelRatio: number; + + constructor(props: Readonly) { + super(props); + this.ctx = null; + this.devicePixelRatio = window.devicePixelRatio; + } + + componentDidMount() { + this.tryCanvasContext(); + if (this.props.initialized) { + this.drawCanvas(); + this.props.onChartRendered(); + } + } + + shouldComponentUpdate(nextProps: Props) { + return !deepEqual(this.props, nextProps); + } + + componentDidUpdate() { + if (!this.ctx) { + this.tryCanvasContext(); + } + if (this.props.initialized) { + this.drawCanvas(); + this.props.onChartRendered(); + } + } + + private tryCanvasContext() { + const canvas = this.props.forwardStageRef.current; + this.ctx = canvas && canvas.getContext('2d'); + } + + private drawCanvas() { + if (this.ctx) { + renderBulletGraph(this.ctx, this.devicePixelRatio, this.props); + } + } + + render() { + /* eslint-disable prettier/prettier */ + // TODO - Prettier is going crazy on this line, need to investigate + const { + initialized, + size, + forwardStageRef, + a11y, + dimensions, + spec, + style, + backgroundColor, + locale, + contrastOptions, + } = this.props; + /* eslint-enable prettier/prettier */ + + if (!initialized || size.width === 0 || size.height === 0 || !spec) { + return null; + } + + return ( +
+ + {dimensions.shouldRenderMetric && ( +
+ + data={spec.data} + contentComponent={({ datum, stats }) => { + const colorScale = chroma + // TODO use colorBands in metric implementation + // @ts-ignore - TODO fix when not an array + .scale(Array.isArray(this.props.colorBands) ? this.props.colorBands : this.props.style.colorBands) + .domain(datum.domain); + const bulletDatum: BulletMetricWProgress = { + value: datum.value, + target: datum.target, + valueFormatter: datum.valueFormatter, + targetFormatter: datum.targetFormatter, + color: style.barBackground, + progressBarDirection: spec.subtype === BulletGraphSubtype.vertical ? 'vertical' : 'horizontal', + title: datum.title, + subtitle: datum.subtitle, + domain: datum.domain, + niceDomain: datum.niceDomain, + valueLabels: mergeValueLabels(spec.valueLabels), + extra: datum.target ? ( + + target: {(datum.targetFormatter ?? datum.valueFormatter)(datum.target)} + + ) : undefined, + }; + + return ( + + ); + }} + /> + ); +
+ )} +
+ ); + } +} + +const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => + bindActionCreators( + { + onChartRendered, + }, + dispatch, + ); + +const DEFAULT_PROPS: StateProps = { + initialized: false, + debug: false, + chartId: '', + spec: undefined, + hasTitles: false, + size: { + width: 0, + height: 0, + }, + a11y: DEFAULT_A11Y_SETTINGS, + dimensions: { + rows: [], + panel: { height: 0, width: 0 }, + layoutAlignment: [], + shouldRenderMetric: false, + }, + activeValues: [], + style: LIGHT_THEME_BULLET_STYLE, + backgroundColor: LIGHT_THEME.background.color, + locale: settingsBuildProps.defaults.locale, + colorBands: LIGHT_THEME.bulletGraph.colorBands, + contrastOptions: {}, +}; + +const mapStateToProps = (state: GlobalChartState): StateProps => { + if (getInternalIsInitializedSelector(state) !== InitStatus.Initialized) { + return DEFAULT_PROPS; + } + const { bulletGraph: style, metric: metricStyle } = getChartThemeSelector(state); + + const { debug, onElementOver, locale } = getSettingsSpecSelector(state); + + return { + initialized: true, + debug, + chartId: state.chartId, + hasTitles: hasChartTitles(state), + spec: getBulletSpec(state), + size: getChartSize(state), + a11y: getA11ySettingsSelector(state), + dimensions: getPanelDimensions(state), + activeValues: getActiveValues(state), + style, + locale, + backgroundColor: getResolvedBackgroundColorSelector(state), + colorBands: style.colorBands, + onElementOver, + contrastOptions: { + lightColor: colorToRgba(metricStyle.text.lightColor), + darkColor: colorToRgba(metricStyle.text.darkColor), + }, + }; +}; + +/** @internal */ +export const BulletGraphRenderer = connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/packages/charts/src/chart_types/bullet_graph/renderer/canvas/sub_types/angular.ts b/packages/charts/src/chart_types/bullet_graph/renderer/canvas/sub_types/angular.ts new file mode 100644 index 0000000000..7f974381ec --- /dev/null +++ b/packages/charts/src/chart_types/bullet_graph/renderer/canvas/sub_types/angular.ts @@ -0,0 +1,146 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Color } from '../../../../../common/colors'; +import { cssFontShorthand } from '../../../../../common/text_utils'; +import { measureText } from '../../../../../utils/bbox/canvas_text_bbox_calculator'; +import { clamp, isBetween, isFiniteNumber, sortNumbers } from '../../../../../utils/common'; +import { ContinuousDomain, GenericDomain } from '../../../../../utils/domain'; +import { drawPolarLine } from '../../../../xy_chart/renderer/canvas/lines'; +import { renderDebugPoint } from '../../../../xy_chart/renderer/canvas/utils/debug'; +import { ActiveValue } from '../../../selectors/get_active_values'; +import { BulletPanelDimensions } from '../../../selectors/get_panel_dimensions'; +import { BulletGraphSpec } from '../../../spec'; +import { BulletGraphStyle, GRAPH_PADDING, TICK_FONT, TICK_FONT_SIZE } from '../../../theme'; +import { getAngledChartSizing } from '../../../utils/angular'; +import { TARGET_SIZE, BULLET_SIZE, TICK_WIDTH, BAR_SIZE, TARGET_STROKE_WIDTH } from '../constants'; + +/** @internal */ +export function angularBullet( + ctx: CanvasRenderingContext2D, + dimensions: BulletPanelDimensions, + style: BulletGraphStyle, + backgroundColor: Color, + spec: BulletGraphSpec, + debug: boolean, + activeValue?: ActiveValue | null, +) { + const { datum, graphArea, scale, ticks, colorBands } = dimensions; + const { radius } = getAngledChartSizing(graphArea.size, spec.subtype); + const [startAngle, endAngle] = scale.range() as [number, number]; + const center = { + x: graphArea.center.x, + y: radius + TARGET_SIZE / 2, + }; + + ctx.translate(GRAPH_PADDING.left, GRAPH_PADDING.top); + + const [start, end] = scale.domain() as GenericDomain; + // const counterClockwise = true; + const counterClockwise = startAngle < endAngle && start > end; + const [min, max] = sortNumbers([start, end]) as ContinuousDomain; + const formatterColorTicks = ticks.map((v) => ({ value: v, formattedValue: datum.tickFormatter(v) })); + + // Color bands + colorBands.forEach((band) => { + ctx.beginPath(); + ctx.arc(center.x, center.y, radius, band.start, band.end, false); + ctx.lineWidth = BULLET_SIZE; + ctx.strokeStyle = band.color; + ctx.stroke(); + }); + + // Ticks + ctx.beginPath(); + ctx.strokeStyle = backgroundColor; + ctx.lineWidth = TICK_WIDTH; + formatterColorTicks + .filter((tick) => tick.value > min && tick.value < max) + .forEach((tick) => { + const bulletWidth = BULLET_SIZE + 4; // TODO fix arbitrary extension + drawPolarLine(ctx, scale(tick.value), radius, bulletWidth, center); + }); + + ctx.stroke(); + + // Bar + const confinedValue = clamp(datum.value, min, max); + const adjustedZero = clamp(0, min, max); + ctx.beginPath(); + ctx.lineWidth = BAR_SIZE; + ctx.strokeStyle = style.barBackground; + ctx.arc( + center.x, + center.y, + radius, + confinedValue > 0 ? scale(adjustedZero) : scale(confinedValue), + confinedValue > 0 ? scale(confinedValue) : scale(adjustedZero), + counterClockwise, + ); + ctx.stroke(); + + // Target + if (isFiniteNumber(datum.target) && datum.target <= max && datum.target >= min) { + ctx.beginPath(); + ctx.strokeStyle = style.barBackground; + ctx.lineWidth = TARGET_STROKE_WIDTH; + + drawPolarLine(ctx, scale(datum.target), radius, TARGET_SIZE, center); + + ctx.stroke(); + } + + // Zero baseline + if (isBetween(min, max, true)(0)) { + ctx.beginPath(); + ctx.strokeStyle = style.barBackground; + ctx.lineWidth = TICK_WIDTH; + + drawPolarLine(ctx, scale(0), radius, BULLET_SIZE, center); + + ctx.stroke(); + } + + const measure = measureText(ctx); + // Assumes mostly homogenous formatting + const maxTickWidth = formatterColorTicks.reduce((acc, t) => { + const { width } = measure(t.formattedValue, TICK_FONT, TICK_FONT_SIZE); + return Math.max(acc, width); + }, 0); + + // Tick labels + ctx.fillStyle = style.textColor; + ctx.textBaseline = 'middle'; + ctx.font = cssFontShorthand(TICK_FONT, TICK_FONT_SIZE); + formatterColorTicks + .filter((tick) => tick.value >= min && tick.value <= max) + .forEach((tick) => { + ctx.textAlign = 'center'; + const textPadding = style.angularTickLabelPadding + maxTickWidth / 2; + const start = scale(tick.value); + const y1 = Math.sin(start) * (radius - BULLET_SIZE / 2 - textPadding); + const x1 = Math.cos(start) * (radius - BULLET_SIZE / 2 - textPadding); + + ctx.fillText(tick.formattedValue, center.x + x1, center.y + y1); + }); + + if (activeValue) { + ctx.beginPath(); + ctx.strokeStyle = style.barBackground; + ctx.lineWidth = TARGET_STROKE_WIDTH; + drawPolarLine(ctx, activeValue.value, radius, TARGET_SIZE, center); + + ctx.stroke(); + } + + ctx.beginPath(); + + if (debug) { + renderDebugPoint(ctx, center.x, center.y); // arch center + } +} diff --git a/packages/charts/src/chart_types/bullet_graph/renderer/canvas/sub_types/horizontal.ts b/packages/charts/src/chart_types/bullet_graph/renderer/canvas/sub_types/horizontal.ts new file mode 100644 index 0000000000..1b13b7a06b --- /dev/null +++ b/packages/charts/src/chart_types/bullet_graph/renderer/canvas/sub_types/horizontal.ts @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Color } from '../../../../../common/colors'; +import { cssFontShorthand } from '../../../../../common/text_utils'; +import { measureText } from '../../../../../utils/bbox/canvas_text_bbox_calculator'; +import { clamp, isBetween, isFiniteNumber, sortNumbers } from '../../../../../utils/common'; +import { ContinuousDomain, GenericDomain } from '../../../../../utils/domain'; +import { ActiveValue } from '../../../selectors/get_active_values'; +import { BulletPanelDimensions } from '../../../selectors/get_panel_dimensions'; +import { BulletGraphStyle, GRAPH_PADDING, TICK_FONT, TICK_FONT_SIZE } from '../../../theme'; +import { TARGET_SIZE, BULLET_SIZE, TICK_WIDTH, BAR_SIZE, TARGET_STROKE_WIDTH } from '../constants'; + +/** @internal */ +export function horizontalBullet( + ctx: CanvasRenderingContext2D, + dimensions: BulletPanelDimensions, + style: BulletGraphStyle, + backgroundColor: Color, + activeValue?: ActiveValue | null, +) { + ctx.translate(GRAPH_PADDING.left, 0); + + const { datum, colorBands, ticks, scale } = dimensions; + const [start, end] = scale.domain() as GenericDomain; + const [min, max] = sortNumbers([start, end]) as ContinuousDomain; + + // Color bands + const verticalAlignment = TARGET_SIZE / 2; + colorBands.forEach((band) => { + ctx.fillStyle = band.color; + ctx.fillRect(band.start, verticalAlignment - BULLET_SIZE / 2, band.size, BULLET_SIZE); + }); + + // Ticks + ctx.beginPath(); + ctx.strokeStyle = backgroundColor; + ctx.lineWidth = TICK_WIDTH; + ticks + .filter((tick) => tick > min && tick < max) + .forEach((tick) => { + ctx.moveTo(scale(tick), verticalAlignment - BULLET_SIZE / 2); + ctx.lineTo(scale(tick), verticalAlignment + BULLET_SIZE / 2); + }); + ctx.stroke(); + + // Bar + const confinedValue = clamp(datum.value, min, max); + const adjustedZero = clamp(0, min, max); + ctx.fillStyle = style.barBackground; + ctx.fillRect( + datum.value > 0 ? scale(adjustedZero) : scale(confinedValue), + verticalAlignment - BAR_SIZE / 2, + confinedValue > 0 ? scale(confinedValue) - scale(adjustedZero) : scale(adjustedZero) - scale(confinedValue), + BAR_SIZE, + ); + + // Target + if (isFiniteNumber(datum.target) && datum.target <= max && datum.target >= min) { + ctx.fillRect( + scale(datum.target) - TARGET_STROKE_WIDTH / 2, + verticalAlignment - TARGET_SIZE / 2, + TARGET_STROKE_WIDTH, + TARGET_SIZE, + ); + } + + // Zero baseline + if (isBetween(min, max, true)(0)) { + ctx.fillRect(scale(0) - TICK_WIDTH / 2, verticalAlignment - BULLET_SIZE / 2, TICK_WIDTH, BULLET_SIZE); + } + + // Active Value + if (activeValue && (datum.syncCursor || !activeValue.external)) { + ctx.fillRect( + activeValue.value - TARGET_STROKE_WIDTH / 2, + verticalAlignment - TARGET_SIZE / 2, + TARGET_STROKE_WIDTH, + TARGET_SIZE, + ); + } + + // Tick labels + ctx.fillStyle = style.textColor; + ctx.textBaseline = 'top'; + ctx.font = cssFontShorthand(TICK_FONT, TICK_FONT_SIZE); + ticks + .filter((tick) => tick >= min && tick <= max) + .forEach((tick, i) => { + const labelText = datum.tickFormatter(tick); + if (i === ticks.length - 1) { + const availableWidth = Math.abs((start > end ? min : max) - (ticks.at(i) ?? NaN)); + const { width: labelWidth } = measureText(ctx)(labelText, TICK_FONT, TICK_FONT_SIZE); + ctx.textAlign = labelWidth >= Math.abs(scale(availableWidth) - scale(0)) ? 'end' : 'start'; + } else { + ctx.textAlign = 'start'; + } + ctx.fillText(labelText, scale(tick), verticalAlignment + TARGET_SIZE / 2); + }); +} diff --git a/packages/charts/src/chart_types/bullet_graph/renderer/canvas/sub_types/index.ts b/packages/charts/src/chart_types/bullet_graph/renderer/canvas/sub_types/index.ts new file mode 100644 index 0000000000..370820b99d --- /dev/null +++ b/packages/charts/src/chart_types/bullet_graph/renderer/canvas/sub_types/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './horizontal'; +export * from './vertical'; +export * from './angular'; diff --git a/packages/charts/src/chart_types/bullet_graph/renderer/canvas/sub_types/vertical.ts b/packages/charts/src/chart_types/bullet_graph/renderer/canvas/sub_types/vertical.ts new file mode 100644 index 0000000000..959c53cd75 --- /dev/null +++ b/packages/charts/src/chart_types/bullet_graph/renderer/canvas/sub_types/vertical.ts @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Color } from '../../../../../common/colors'; +import { cssFontShorthand } from '../../../../../common/text_utils'; +import { clamp, isBetween, isFiniteNumber, sortNumbers } from '../../../../../utils/common'; +import { ContinuousDomain, GenericDomain } from '../../../../../utils/domain'; +import { ActiveValue } from '../../../selectors/get_active_values'; +import { BulletPanelDimensions } from '../../../selectors/get_panel_dimensions'; +import { BulletGraphStyle, GRAPH_PADDING, TICK_FONT, TICK_FONT_SIZE } from '../../../theme'; +import { TARGET_SIZE, BULLET_SIZE, TICK_WIDTH, BAR_SIZE, TARGET_STROKE_WIDTH } from '../constants'; + +/** @internal */ +export function verticalBullet( + ctx: CanvasRenderingContext2D, + dimensions: BulletPanelDimensions, + style: BulletGraphStyle, + backgroundColor: Color, + activeValue?: ActiveValue | null, +) { + ctx.translate(0, GRAPH_PADDING.top); + + const { datum, graphArea, scale, colorBands, ticks } = dimensions; + const [start, end] = scale.domain() as GenericDomain; + const [min, max] = sortNumbers([start, end]) as ContinuousDomain; + const graphPaddedHeight = graphArea.size.height - GRAPH_PADDING.bottom - GRAPH_PADDING.top; + + // color bands + colorBands.reverse().forEach((band) => { + ctx.fillStyle = band.color; + ctx.fillRect( + graphArea.size.width / 2 - BULLET_SIZE / 2, + graphPaddedHeight - band.start - band.size, + BULLET_SIZE, + band.size, + ); + }); + + // Ticks + ctx.beginPath(); + ctx.strokeStyle = backgroundColor; + ctx.lineWidth = TICK_WIDTH; + + ticks + .filter((tick) => tick > min && tick < max) + .forEach((tick) => { + ctx.moveTo(graphArea.size.width / 2 - BULLET_SIZE / 2, graphPaddedHeight - scale(tick)); + ctx.lineTo(graphArea.size.width / 2 + BULLET_SIZE / 2, graphPaddedHeight - scale(tick)); + }); + ctx.stroke(); + + // Bar + const confinedValue = clamp(datum.value, min, max); + const adjustedZero = clamp(0, min, max); + ctx.fillStyle = style.barBackground; + ctx.fillRect( + graphArea.size.width / 2 - BAR_SIZE / 2, + confinedValue > 0 ? graphPaddedHeight - scale(confinedValue) : graphPaddedHeight - scale(adjustedZero), + BAR_SIZE, + confinedValue > 0 ? scale(confinedValue) - scale(adjustedZero) : scale(adjustedZero) - scale(confinedValue), + ); + + // Target + if (isFiniteNumber(datum.target) && datum.target <= max && datum.target >= min) { + ctx.fillRect( + graphArea.size.width / 2 - TARGET_SIZE / 2, + graphPaddedHeight - scale(datum.target) - TARGET_STROKE_WIDTH / 2, + TARGET_SIZE, + TARGET_STROKE_WIDTH, + ); + } + + // Zero baseline + if (isBetween(min, max, true)(0)) { + ctx.fillRect( + graphArea.size.width / 2 - BULLET_SIZE / 2, + graphPaddedHeight - scale(0) - TICK_WIDTH / 2, + BULLET_SIZE, + TICK_WIDTH, + ); + } + + // Active Value + if (activeValue && (datum.syncCursor || !activeValue.external)) { + ctx.fillRect( + graphArea.size.width / 2 - TARGET_SIZE / 2, + graphPaddedHeight - activeValue.value - TARGET_STROKE_WIDTH / 2, + TARGET_SIZE, + TARGET_STROKE_WIDTH, + ); + } + + // Tick labels + ctx.textBaseline = 'top'; + ctx.fillStyle = style.textColor; + ctx.font = cssFontShorthand(TICK_FONT, TICK_FONT_SIZE); + ticks + .filter((tick) => tick >= min && tick <= max) + .forEach((tick, i) => { + ctx.textAlign = 'end'; + + const labelText = datum.tickFormatter(tick); + if (i === ticks.length - 1) { + const availableHeight = Math.abs((start > end ? min : max) - (ticks.at(i) ?? NaN)); + const labelHeight = TICK_FONT_SIZE; + ctx.textBaseline = labelHeight >= Math.abs(scale(availableHeight) - scale(0)) ? 'hanging' : 'bottom'; + } else { + ctx.textBaseline = 'bottom'; + } + + ctx.fillText(labelText, graphArea.size.width / 2 - TARGET_SIZE / 2 - 6, graphPaddedHeight - scale(tick)); + }); +} diff --git a/packages/charts/src/chart_types/bullet_graph/selectors/can_display_chart_titles.ts b/packages/charts/src/chart_types/bullet_graph/selectors/can_display_chart_titles.ts new file mode 100644 index 0000000000..10cd055f91 --- /dev/null +++ b/packages/charts/src/chart_types/bullet_graph/selectors/can_display_chart_titles.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { getBulletSpec } from './get_bullet_spec'; +import { createCustomCachedSelector } from '../../../state/create_selector'; + +/** @internal */ +export const canDisplayChartTitles = createCustomCachedSelector([getBulletSpec], (spec): boolean => { + return (spec?.data?.length ?? 0) > 1 || (spec?.data?.[0]?.length ?? 0) > 1; +}); diff --git a/packages/charts/src/chart_types/bullet_graph/selectors/get_active_value.ts b/packages/charts/src/chart_types/bullet_graph/selectors/get_active_value.ts new file mode 100644 index 0000000000..ce643a6027 --- /dev/null +++ b/packages/charts/src/chart_types/bullet_graph/selectors/get_active_value.ts @@ -0,0 +1,166 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { getBulletSpec } from './get_bullet_spec'; +import { BulletPanelDimensions, getPanelDimensions } from './get_panel_dimensions'; +import { TAU } from '../../../common/constants'; +import { Radian } from '../../../common/geometry'; +import { createCustomCachedSelector } from '../../../state/create_selector'; +import { getActivePointerPosition } from '../../../state/selectors/get_active_pointer_position'; +import { isBetween, isFiniteNumber, roundTo, sortNumbers } from '../../../utils/common'; +import { ContinuousDomain, Range } from '../../../utils/domain'; +import { Point } from '../../../utils/point'; +import { BULLET_SIZE, HOVER_SLOP, TARGET_SIZE } from '../renderer/canvas/constants'; +import { BulletGraphSpec, BulletGraphSubtype } from '../spec'; +import { GRAPH_PADDING } from '../theme'; +import { getAngledChartSizing } from '../utils/angular'; + +/** @internal */ +export interface ActiveValueDetails { + value: number; + snapValue: number; + color: string; + pixelValue: number; + rowIndex: number; + columnIndex: number; + panel: BulletPanelDimensions; +} + +/** @internal */ +export const getActiveValue = createCustomCachedSelector( + [getActivePointerPosition, getPanelDimensions, getBulletSpec], + (pointer, dimensions, spec): ActiveValueDetails | null => { + if (!pointer) return null; + const { x, y } = pointer; + + const rowIndex = Math.ceil(y / dimensions.panel.height) - 1; + const columnIndex = Math.ceil(x / dimensions.panel.width) - 1; + const activePanel = dimensions.rows?.[rowIndex]?.[columnIndex]; + + if (!activePanel) return null; + + const relativePointer = { + x: x - activePanel.panel.x, + y: y - activePanel.panel.y, + }; + + const valueDetails = getPanelValue(activePanel, relativePointer, spec); + + if (!valueDetails || !isFiniteNumber(valueDetails.value)) return null; + + return { + ...valueDetails, + rowIndex, + columnIndex, + panel: activePanel, + }; + }, +); + +function getPanelValue( + panel: BulletPanelDimensions, + pointer: Point, + spec: BulletGraphSpec, +): Pick | undefined { + const { graphArea, scale } = panel; + const [min, max] = sortNumbers(scale.domain()) as ContinuousDomain; + const isWithinDomain = isBetween(min, max); + + switch (spec.subtype) { + case BulletGraphSubtype.circle: + case BulletGraphSubtype.halfCircle: + case BulletGraphSubtype.twoThirdsCircle: { + const { radius } = getAngledChartSizing(graphArea.size, spec.subtype); + const center = { + x: graphArea.center.x, + y: radius + TARGET_SIZE / 2, + }; + const { x, y } = pointer; + const normalizedPointer = { + x: x - center.x - graphArea.origin.x - GRAPH_PADDING.left, + y: y - center.y - graphArea.origin.y - GRAPH_PADDING.top, + }; + + const distance = Math.sqrt(Math.pow(normalizedPointer.x, 2) + Math.pow(normalizedPointer.y, 2)); + const outerLimit = radius + BULLET_SIZE / 2 + HOVER_SLOP; + const innerLimit = radius - BULLET_SIZE / 2 - HOVER_SLOP; + + if (distance <= outerLimit && distance >= innerLimit) { + // TODO find why to determine angle between origin and point + // The angle goes from -Ï€ in Quadrant 2 to +Ï€ in Quadrant 3 + // This angle offset is a temporary fix + const angleOffset = normalizedPointer.x < 0 && normalizedPointer.y > 0 ? -TAU : 0; + const angle: Radian = Math.atan2(normalizedPointer.y, normalizedPointer.x) + angleOffset; + const value = scale.invert(angle); + const snapValue = spec.tickSnapStep ? roundTo(value, spec.tickSnapStep) : value; + + if (isWithinDomain(snapValue)) { + return { + value, + snapValue, + color: panel.colorScale(snapValue).hex(), + pixelValue: angle, + }; + } + } + break; + } + + case BulletGraphSubtype.horizontal: { + const yCenterOffset = Math.abs(pointer.y - graphArea.origin.y - TARGET_SIZE / 2); + + if (yCenterOffset > TARGET_SIZE / 2 + HOVER_SLOP) return; + + const relativeX = pointer.x - GRAPH_PADDING.left; + const [min, max] = scale.range() as Range; + + if (relativeX < min || relativeX > max) break; + + const value = panel.scale.invert(relativeX); + const snapValue = spec.tickSnapStep ? roundTo(value, spec.tickSnapStep) : value; + + if (isWithinDomain(snapValue)) { + return { + value, + snapValue, + color: panel.colorScale(snapValue).hex(), + pixelValue: relativeX, + }; + } + + break; + } + + case BulletGraphSubtype.vertical: { + const xCenterOffset = Math.abs(pointer.x - graphArea.center.x - GRAPH_PADDING.left); + + if (xCenterOffset > TARGET_SIZE / 2 + HOVER_SLOP) return; + + const relativeY = panel.panel.height - pointer.y - GRAPH_PADDING.bottom; + const [min, max] = scale.range() as Range; + + if (relativeY < min || relativeY > max) break; + + const value = panel.scale.invert(relativeY); + const snapValue = spec.tickSnapStep ? roundTo(value, spec.tickSnapStep) : value; + + if (isWithinDomain(snapValue)) { + return { + value, + snapValue, + color: panel.colorScale(snapValue).hex(), + pixelValue: relativeY, + }; + } + break; + } + + default: + return; + } +} diff --git a/packages/charts/src/chart_types/bullet_graph/selectors/get_active_values.ts b/packages/charts/src/chart_types/bullet_graph/selectors/get_active_values.ts new file mode 100644 index 0000000000..780dd12c48 --- /dev/null +++ b/packages/charts/src/chart_types/bullet_graph/selectors/get_active_values.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { getActiveValue } from './get_active_value'; +import { getPanelDimensions } from './get_panel_dimensions'; +import { createCustomCachedSelector } from '../../../state/create_selector'; +import { sortNumbers } from '../../../utils/common'; +import { ContinuousDomain } from '../../../utils/domain'; + +/** @internal */ +export interface ActiveValue { + value: number; + external: boolean; +} + +/** @internal */ +export const getActiveValues = createCustomCachedSelector( + [getActiveValue, getPanelDimensions], + (activeValue, dimensions): (ActiveValue | null)[][] => { + if (!activeValue) return []; + + // Synced cursor values should always use the snapValue to avoid strange diffs + const { snapValue, rowIndex, columnIndex } = activeValue; + + return dimensions.rows.map((row, ri) => + row.map((panel, ci): ActiveValue | null => { + const external = !(rowIndex === ri && columnIndex === ci); + if (!panel || (!panel.datum.syncCursor && external)) return null; + const [min, max] = sortNumbers(panel.scale.domain()) as ContinuousDomain; + if (snapValue > max || snapValue < min) return null; + + return { + value: panel.scale(snapValue), + external, + }; + }), + ); + }, +); diff --git a/packages/charts/src/chart_types/bullet_graph/selectors/get_bullet_spec.ts b/packages/charts/src/chart_types/bullet_graph/selectors/get_bullet_spec.ts new file mode 100644 index 0000000000..7fe31ceac6 --- /dev/null +++ b/packages/charts/src/chart_types/bullet_graph/selectors/get_bullet_spec.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ChartType } from '../../../chart_types'; +import { BulletGraphSpec } from '../../../chart_types/bullet_graph/spec'; +import { SpecType } from '../../../specs'; +import { GlobalChartState } from '../../../state/chart_state'; +import { getSpecFromStore } from '../../../state/utils'; + +/** @internal */ + +export function getBulletSpec(state: GlobalChartState): BulletGraphSpec { + return getSpecFromStore(state.specs, ChartType.BulletGraph, SpecType.Series, true); +} diff --git a/packages/charts/src/chart_types/bullet_graph/selectors/get_chart_size.ts b/packages/charts/src/chart_types/bullet_graph/selectors/get_chart_size.ts new file mode 100644 index 0000000000..eeb727a1be --- /dev/null +++ b/packages/charts/src/chart_types/bullet_graph/selectors/get_chart_size.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { GlobalChartState } from '../../../state/chart_state'; +import { createCustomCachedSelector } from '../../../state/create_selector'; +import { Dimensions } from '../../../utils/dimensions'; + +const getParentDimension = (state: GlobalChartState) => state.parentDimensions; + +/** @internal */ +export const getChartSize = createCustomCachedSelector([getParentDimension], (container): Dimensions => { + return { ...container }; +}); diff --git a/packages/charts/src/chart_types/bullet_graph/selectors/get_layout.ts b/packages/charts/src/chart_types/bullet_graph/selectors/get_layout.ts new file mode 100644 index 0000000000..fbfb2bf543 --- /dev/null +++ b/packages/charts/src/chart_types/bullet_graph/selectors/get_layout.ts @@ -0,0 +1,225 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { getBulletSpec } from './get_bullet_spec'; +import { getChartSize } from './get_chart_size'; +import { BulletDatum, BulletGraphSubtype } from '../../../chart_types/bullet_graph/spec'; +import { createCustomCachedSelector } from '../../../state/create_selector'; +import { getSettingsSpecSelector } from '../../../state/selectors/get_settings_spec'; +import { withTextMeasure } from '../../../utils/bbox/canvas_text_bbox_calculator'; +import { Size } from '../../../utils/dimensions'; +import { wrapText } from '../../../utils/text/wrap'; +import { + HEADER_PADDING, + SUBTITLE_FONT, + SUBTITLE_FONT_SIZE, + SUBTITLE_LINE_HEIGHT, + TARGET_FONT, + TARGET_FONT_SIZE, + TITLE_FONT, + TITLE_FONT_SIZE, + TITLE_LINE_HEIGHT, + VALUE_FONT, + VALUE_FONT_SIZE, + VALUE_LINE_HEIGHT, +} from '../theme'; + +/** @internal */ +export interface BulletHeaderLayout { + panel: Size; + header: Size; + title: string[]; + subtitle: string | undefined; + value: string; + target: string; + multiline: boolean; + valueWidth: number; + targetWidth: number; + sizes: { title: number; subtitle: number; value: number; target: number }; + datum: BulletDatum; +} + +/** @internal */ +export interface BulletLayoutAlignment { + maxTitleRows: number; + maxSubtitleRows: number; + multiline: boolean; + minHeight: number; + minWidth: number; +} + +/** @internal */ +export interface BulletGraphLayout { + /** Common panel size */ + panel: Size; + headerLayout: (BulletHeaderLayout | null)[][]; + layoutAlignment: BulletLayoutAlignment[]; + shouldRenderMetric: boolean; +} + +const minChartHeights: Record = { + [BulletGraphSubtype.horizontal]: 50, + [BulletGraphSubtype.vertical]: 100, + [BulletGraphSubtype.circle]: 160, + [BulletGraphSubtype.halfCircle]: 160, + [BulletGraphSubtype.twoThirdsCircle]: 160, +}; + +const minChartWidths: Record = { + [BulletGraphSubtype.horizontal]: 140, + [BulletGraphSubtype.vertical]: 140, + [BulletGraphSubtype.circle]: 160, + [BulletGraphSubtype.halfCircle]: 160, + [BulletGraphSubtype.twoThirdsCircle]: 160, +}; + +/** @internal */ +export const getLayout = createCustomCachedSelector( + [getBulletSpec, getChartSize, getSettingsSpecSelector], + (spec, chartSize, { locale }): BulletGraphLayout => { + const { data } = spec; + const rows = data.length; + const columns = data.reduce((acc, row) => { + return Math.max(acc, row.length); + }, 0); + + const panel: Size = { width: chartSize.width / columns, height: chartSize.height / rows }; + const headerSize: Size = { + width: panel.width - HEADER_PADDING.left - HEADER_PADDING.right, + height: panel.height - HEADER_PADDING.top - HEADER_PADDING.bottom, + }; + + return withTextMeasure((textMeasurer) => { + // collect header elements title, subtitles and values + const header = data.map((row) => + row.map((cell) => { + if (!cell) return null; + + const content = { + title: cell.title.trim(), + subtitle: cell.subtitle?.trim(), + value: `${cell.valueFormatter(cell.value)}${cell.target ? ' ' : ''}`, + target: cell.target ? `/ ${(cell.targetFormatter ?? cell.valueFormatter)(cell.target)}` : '', + datum: cell, + }; + const size = { + title: textMeasurer(content.title.trim(), TITLE_FONT, TITLE_FONT_SIZE).width, + subtitle: content.subtitle ? textMeasurer(content.subtitle, TITLE_FONT, TITLE_FONT_SIZE).width : 0, + value: textMeasurer(content.value, VALUE_FONT, VALUE_FONT_SIZE).width, + target: textMeasurer(content.target, TARGET_FONT, TARGET_FONT_SIZE).width, + }; + return { content, size }; + }), + ); + + const goesToMultiline = header.some((row) => { + const valueAlignedWithSubtitle = row.some((cell) => cell?.content.subtitle); + return row.some((cell) => { + if (!cell) return false; + const valuesWidth = cell.size.value + cell.size.target; + return valueAlignedWithSubtitle + ? cell.size.subtitle + valuesWidth > headerSize.width || cell.size.title > headerSize.width + : cell.size.title + valuesWidth > headerSize.width; + }); + }); + + const headerLayout = header.map((row) => { + return row.map((cell) => { + if (!cell) return null; + + if (goesToMultiline) { + return { + panel, + header: headerSize, + // wrap only title if necessary + title: wrapText( + cell.content.title, + TITLE_FONT, + TITLE_FONT_SIZE, + headerSize.width, + 2, + textMeasurer, + locale, + ), + subtitle: cell.content.subtitle + ? wrapText( + cell.content.subtitle, + SUBTITLE_FONT, + SUBTITLE_FONT_SIZE, + headerSize.width, + 1, + textMeasurer, + locale, + )[0] + : undefined, + value: cell.content.value, + target: cell.content.target, + multiline: true, + valueWidth: cell.size.value, + targetWidth: cell.size.target, + sizes: cell.size, + datum: cell.content.datum, + }; + } + + return { + panel, + header: headerSize, + title: [cell.content.title], + subtitle: cell.content.subtitle ? cell.content.subtitle : undefined, + value: cell.content.value, + target: cell.content.target, + multiline: false, + valueWidth: cell.size.value, + targetWidth: cell.size.target, + sizes: cell.size, + datum: cell.content.datum, + }; + }); + }); + const layoutAlignment = headerLayout.map((curr) => { + return curr.reduce( + (rowStats, cell) => { + const maxTitleRows = Math.max(rowStats.maxTitleRows, cell?.title.length ?? 0); + const maxSubtitleRows = Math.max(rowStats.maxSubtitleRows, cell?.subtitle ? 1 : 0); + return { + maxTitleRows, + maxSubtitleRows, + multiline: cell?.multiline ?? false, + minHeight: + maxTitleRows * TITLE_LINE_HEIGHT + + maxSubtitleRows * SUBTITLE_LINE_HEIGHT + + (cell?.multiline ? VALUE_LINE_HEIGHT : 0) + + HEADER_PADDING.top + + HEADER_PADDING.bottom + + minChartHeights[spec.subtype], + minWidth: minChartWidths[spec.subtype], + }; + }, + { maxTitleRows: 0, maxSubtitleRows: 0, multiline: false, minHeight: 0, minWidth: 0 }, + ); + }); + + const totalHeight = layoutAlignment.reduce((acc, curr) => { + return acc + curr.minHeight; + }, 0); + + const totalWidth = layoutAlignment.reduce((acc, curr) => { + return Math.max(acc, curr.minWidth); + }, 0); + const shouldRenderMetric = chartSize.height <= totalHeight || chartSize.width <= totalWidth * columns; + + return { + panel, + headerLayout, + layoutAlignment, + shouldRenderMetric, + }; + }); + }, +); diff --git a/packages/charts/src/chart_types/bullet_graph/selectors/get_panel_dimensions.ts b/packages/charts/src/chart_types/bullet_graph/selectors/get_panel_dimensions.ts new file mode 100644 index 0000000000..30b8c80a8c --- /dev/null +++ b/packages/charts/src/chart_types/bullet_graph/selectors/get_panel_dimensions.ts @@ -0,0 +1,230 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ScaleLinear, scaleLinear } from 'd3-scale'; + +import { getBulletSpec } from './get_bullet_spec'; +import { BulletGraphLayout, BulletHeaderLayout, getLayout } from './get_layout'; +import { ChromaColorScale, Color } from '../../../common/colors'; +import { Rect } from '../../../geoms/types'; +import { createCustomCachedSelector } from '../../../state/create_selector'; +import { getChartThemeSelector } from '../../../state/selectors/get_chart_theme'; +import { getResolvedBackgroundColorSelector } from '../../../state/selectors/get_resolved_background_color'; +import { isWithinRange } from '../../../utils/common'; +import { Size } from '../../../utils/dimensions'; +import { GenericDomain, Range } from '../../../utils/domain'; +import { Point } from '../../../utils/point'; +import { ANGULAR_TICK_INTERVAL, TICK_INTERVAL } from '../renderer/canvas/constants'; +import { BulletDatum, BulletGraphSpec, BulletGraphSubtype } from '../spec'; +import { + BulletGraphStyle, + GRAPH_PADDING, + HEADER_PADDING, + SUBTITLE_LINE_HEIGHT, + TITLE_LINE_HEIGHT, + VALUE_LINE_HEIGHT, +} from '../theme'; +import { getAngledChartSizing, getAnglesBySize } from '../utils/angular'; +import { ColorTick, getColorBands } from '../utils/color'; +import { TickOptions, getTicks } from '../utils/ticks'; + +/** @internal */ +export type BulletPanelDimensions = { + graphArea: { + size: Size; + origin: Point; + center: Point; + }; + scale: ScaleLinear; + ticks: number[]; + colorScale: ChromaColorScale; + colorBands: ColorTick[]; + panel: Rect; +} & Omit; + +/** @internal */ +export type BulletDimensions = { + rows: (BulletPanelDimensions | null)[][]; + panel: Size; +} & Pick; + +/** @internal */ +export const getPanelDimensions = createCustomCachedSelector( + [getLayout, getBulletSpec, getChartThemeSelector, getResolvedBackgroundColorSelector], + ( + { shouldRenderMetric, headerLayout, layoutAlignment, panel: panelSize }, + spec, + { bulletGraph: bulletGraphStyles }, + backgroundColor, + ): BulletDimensions => { + if (shouldRenderMetric) + return { + rows: [], + panel: { width: 0, height: 0 }, + layoutAlignment, + shouldRenderMetric, + }; + + const rows = headerLayout.map((row, rowIndex) => { + return row.map((bulletGraph, columnIndex): BulletPanelDimensions | null => { + if (!bulletGraph) return null; + const { panel, multiline, datum, ...rest } = bulletGraph; + const verticalAlignment = layoutAlignment[rowIndex]!; + + const graphSize = { + width: panel.width, + height: + panel.height - + HEADER_PADDING.top - + verticalAlignment.maxTitleRows * TITLE_LINE_HEIGHT - + verticalAlignment.maxSubtitleRows * SUBTITLE_LINE_HEIGHT - + (multiline ? VALUE_LINE_HEIGHT : 0) - + HEADER_PADDING.bottom, + }; + + return { + ...rest, + ...getSubtypeDimensions(spec, graphSize, datum, bulletGraphStyles, backgroundColor), + datum, + multiline, + graphArea: { + size: graphSize, + origin: { + x: 0, + y: panel.height - graphSize.height, + }, + center: { + x: graphSize.width / 2 - GRAPH_PADDING.left, + y: graphSize.height / 2 - GRAPH_PADDING.top, + }, + }, + panel: { + x: panel.width * columnIndex, + y: panel.height * rowIndex, + ...panel, + }, + }; + }); + }); + + return { + rows, + panel: panelSize, + layoutAlignment, + shouldRenderMetric, + }; + }, +); + +function getSubtypeDimensions( + { subtype, colorBands: colorBandsConfig }: BulletGraphSpec, + graphSize: Size, + { ticks: desiredTicks, domain, niceDomain }: BulletDatum, + { colorBands: defaultColorBandsConfig, fallbackBandColor }: BulletGraphStyle, + backgroundColor: Color, +): Pick { + switch (subtype) { + case BulletGraphSubtype.circle: + case BulletGraphSubtype.halfCircle: + case BulletGraphSubtype.twoThirdsCircle: { + const [startAngle, endAngle] = getAnglesBySize(subtype); + const { radius } = getAngledChartSizing(graphSize, subtype); + + const { scale, ticks } = getScaleWithTicks(domain, [startAngle, endAngle], { + rangeMultiplier: radius, + desiredTicks, + nice: niceDomain, + interval: ANGULAR_TICK_INTERVAL, + }); + + const { bands: colorBands, scale: colorScale } = getColorBands( + scale, + colorBandsConfig ?? defaultColorBandsConfig, + ticks, + backgroundColor, + fallbackBandColor, + ); + + return { + scale, + ticks, + colorBands, + colorScale, + }; + } + + case BulletGraphSubtype.horizontal: { + const paddedWidth = graphSize.width - GRAPH_PADDING.left - GRAPH_PADDING.right; + const { scale, ticks } = getScaleWithTicks(domain, [0, paddedWidth], { + desiredTicks, + nice: niceDomain, + interval: TICK_INTERVAL, + }); + + const { bands: colorBands, scale: colorScale } = getColorBands( + scale, + colorBandsConfig ?? defaultColorBandsConfig, + ticks, + backgroundColor, + fallbackBandColor, + ); + + return { + scale, + ticks, + colorBands, + colorScale, + }; + } + + case BulletGraphSubtype.vertical: { + const paddedHeight = graphSize.height - GRAPH_PADDING.bottom - GRAPH_PADDING.top; + const { scale, ticks } = getScaleWithTicks(domain, [0, paddedHeight], { + desiredTicks, + nice: niceDomain, + interval: TICK_INTERVAL, + }); + + const { bands: colorBands, scale: colorScale } = getColorBands( + scale, + colorBandsConfig ?? defaultColorBandsConfig, + ticks, + backgroundColor, + fallbackBandColor, + ); + + return { + scale, + ticks, + colorBands, + colorScale, + }; + } + + default: + throw new Error('Unknown Bullet subtype'); + } +} + +function getScaleWithTicks(domain: GenericDomain, range: Range, tickOptions: TickOptions) { + let scale = scaleLinear().domain(domain).range(range); + const scaleRange: Range = scale.range() as Range; + const ticks = getTicks(Math.abs(scaleRange[1] - scaleRange[0]) * (tickOptions.rangeMultiplier || 1), tickOptions); + const customRange = typeof ticks !== 'number'; + + if (tickOptions.nice) { + scale = scale.nice(customRange ? undefined : ticks); + } + + const updatedDomain = scale.domain() as GenericDomain; + + return { + scale, + ticks: customRange ? ticks(updatedDomain).filter(isWithinRange(updatedDomain)) : scale.ticks(ticks), + }; +} diff --git a/packages/charts/src/chart_types/bullet_graph/selectors/get_tooltip_anchor.ts b/packages/charts/src/chart_types/bullet_graph/selectors/get_tooltip_anchor.ts new file mode 100644 index 0000000000..871ac336a0 --- /dev/null +++ b/packages/charts/src/chart_types/bullet_graph/selectors/get_tooltip_anchor.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { AnchorPosition } from '../../../components/portal'; +import { createCustomCachedSelector } from '../../../state/create_selector'; +import { getActivePointerPosition } from '../../../state/selectors/get_active_pointer_position'; + +/** @internal */ +export const getTooltipAnchor = createCustomCachedSelector( + [getActivePointerPosition], + (pointer): AnchorPosition | null => { + return { + x: pointer?.x ?? 0, + y: pointer?.y ?? 0, + width: 0, + height: 0, + }; + }, +); diff --git a/packages/charts/src/chart_types/bullet_graph/selectors/get_tooltip_info.ts b/packages/charts/src/chart_types/bullet_graph/selectors/get_tooltip_info.ts new file mode 100644 index 0000000000..8d9fd0961f --- /dev/null +++ b/packages/charts/src/chart_types/bullet_graph/selectors/get_tooltip_info.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { getActiveValue } from './get_active_value'; +import { getBulletSpec } from './get_bullet_spec'; +import { TooltipInfo } from '../../../components/tooltip'; +import { createCustomCachedSelector } from '../../../state/create_selector'; +import { isBetween } from '../../../utils/common'; +import { mergeValueLabels } from '../spec'; + +/** @internal */ +export const getTooltipInfo = createCustomCachedSelector( + [getActiveValue, getBulletSpec], + (activeValue, spec): TooltipInfo | undefined => { + if (!activeValue) return; + + const useHighlighter = false; + const highlightMargin = 2; + const valueLabels = mergeValueLabels(spec.valueLabels); + + const activeDatum = activeValue.panel.datum; + const tooltipInfo: TooltipInfo = { + header: null, + values: [], + }; + + tooltipInfo.values.push({ + label: valueLabels.active, + value: activeValue.value, + color: activeValue.color, + isHighlighted: false, + seriesIdentifier: { + specId: 'bullet', + key: 'active', + }, + isVisible: true, + formattedValue: activeDatum.valueFormatter(activeValue.snapValue), + }); + + const isHighlighted = useHighlighter + ? isBetween(activeValue.pixelValue - highlightMargin, activeValue.pixelValue + highlightMargin) + : () => false; + + tooltipInfo.values.push({ + label: valueLabels.value, + value: activeDatum.value, + color: activeValue.panel.colorScale(activeDatum.value).hex(), + isHighlighted: isHighlighted(activeValue.panel.scale(activeDatum.value)), + seriesIdentifier: { + specId: 'bullet', + key: 'value', + }, + isVisible: true, + formattedValue: activeDatum.valueFormatter(activeDatum.value), + }); + + if (activeDatum.target) { + tooltipInfo.values.push({ + label: valueLabels.target, + value: activeDatum.target, + color: activeValue.panel.colorScale(activeDatum.target).hex(), + isHighlighted: isHighlighted(activeValue.panel.scale(activeDatum.target)), + seriesIdentifier: { + // TODO make this better + specId: 'bullet', + key: 'target', + }, + isVisible: true, + formattedValue: activeDatum.valueFormatter(activeDatum.target), + }); + } + + return tooltipInfo; + }, +); diff --git a/packages/charts/src/chart_types/bullet_graph/selectors/has_chart_titles.ts b/packages/charts/src/chart_types/bullet_graph/selectors/has_chart_titles.ts new file mode 100644 index 0000000000..d622f5f7d5 --- /dev/null +++ b/packages/charts/src/chart_types/bullet_graph/selectors/has_chart_titles.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { canDisplayChartTitles } from './can_display_chart_titles'; +import { GlobalChartState } from '../../../state/chart_state'; +import { createCustomCachedSelector } from '../../../state/create_selector'; + +const getChartTitleOrDescription = ({ title, description }: GlobalChartState) => Boolean(title || description); + +/** @internal */ +export const hasChartTitles = createCustomCachedSelector( + [canDisplayChartTitles, getChartTitleOrDescription], + (displayTitles, hasTitles): boolean => { + return displayTitles && hasTitles; + }, +); diff --git a/packages/charts/src/chart_types/bullet_graph/selectors/is_tooltip_visible.ts b/packages/charts/src/chart_types/bullet_graph/selectors/is_tooltip_visible.ts new file mode 100644 index 0000000000..2e27f4ecb8 --- /dev/null +++ b/packages/charts/src/chart_types/bullet_graph/selectors/is_tooltip_visible.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { getTooltipInfo } from './get_tooltip_info'; +import { TooltipType } from '../../../specs'; +import { TooltipVisibility } from '../../../state/chart_state'; +import { createCustomCachedSelector } from '../../../state/create_selector'; +import { getTooltipSpecSelector } from '../../../state/selectors/get_tooltip_spec'; + +/** @internal */ +export const isTooltipVisible = createCustomCachedSelector( + [getTooltipSpecSelector, getTooltipInfo], + ({ type }, tooltipInfo): TooltipVisibility => { + return { + visible: type !== TooltipType.None && (tooltipInfo?.values.length ?? 0) > 0, + isExternal: false, + displayOnly: false, + isPinnable: false, + }; + }, +); diff --git a/packages/charts/src/chart_types/bullet_graph/spec.ts b/packages/charts/src/chart_types/bullet_graph/spec.ts new file mode 100644 index 0000000000..39ace18003 --- /dev/null +++ b/packages/charts/src/chart_types/bullet_graph/spec.ts @@ -0,0 +1,126 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ComponentProps } from 'react'; +import { $Values, Optional } from 'utility-types'; + +import { BulletColorConfig } from './utils/color'; +import { ChartType } from '../../chart_types/index'; +import { Spec } from '../../specs'; +import { SpecType } from '../../specs/constants'; +import { buildSFProps, SFProps, useSpecFactory } from '../../state/spec_factory'; +import { mergePartial, stripUndefined, ValueFormatter } from '../../utils/common'; +import { GenericDomain } from '../../utils/domain'; + +/** @public */ +export interface BulletDatum { + title: string; + subtitle?: string; + value: number; + target?: number; + domain: GenericDomain; + niceDomain?: boolean; + /** + * Approximate number of ticks to be returned. Must be greater than 0. + * + * Or + * + * Function that returns the exact ticks to use, this if you pass bad ticks we will not be able to help you! + * Sort order must match the direction of the domain. + * + * Defaults to auto ticks based on length + * + * See https://d3js.org/d3-scale/linear#linear_ticks + */ + ticks?: number | ((domain: GenericDomain) => number[]); + syncCursor?: boolean; + valueFormatter: ValueFormatter; + targetFormatter?: ValueFormatter; + tickFormatter: ValueFormatter; +} + +/** @public */ +export const BulletGraphSubtype = Object.freeze({ + vertical: 'vertical' as const, + horizontal: 'horizontal' as const, + /** + * This bullet subtype is not yet fully supported + * See https://github.com/elastic/elastic-charts/issues/2200 + * @alpha + */ + circle: 'circle' as const, + halfCircle: 'half-circle' as const, + twoThirdsCircle: 'two-thirds-circle' as const, +}); +/** @public */ +export type BulletGraphSubtype = $Values; + +/** @public */ +export interface BulletValueLabels { + active: string; + value: string; + target: string; +} + +/** @alpha */ +export interface BulletGraphSpec extends Spec { + specType: typeof SpecType.Series; + chartType: typeof ChartType.BulletGraph; + data: (BulletDatum | undefined)[][]; + subtype: BulletGraphSubtype; + tickSnapStep?: number; + colorBands?: BulletColorConfig; + valueLabels?: Optional; +} + +/** @internal */ +export const mergeValueLabels = (labels?: BulletGraphSpec['valueLabels']) => + mergePartial( + { + active: 'Active', + value: 'Value', + target: 'Target', + }, + labels, + ); + +const buildProps = buildSFProps()( + { + specType: SpecType.Series, + chartType: ChartType.BulletGraph, + }, + {}, +); + +/** + * Add Goal spec to chart + * @alpha + */ +export const BulletGraph = function ( + props: SFProps< + BulletGraphSpec, + keyof (typeof buildProps)['overrides'], + keyof (typeof buildProps)['defaults'], + keyof (typeof buildProps)['optionals'], + keyof (typeof buildProps)['requires'] + >, +) { + const { defaults, overrides } = buildProps; + const constraints = {}; + + useSpecFactory({ + ...defaults, + ...stripUndefined(props), + ...overrides, + ...constraints, + }); + return null; +}; + +/** @public */ +export type BulletGraphProps = ComponentProps; diff --git a/packages/charts/src/chart_types/bullet_graph/theme.ts b/packages/charts/src/chart_types/bullet_graph/theme.ts new file mode 100644 index 0000000000..82a2598b06 --- /dev/null +++ b/packages/charts/src/chart_types/bullet_graph/theme.ts @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { BulletColorConfig } from './utils/color'; +import { Color } from '../../common/colors'; +import { DEFAULT_FONT_FAMILY } from '../../common/default_theme_attributes'; +import { Pixels } from '../../common/geometry'; +import { Font } from '../../common/text_utils'; +import { Padding } from '../../utils/dimensions'; +import { DARK_BASE_COLORS, LIGHT_BASE_COLORS } from '../../utils/themes/base_colors'; + +/** @public */ +export interface BulletGraphStyle { + textColor: Color; + border: Color; + barBackground: Color; + /** + * Default band colors when not defined on spec + */ + colorBands: BulletColorConfig; + nonFiniteText: string; + minHeight: Pixels; + angularTickLabelPadding: Pixels; + fallbackBandColor: Color; +} + +/** @internal */ +export const LIGHT_THEME_BULLET_STYLE: BulletGraphStyle = { + textColor: LIGHT_BASE_COLORS.darkestShade, + border: '#EDF0F5', + barBackground: LIGHT_BASE_COLORS.darkestShade, + colorBands: ['#D9C6EF', '#AA87D1'], + nonFiniteText: 'N/A', + minHeight: 64, + angularTickLabelPadding: 10, + fallbackBandColor: LIGHT_BASE_COLORS.mediumShade, +}; + +/** @internal */ +export const DARK_THEME_BULLET_STYLE: BulletGraphStyle = { + textColor: '#E0E5EE', + border: DARK_BASE_COLORS.lightShade, + barBackground: '#FFF', + colorBands: ['#6092C0', '#3F4E61'], + nonFiniteText: 'N/A', + minHeight: 64, + angularTickLabelPadding: 10, + fallbackBandColor: DARK_BASE_COLORS.mediumShade, +}; + +/** @internal */ +export const TITLE_FONT: Font = { + fontStyle: 'normal', + fontFamily: DEFAULT_FONT_FAMILY, + fontVariant: 'normal', + fontWeight: 'bold', + textColor: 'black', +}; +/** @internal */ +export const TITLE_FONT_SIZE = 16; +/** @internal */ +export const TITLE_LINE_HEIGHT = 19; + +/** @internal */ +export const SUBTITLE_FONT: Font = { + ...TITLE_FONT, + fontWeight: 'normal', +}; +/** @internal */ +export const SUBTITLE_FONT_SIZE = 14; +/** @internal */ +export const SUBTITLE_LINE_HEIGHT = 16; + +/** @internal */ +export const VALUE_FONT: Font = { + ...TITLE_FONT, +}; +/** @internal */ +export const VALUE_FONT_SIZE = 22; +/** @internal */ +export const VALUE_LINE_HEIGHT = 22; + +/** @internal */ +export const TARGET_FONT: Font = { + ...SUBTITLE_FONT, +}; +/** @internal */ +export const TARGET_FONT_SIZE = 16; +/** @internal */ +export const TARGET_LINE_HEIGHT = 16; + +/** @internal */ +export const TICK_FONT: Font = { + ...TITLE_FONT, + fontWeight: 'normal', +}; +/** @internal */ +export const TICK_FONT_SIZE = 10; + +/** @internal */ +export const HEADER_PADDING: Padding = { + top: 8, + bottom: 8, + left: 8, + right: 8, +}; +/** @internal */ +export const GRAPH_PADDING: Padding = { + top: 8, + bottom: 8, + left: 8, + right: 8, +}; diff --git a/packages/charts/src/chart_types/bullet_graph/utils/angular.ts b/packages/charts/src/chart_types/bullet_graph/utils/angular.ts new file mode 100644 index 0000000000..56d9bb35f7 --- /dev/null +++ b/packages/charts/src/chart_types/bullet_graph/utils/angular.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { TAU } from '../../../common/constants'; +import { clamp } from '../../../utils/common'; +import { Size } from '../../../utils/dimensions'; +import { TARGET_SIZE } from '../renderer/canvas/constants'; +import { BulletGraphSubtype } from '../spec'; +import { GRAPH_PADDING } from '../theme'; + +type AngularBulletSubtypes = Extract; + +const sizeAngles: Record = { + [BulletGraphSubtype.halfCircle]: { + startAngle: 1 * Math.PI, + endAngle: 0, + }, + [BulletGraphSubtype.twoThirdsCircle]: { + startAngle: 1.25 * Math.PI, + endAngle: -0.25 * Math.PI, + }, + [BulletGraphSubtype.circle]: { + startAngle: 1.5 * Math.PI, + endAngle: -0.5 * Math.PI, + }, +}; + +/** @internal */ +export function getAnglesBySize(subtype: BulletGraphSubtype): [startAngle: number, endAngle: number] { + if (subtype === BulletGraphSubtype.vertical || subtype === BulletGraphSubtype.horizontal) { + throw new Error('Attempting to retrieve angle size from horizontal/vertical bullet'); + } + const angles = sizeAngles[subtype] ?? sizeAngles[BulletGraphSubtype.twoThirdsCircle]!; + // Negative angles used to match current radian pattern + const startAngle = -angles.startAngle; + // limit endAngle to startAngle +/- 2Ï€ + const endAngle = clamp(-angles.endAngle, startAngle - TAU, startAngle + TAU); + return [startAngle, endAngle]; +} + +const heightModifiers: Record = { + [BulletGraphSubtype.halfCircle]: 0.5, + [BulletGraphSubtype.twoThirdsCircle]: 0.86, // approximated to account for flare of arc stroke at the bottom + [BulletGraphSubtype.circle]: 1, +}; + +/** @internal */ +export function getAngledChartSizing( + graphSize: Size, + subtype: BulletGraphSubtype, +): { maxWidth: number; maxHeight: number; radius: number } { + if (subtype === BulletGraphSubtype.vertical || subtype === BulletGraphSubtype.horizontal) { + throw new Error('Attempting to retrieve angle size from horizontal/vertical bullet'); + } + const heightModifier = heightModifiers[subtype] ?? 1; + const maxWidth = graphSize.width - GRAPH_PADDING.left - GRAPH_PADDING.right; + const maxHeight = graphSize.height - GRAPH_PADDING.top - GRAPH_PADDING.bottom; + const modifiedHeight = maxHeight / heightModifier; + const radius = Math.min(maxWidth, modifiedHeight) / 2 - TARGET_SIZE / 2; + + return { maxWidth, maxHeight: modifiedHeight, radius }; +} diff --git a/packages/charts/src/chart_types/bullet_graph/utils/bounds.ts b/packages/charts/src/chart_types/bullet_graph/utils/bounds.ts new file mode 100644 index 0000000000..45e46dc2ce --- /dev/null +++ b/packages/charts/src/chart_types/bullet_graph/utils/bounds.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Required, Assign } from 'utility-types'; + +/** + * Base configurable bounds based on greater than, less than (equal to) + * + * `T` may be set to any value to be used as needed + * @public + */ +export type BaseBoundsConfig = { + /** + * Less than - The max value of the range, exclusive + */ + lt?: T; + /** + * Less than or equal - The max value of the range, inclusive + */ + lte?: T; + /** + * Greater than - The min value of the range, exclusive + */ + gt?: T; + /** + * Greater than or equal - The min value of the range, inclusive + */ + gte?: T; +}; + +/** @public */ +export type BoundsLimiter> = Assign< + BaseBoundsConfig, + Required, U>> +>; + +/** + * Allowed combination to define closed-ended/two-sided bounds + * @public + */ +export type ClosedBoundsConfig = + | BoundsLimiter + | BoundsLimiter + | BoundsLimiter + | BoundsLimiter; + +/** + * Allowed combination to define open-ended/one-sided bounds + * @public + */ +export type OpenBoundsConfig = + | BoundsLimiter + | BoundsLimiter + | BoundsLimiter + | BoundsLimiter; + +/** + * Open and closed bound configurations + * @public + */ +export type OpenClosedBoundsConfig = OpenBoundsConfig | ClosedBoundsConfig; diff --git a/packages/charts/src/chart_types/bullet_graph/utils/color.ts b/packages/charts/src/chart_types/bullet_graph/utils/color.ts new file mode 100644 index 0000000000..73d1496235 --- /dev/null +++ b/packages/charts/src/chart_types/bullet_graph/utils/color.ts @@ -0,0 +1,322 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import chroma from 'chroma-js'; +import { ScaleLinear } from 'd3-scale'; +import { $Values } from 'utility-types'; + +import { BaseBoundsConfig, OpenClosedBoundsConfig } from './bounds'; +import { combineColors } from '../../../common/color_calcs'; +import { RGBATupleToString, colorToRgba, getChromaColor } from '../../../common/color_library_wrappers'; +import { ChromaColorScale, Color } from '../../../common/colors'; +import { isFiniteNumber, isNil, isWithinRange, sortNumbers } from '../../../utils/common'; +import { ContinuousDomain, GenericDomain } from '../../../utils/domain'; + +/** + * @public + */ +export const ColorBandValueType = Object.freeze({ + /** + * Value in the scaled space + */ + Scale: 'scale' as const, + /** + * Percentage of the scaled space as a ratio from `0` to `1` + */ + Percentage: 'percentage' as const, +}); +/** @public */ +export type ColorBandValueType = $Values; + +/** @public */ +export interface ColorBandValue { + /** + * Type of value + * + * @defaultValue `scale` + */ + type: ColorBandValueType; + value: number; +} + +/** @public */ +export type ColorBandConfig = OpenClosedBoundsConfig & { + /** + * Color to be applied to band + */ + color: Color; +}; + +/** @public */ +export interface ColorBandSimpleConfig { + /** + * Distinct color classes to defined discrete color breakdown + * Defaults to intervals between ticks + * + * Number value scales colors evenly n times + * Array of numbers defines the color stop positions + * + * See https://gka.github.io/chroma.js/#scale-classes + */ + classes?: number | number[]; + colors: Color[]; +} + +/** @public */ +export type ColorBandComplexConfig = ColorBandConfig[]; + +/** + * Defines the color of bullet chart bands + * @public + */ +export type BulletColorConfig = Color[] | ColorBandSimpleConfig | ColorBandComplexConfig; + +const getValueByTypeFn = ([min, max]: ContinuousDomain) => { + const domainLength = max - min; + const minOffset = domainLength / 100000; // TODO validate approach + return (bandValue: ColorBandValue | number, openOffset: -1 | 0 | 1 = 0): number | null => { + const openOffsetValue = openOffset * minOffset; + if (typeof bandValue === 'number') return bandValue + openOffsetValue; + const { type, value } = bandValue; + if (type === 'scale') return value + openOffsetValue; + if (type === 'percentage') return min + value * domainLength + openOffsetValue; + return null; + }; +}; + +const getBandValueFn = (domain: ContinuousDomain) => { + const getValueByType = getValueByTypeFn(domain); + return (bandValue?: number | ColorBandValue, openOffset?: -1 | 0 | 1): number | null => { + if (isNil(bandValue)) return null; + return getValueByType(bandValue, openOffset); + }; +}; + +const getDomainPairFn = (domain: ContinuousDomain) => { + const getBandValue = getBandValueFn(domain); + return (config: BaseBoundsConfig): [start: number | null, end: number | null] => { + return [ + getBandValue(config.gt, 1) ?? getBandValue(config.gte), + getBandValue(config.lt, -1) ?? getBandValue(config.lte), + ]; + }; +}; + +const isComplexConfig = (config: BulletColorConfig): config is ColorBandComplexConfig => + Array.isArray(config) && typeof config[0] !== 'string'; + +const getFullDomainTicks = ([min, max]: ContinuousDomain, ticks: number[]): number[] => { + const fullTicks = ticks.slice(); + const first = fullTicks.at(0)!; + const last = fullTicks.at(-1)!; + const minIndex = first > last ? -1 : 0; + const maxIndex = first < last ? -1 : 0; + if (fullTicks.at(minIndex) !== min) { + if (minIndex === 0) fullTicks.unshift(min); + else fullTicks.push(min); + } + if (fullTicks.at(maxIndex) !== max) { + if (maxIndex === 0) fullTicks.unshift(max); + else fullTicks.push(max); + } + return fullTicks; +}; + +function getScaleInputs( + baseDomain: ContinuousDomain, + flippedDomain: boolean, + config: BulletColorConfig, + fullTicks: number[], + backgroundColor: Color, +): { + domain: number[]; + colors: string[]; + classes?: number | number[]; +} { + if (!Array.isArray(config) || !isComplexConfig(config)) { + const { colors: rawColors, classes }: { colors: string[]; classes?: number | number[] } = !Array.isArray(config) + ? config + : { + colors: config, + }; + + // TODO - fix thrown error for RGBA colors from storybook + const colors = rawColors.map((c) => c.toLowerCase()); + if (colors.length === 1) { + // Adds second color + const [color] = colors; + // should always have color + if (color) { + const secondary = getChromaColor(color).alpha(0.7).hex(); + const blendedSecondary = combineColors(colorToRgba(secondary), colorToRgba(backgroundColor)); + colors.push(RGBATupleToString(blendedSecondary)); + } + } + + if (flippedDomain) { + // Array of colors should always begin at the domain start + colors.reverse(); + } + + return { + domain: baseDomain, + colors, + classes: classes ?? fullTicks, + }; + } + + if (!isComplexConfig(config)) { + return { + domain: baseDomain, + colors: config, + }; + } + + const getDomainPair = getDomainPairFn(baseDomain); + const { colors, boundedDomains } = config.reduce<{ + boundedDomains: [number | null, number | null][]; + colors: string[]; + }>( + (acc, colorConfig) => { + if (typeof colorConfig === 'string') { + acc.colors.push(colorConfig); + } else { + acc.colors.push(colorConfig.color); + const domainPair = getDomainPair(colorConfig); + acc.boundedDomains.push(domainPair); + } + + return acc; + }, + { + boundedDomains: [], + colors: [], + }, + ); + + let prevMax = -Infinity; + return boundedDomains.reduce<{ + domain: number[]; + colors: string[]; + }>( + (acc, [min, max], i) => { + const testMinValue = isFiniteNumber(min) ? min : isFiniteNumber(max) ? max : null; + if (testMinValue === null || testMinValue < prevMax) return acc; + const newMaxValue = isFiniteNumber(max) ? max : isFiniteNumber(min) ? min : null; + if (newMaxValue === null) { + // TODO remove this error + throw new Error('newMaxValue is null?????'); + } + prevMax = newMaxValue; + + const color = colors[i]; + + if (!color) { + // TODO remove this error + throw new Error('color is undefined????????'); + } + + if (isFiniteNumber(min)) { + acc.domain.push(min); + acc.colors.push(color); + } + if (isFiniteNumber(max)) { + acc.domain.push(max); + acc.colors.push(color); + } + + return acc; + }, + { + domain: [], + colors: [], + }, + ); +} + +/** @internal */ +export function getColorScale( + baseDomain: ContinuousDomain, + flippedDomain: boolean, + config: BulletColorConfig, + fullTicks: number[], + backgroundColor: Color, + fallbackBandColor: Color, +): ChromaColorScale { + const { colors, domain, classes } = getScaleInputs(baseDomain, flippedDomain, config, fullTicks, backgroundColor); + const scale = chroma.scale(colors).mode('lab').domain(domain); + + if (classes) scale.classes(classes); + const isInDomain = isWithinRange(baseDomain); + + return (n) => (isInDomain(n) ? scale(n) : getChromaColor(fallbackBandColor)); +} + +/** @internal */ +export interface BandPositions { + start: number; + end: number; + size: number; +} + +/** @internal */ +export type ColorTick = { color: Color } & BandPositions; + +// TODO memoize for duplicate calls +/** @internal */ +export function getColorBands( + scale: ScaleLinear, + config: BulletColorConfig, + ticks: number[], + backgroundColor: Color, + fallbackBandColor: Color, +): { + scale: ChromaColorScale; + bands: ColorTick[]; +} { + const domain = scale.domain() as GenericDomain; + const orderedDomain = sortNumbers(domain) as ContinuousDomain; + const fullTicks = getFullDomainTicks(orderedDomain, ticks); + const colorScale = getColorScale( + orderedDomain, + domain[0] > domain[1], + config, + sortNumbers(fullTicks), + backgroundColor, + fallbackBandColor, + ); + const scaledBandPositions = fullTicks.reduce<[pixelPosition: BandPositions, tick: number][]>((acc, start, i) => { + const end = fullTicks[i + 1]; + if (end === undefined) return acc; + const scaledStart = scale(start); + const scaledEnd = scale(end); + const size = Math.abs(scaledEnd - scaledStart); + acc.push([ + { start: scaledStart, end: scaledEnd, size }, + // pegs color at start of band - maybe allow control of this later + start + (end - start) / 2, + ]); + return acc; + }, []); + + // TODO allow continuous gradients + const bands = scaledBandPositions.reduce((acc, [pxPosition, tick]) => { + return [ + ...acc, + { + ...pxPosition, + color: colorScale(tick).hex(), + }, + ]; + }, []); + + return { + scale: colorScale, + bands, + }; +} diff --git a/packages/charts/src/chart_types/bullet_graph/utils/ticks.ts b/packages/charts/src/chart_types/bullet_graph/utils/ticks.ts new file mode 100644 index 0000000000..9c6d70fdbe --- /dev/null +++ b/packages/charts/src/chart_types/bullet_graph/utils/ticks.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { clamp, isFiniteNumber } from '../../../utils/common'; +import { MAX_TICK_COUNT, MIN_TICK_COUNT } from '../renderer/canvas/constants'; +import { BulletDatum } from '../spec'; + +/** @internal */ +export interface TickOptions { + nice?: boolean; + interval: number; + desiredTicks: BulletDatum['ticks']; + /** + * Amplifies range by constant, use for angular to convert angles to arc lengths + */ + rangeMultiplier?: number; +} + +/** @internal */ +export function getTicks(length: number, { desiredTicks, interval }: Omit) { + if ((isFiniteNumber(desiredTicks) && desiredTicks > 0) || typeof desiredTicks === 'function') return desiredTicks; + const target = Math.floor(length / interval); + return clamp(target, MIN_TICK_COUNT, MAX_TICK_COUNT); +} diff --git a/packages/charts/src/chart_types/goal_chart/layout/viewmodel/utils.ts b/packages/charts/src/chart_types/goal_chart/layout/viewmodel/utils.ts index 8d44dc976c..87006bc16d 100644 --- a/packages/charts/src/chart_types/goal_chart/layout/viewmodel/utils.ts +++ b/packages/charts/src/chart_types/goal_chart/layout/viewmodel/utils.ts @@ -77,11 +77,15 @@ const controllingAngle = (angleStart: Radian, angleEnd: Radian): number => { * Assumes angles are no more that 2Ï€ apart. * @internal */ -export function normalizeAngles(angleStart: Radian, angleEnd: Radian): [angleStart: Radian, angleEnd: Radian] { +export function normalizeAngles( + angleStart: Radian, + angleEnd: Radian, + multiplier = 1, +): [angleStart: Radian, angleEnd: Radian] { const maxOffset = Math.max(Math.ceil(Math.abs(angleStart) / TAU), Math.ceil(Math.abs(angleEnd) / TAU)) - 1; const offsetDirection = angleStart > 0 && angleEnd > 0 ? -1 : 1; const offset = offsetDirection * maxOffset * TAU; - return [angleStart + offset, angleEnd + offset]; + return [multiplier * (angleStart + offset), multiplier * (angleEnd + offset)]; } /** diff --git a/packages/charts/src/chart_types/goal_chart/specs/index.ts b/packages/charts/src/chart_types/goal_chart/specs/index.ts index 4a65f47ee8..f588dcc318 100644 --- a/packages/charts/src/chart_types/goal_chart/specs/index.ts +++ b/packages/charts/src/chart_types/goal_chart/specs/index.ts @@ -93,6 +93,7 @@ const buildProps = buildSFProps()( /** * Add Goal spec to chart + * @deprecated please use `BulletGraph` spec instead * @alpha */ export const Goal = function ( diff --git a/packages/charts/src/chart_types/heatmap/layout/viewmodel/scenegraph.ts b/packages/charts/src/chart_types/heatmap/layout/viewmodel/scenegraph.ts index 406edbbfab..b41fb1f92e 100644 --- a/packages/charts/src/chart_types/heatmap/layout/viewmodel/scenegraph.ts +++ b/packages/charts/src/chart_types/heatmap/layout/viewmodel/scenegraph.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { ColorScale } from '../../../../common/colors'; import { SmallMultipleScales, SmallMultiplesGroupBy } from '../../../../common/panel_utils'; import { withTextMeasure } from '../../../../utils/bbox/canvas_text_bbox_calculator'; import { Theme } from '../../../../utils/themes/theme'; @@ -14,7 +15,6 @@ import { ShapeViewModel } from '../../layout/types/viewmodel_types'; import { shapeViewModel } from '../../layout/viewmodel/viewmodel'; import { HeatmapSpec } from '../../specs'; import { ChartElementSizes } from '../../state/selectors/compute_chart_element_sizes'; -import { ColorScale } from '../../state/selectors/get_color_scale'; import { HeatmapTable } from '../../state/selectors/get_heatmap_table'; /** @internal */ diff --git a/packages/charts/src/chart_types/heatmap/layout/viewmodel/viewmodel.ts b/packages/charts/src/chart_types/heatmap/layout/viewmodel/viewmodel.ts index 8f76c37e05..a94ab8a2e0 100644 --- a/packages/charts/src/chart_types/heatmap/layout/viewmodel/viewmodel.ts +++ b/packages/charts/src/chart_types/heatmap/layout/viewmodel/viewmodel.ts @@ -10,6 +10,7 @@ import { ScaleBand, scaleBand, scaleQuantize } from 'd3-scale'; import { BaseDatum } from './../../../xy_chart/utils/specs'; import { colorToRgba } from '../../../../common/color_library_wrappers'; +import { ColorScale } from '../../../../common/colors'; import { fillTextColor } from '../../../../common/fill_text_color'; import { Pixels } from '../../../../common/geometry'; import { @@ -35,7 +36,6 @@ import { PrimitiveValue } from '../../../partition_chart/layout/utils/group_by_r import { ChartDimensions } from '../../../xy_chart/utils/dimensions'; import { HeatmapSpec } from '../../specs'; import { ChartElementSizes } from '../../state/selectors/compute_chart_element_sizes'; -import { ColorScale } from '../../state/selectors/get_color_scale'; import { HeatmapTable } from '../../state/selectors/get_heatmap_table'; import { Cell, diff --git a/packages/charts/src/chart_types/heatmap/renderer/canvas/connected_component.tsx b/packages/charts/src/chart_types/heatmap/renderer/canvas/connected_component.tsx index 7b87c5114a..3cca760a2f 100644 --- a/packages/charts/src/chart_types/heatmap/renderer/canvas/connected_component.tsx +++ b/packages/charts/src/chart_types/heatmap/renderer/canvas/connected_component.tsx @@ -24,6 +24,7 @@ import { getChartThemeSelector } from '../../../../state/selectors/get_chart_the import { getInternalIsInitializedSelector, InitStatus } from '../../../../state/selectors/get_internal_is_intialized'; import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_spec'; import { Dimensions } from '../../../../utils/dimensions'; +import { GenericDomain } from '../../../../utils/domain'; import { deepEqual } from '../../../../utils/fast_deep_equal'; import { LIGHT_THEME } from '../../../../utils/themes/light_theme'; import { Theme } from '../../../../utils/themes/theme'; @@ -38,7 +39,7 @@ export interface ReactiveChartStateProps { initialized: boolean; geometries: ShapeViewModel; chartContainerDimensions: Dimensions; - highlightedLegendBands: Array<[start: number, end: number]>; + highlightedLegendBands: Array; theme: Theme; a11ySettings: A11ySettings; background: Color; diff --git a/packages/charts/src/chart_types/heatmap/renderer/canvas/utils.ts b/packages/charts/src/chart_types/heatmap/renderer/canvas/utils.ts index 908ef8503d..26e76befed 100644 --- a/packages/charts/src/chart_types/heatmap/renderer/canvas/utils.ts +++ b/packages/charts/src/chart_types/heatmap/renderer/canvas/utils.ts @@ -8,6 +8,7 @@ import { overrideOpacity } from '../../../../common/color_library_wrappers'; import { Fill, Stroke } from '../../../../geoms/types'; +import { GenericDomain } from '../../../../utils/domain'; import { GeometryStateStyle, SharedGeometryStateStyle } from '../../../../utils/themes/theme'; import { Cell } from '../../layout/types/viewmodel_types'; import { isValueInRanges } from '../../layout/viewmodel/viewmodel'; @@ -16,7 +17,7 @@ import { isValueInRanges } from '../../layout/viewmodel/viewmodel'; export function getGeometryStateStyle( cell: Cell, sharedGeometryStyle: SharedGeometryStateStyle, - highlightedLegendBands: Array<[start: number, end: number]>, + highlightedLegendBands: Array, ): GeometryStateStyle { const { default: defaultStyles, highlighted, unhighlighted } = sharedGeometryStyle; diff --git a/packages/charts/src/chart_types/heatmap/scales/band_color_scale.ts b/packages/charts/src/chart_types/heatmap/scales/band_color_scale.ts index ca7868166e..d82e336171 100644 --- a/packages/charts/src/chart_types/heatmap/scales/band_color_scale.ts +++ b/packages/charts/src/chart_types/heatmap/scales/band_color_scale.ts @@ -6,11 +6,10 @@ * Side Public License, v 1. */ -import { Colors } from '../../../common/colors'; +import { Colors, ColorScale } from '../../../common/colors'; import { getPredicateFn } from '../../../common/predicate'; import { isFiniteNumber, safeFormat, ValueFormatter } from '../../../utils/common'; import { ColorBand, HeatmapBandsColorScale } from '../specs/heatmap'; -import { ColorScale } from '../state/selectors/get_color_scale'; function defaultColorBandFormatter(valueFormatter?: ValueFormatter) { return (startValue: number, endValue: number) => { diff --git a/packages/charts/src/chart_types/heatmap/state/selectors/get_color_scale.ts b/packages/charts/src/chart_types/heatmap/state/selectors/get_color_scale.ts index 27df94c690..a10d584acd 100644 --- a/packages/charts/src/chart_types/heatmap/state/selectors/get_color_scale.ts +++ b/packages/charts/src/chart_types/heatmap/state/selectors/get_color_scale.ts @@ -7,15 +7,12 @@ */ import { getHeatmapSpecSelector } from './get_heatmap_spec'; -import { Color } from '../../../../common/colors'; +import { ColorScale } from '../../../../common/colors'; import { createCustomCachedSelector } from '../../../../state/create_selector'; import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_spec'; import { getBandsColorScale } from '../../scales/band_color_scale'; import { ColorBand } from '../../specs/heatmap'; -/** @internal */ -export type ColorScale = (value: number) => Color; - /** * @internal * Gets color scale based on specification and values range. diff --git a/packages/charts/src/chart_types/heatmap/state/selectors/get_highlighted_legend_bands.ts b/packages/charts/src/chart_types/heatmap/state/selectors/get_highlighted_legend_bands.ts index 9225167196..a4db0fdf22 100644 --- a/packages/charts/src/chart_types/heatmap/state/selectors/get_highlighted_legend_bands.ts +++ b/packages/charts/src/chart_types/heatmap/state/selectors/get_highlighted_legend_bands.ts @@ -9,11 +9,12 @@ import { getColorScale } from './get_color_scale'; import { getHighlightedLegendItemSelector } from './get_highlighted_legend_item'; import { createCustomCachedSelector } from '../../../../state/create_selector'; +import { GenericDomain } from '../../../../utils/domain'; /** @internal */ export const getHighlightedLegendBandsSelector = createCustomCachedSelector( [getHighlightedLegendItemSelector, getColorScale], - (highlightedLegendItem, { bands }): Array<[start: number, end: number]> => { + (highlightedLegendItem, { bands }): Array => { if (!highlightedLegendItem) return []; // instead of using the specId, each legend item is associated with an unique band label return bands.filter(({ label }) => highlightedLegendItem.label === label).map(({ start, end }) => [start, end]); diff --git a/packages/charts/src/chart_types/index.ts b/packages/charts/src/chart_types/index.ts index 2726dc7ce7..d53b31f064 100644 --- a/packages/charts/src/chart_types/index.ts +++ b/packages/charts/src/chart_types/index.ts @@ -22,6 +22,7 @@ export const ChartType = Object.freeze({ Heatmap: 'heatmap' as const, Wordcloud: 'wordcloud' as const, Metric: 'metric' as const, + BulletGraph: 'bullet_graph' as const, }); /** @public */ export type ChartType = $Values; diff --git a/packages/charts/src/chart_types/metric/renderer/_index.scss b/packages/charts/src/chart_types/metric/renderer/_index.scss index 292d519d37..996596089b 100644 --- a/packages/charts/src/chart_types/metric/renderer/_index.scss +++ b/packages/charts/src/chart_types/metric/renderer/_index.scss @@ -48,14 +48,31 @@ } &--vertical { - &.echMetric--small { - padding-left: 10px; + // TODO: find a better way to style based on sizes (i.e. sass functions) + &.echMetric--withProgressBar { + &--small { + padding-left: 10px; + } + } + + &.echMetric--withTargetProgressBar { + &--small { + padding-left: 14px; + } } } &--horizontal { - &.echMetric--small { - padding-bottom: 10px; + &.echMetric--withProgressBar { + &--small { + padding-bottom: 10px; + } + } + + &.echMetric--withTargetProgressBar { + &--small { + padding-bottom: 12px; + } } } } diff --git a/packages/charts/src/chart_types/metric/renderer/dom/_progress.scss b/packages/charts/src/chart_types/metric/renderer/dom/_progress.scss index db4fd75a86..7bc2809585 100644 --- a/packages/charts/src/chart_types/metric/renderer/dom/_progress.scss +++ b/packages/charts/src/chart_types/metric/renderer/dom/_progress.scss @@ -1,6 +1,5 @@ .echSingleMetricProgress { position: absolute; - overflow: hidden; &--vertical { left: 0; @@ -9,6 +8,7 @@ bottom: 0; height: 100%; width: 100%; + &.echSingleMetricProgress--small { right: auto; width: 10px; @@ -22,6 +22,7 @@ top: 0; bottom: 0; height: 100%; + &.echSingleMetricProgress--small { top: auto; height: 10px; @@ -31,6 +32,7 @@ .echSingleMetricProgressBar { transition: background-color ease-in-out 0.1s; + &--vertical { position: absolute; left: 0; @@ -38,6 +40,7 @@ bottom: 0; width: 100%; } + &--horizontal { position: absolute; top: 0; @@ -46,3 +49,54 @@ height: 100%; } } + +.echSingleMetricTarget { + display: flex; + justify-content: center; + align-items: center; + overflow: visible; + z-index: 1; // needed to pop up to top for hovered title + + &--vertical { + transform: rotate(90deg); + position: absolute; + left: 100%; + bottom: 0; + } + + &--horizontal { + position: absolute; + bottom: 100%; + } +} + +.echSingleMetricZeroBaseline { + display: flex; + justify-content: center; + align-items: center; + overflow: visible; + $line-width: 2px; + $line-length: 13px; + + &--vertical { + position: absolute; + left: 0; + + &.echSingleMetricZeroBaseline--small { + right: auto; + height: $line-width; + width: $line-length; + } + } + + &--horizontal { + position: absolute; + bottom: 0; + + &.echSingleMetricZeroBaseline--small { + top: auto; + width: $line-width; + height: $line-length; + } + } +} diff --git a/packages/charts/src/chart_types/metric/renderer/dom/index.tsx b/packages/charts/src/chart_types/metric/renderer/dom/index.tsx index f3524603d0..c06d0aaa7c 100644 --- a/packages/charts/src/chart_types/metric/renderer/dom/index.tsx +++ b/packages/charts/src/chart_types/metric/renderer/dom/index.tsx @@ -17,7 +17,7 @@ import { bindActionCreators, Dispatch } from 'redux'; import { Metric as MetricComponent } from './metric'; import { ColorContrastOptions, highContrastColor } from '../../../../common/color_calcs'; import { colorToRgba } from '../../../../common/color_library_wrappers'; -import { getResolvedBackgroundColor } from '../../../../common/fill_text_color'; +import { Color } from '../../../../common/colors'; import { BasicListener, ElementClickListener, ElementOverListener, settingsBuildProps } from '../../../../specs'; import { onChartRendered } from '../../../../state/actions/chart'; import { GlobalChartState } from '../../../../state/chart_state'; @@ -28,9 +28,10 @@ import { } from '../../../../state/selectors/get_accessibility_config'; import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; import { getInternalIsInitializedSelector, InitStatus } from '../../../../state/selectors/get_internal_is_intialized'; +import { getResolvedBackgroundColorSelector } from '../../../../state/selectors/get_resolved_background_color'; import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_spec'; import { LIGHT_THEME } from '../../../../utils/themes/light_theme'; -import { BackgroundStyle, MetricStyle } from '../../../../utils/themes/theme'; +import { MetricStyle } from '../../../../utils/themes/theme'; import { MetricSpec } from '../../specs'; import { chartSize } from '../../state/selectors/chart_size'; import { getMetricSpecs } from '../../state/selectors/data'; @@ -47,7 +48,7 @@ interface StateProps { specs: MetricSpec[]; a11y: A11ySettings; style: MetricStyle; - background: BackgroundStyle; + backgroundColor: Color; locale: string; onElementClick?: ElementClickListener; onElementOut?: BasicListener; @@ -77,7 +78,7 @@ class Component extends React.Component { a11y, specs: [spec], // ignoring other specs style, - background, + backgroundColor, onElementClick, onElementOut, onElementOver, @@ -95,7 +96,6 @@ class Component extends React.Component { }, 0); const panel = { width: width / maxColumns, height: height / totalRows }; - const backgroundColor = getResolvedBackgroundColor(background.fallbackColor, background.color); const contrastOptions: ColorContrastOptions = { lightColor: colorToRgba(style.text.lightColor), darkColor: colorToRgba(style.text.darkColor), @@ -190,7 +190,7 @@ const DEFAULT_PROPS: StateProps = { }, a11y: DEFAULT_A11Y_SETTINGS, style: LIGHT_THEME.metric, - background: LIGHT_THEME.background, + backgroundColor: LIGHT_THEME.background.color, locale: settingsBuildProps.defaults.locale, }; @@ -199,7 +199,7 @@ const mapStateToProps = (state: GlobalChartState): StateProps => { return DEFAULT_PROPS; } const { onElementClick, onElementOut, onElementOver, locale } = getSettingsSpecSelector(state); - const { metric: style, background } = getChartThemeSelector(state); + const { metric: style } = getChartThemeSelector(state); return { initialized: true, chartId: state.chartId, @@ -210,7 +210,7 @@ const mapStateToProps = (state: GlobalChartState): StateProps => { onElementClick, onElementOver, onElementOut, - background, + backgroundColor: getResolvedBackgroundColorSelector(state), style, locale, }; diff --git a/packages/charts/src/chart_types/metric/renderer/dom/metric.tsx b/packages/charts/src/chart_types/metric/renderer/dom/metric.tsx index e69f0249b8..e2ad7679fc 100644 --- a/packages/charts/src/chart_types/metric/renderer/dom/metric.tsx +++ b/packages/charts/src/chart_types/metric/renderer/dom/metric.tsx @@ -24,10 +24,10 @@ import { MetricDatum, MetricElementEvent, } from '../../../../specs'; -import { LayoutDirection } from '../../../../utils/common'; +import { LayoutDirection, isNil } from '../../../../utils/common'; import { Size } from '../../../../utils/dimensions'; import { MetricStyle } from '../../../../utils/themes/theme'; -import { isMetricWProgress, isMetricWTrend } from '../../specs'; +import { MetricWNumber, isMetricWProgress, isMetricWTrend } from '../../specs'; /** @internal */ export const Metric: React.FunctionComponent<{ @@ -63,6 +63,7 @@ export const Metric: React.FunctionComponent<{ onElementOver, onElementOut, }) => { + const progressBarSize = 'small'; // currently we provide only the small progress bar; const [mouseState, setMouseState] = useState<'leave' | 'enter' | 'down'>('leave'); const [lastMouseDownTimestamp, setLastMouseDownTimestamp] = useState(0); const metricHTMLId = `echMetric-${chartId}-${rowIndex}-${columnIndex}`; @@ -73,9 +74,10 @@ export const Metric: React.FunctionComponent<{ 'echMetric--rightBorder': columnIndex < totalColumns - 1, 'echMetric--bottomBorder': rowIndex < totalRows - 1, 'echMetric--topBorder': hasTitles && rowIndex === 0, - 'echMetric--small': hasProgressBar, 'echMetric--vertical': progressBarDirection === LayoutDirection.Vertical, 'echMetric--horizontal': progressBarDirection === LayoutDirection.Horizontal, + [`echMetric--withProgressBar--${progressBarSize}`]: hasProgressBar, + [`echMetric--withTargetProgressBar--${progressBarSize}`]: !isNil((datum as MetricWNumber)?.target), }); const lightnessAmount = mouseState === 'leave' ? 0 : mouseState === 'enter' ? 0.05 : 0.1; @@ -162,12 +164,13 @@ export const Metric: React.FunctionComponent<{ panel={panel} style={style} onElementClick={onElementClick ? onElementClickHandler : undefined} + progressBarSize={progressBarSize} highContrastTextColor={finalTextColor.keyword} locale={locale} /> {isMetricWTrend(datumWithInteractionColor) && } {isMetricWProgress(datumWithInteractionColor) && ( - + )}
diff --git a/packages/charts/src/chart_types/metric/renderer/dom/progress.tsx b/packages/charts/src/chart_types/metric/renderer/dom/progress.tsx index e61b8258ef..45f5bba413 100644 --- a/packages/charts/src/chart_types/metric/renderer/dom/progress.tsx +++ b/packages/charts/src/chart_types/metric/renderer/dom/progress.tsx @@ -7,44 +7,102 @@ */ import classNames from 'classnames'; +import { scaleLinear } from 'd3-scale'; import React from 'react'; import { Color } from '../../../../common/colors'; -import { clamp, LayoutDirection } from '../../../../utils/common'; -import { MetricWProgress } from '../../specs'; +import { Icon } from '../../../../components/icons/icon'; +import { isNil, LayoutDirection, sortNumbers } from '../../../../utils/common'; +import { ContinuousDomain, GenericDomain } from '../../../../utils/domain'; +import { BulletMetricWProgress, isBulletMetric, MetricWProgress } from '../../specs'; -/** @internal */ -export const ProgressBar: React.FunctionComponent<{ - datum: MetricWProgress; +const TARGET_SIZE = 8; +const BASELINE_SIZE = 2; + +interface ProgressBarProps { + datum: MetricWProgress | BulletMetricWProgress; barBackground: Color; -}> = ({ datum: { title, domainMax, value, color, progressBarDirection }, barBackground }) => { - const verticalDirection = progressBarDirection === LayoutDirection.Vertical; - // currently we provide only the small progress bar; - const isSmall = true; - const percent = Number(clamp((value / domainMax) * 100, 0, 100).toFixed(1)); - - const bgClassName = classNames('echSingleMetricProgress', { - 'echSingleMetricProgress--vertical': verticalDirection, - 'echSingleMetricProgress--horizontal': !verticalDirection, - 'echSingleMetricProgress--small': isSmall, - }); - const barClassName = classNames('echSingleMetricProgressBar', { - 'echSingleMetricProgressBar--vertical': verticalDirection, - 'echSingleMetricProgressBar--horizontal': !verticalDirection, - 'echSingleMetricProgressBar--small': isSmall, - }); - const percentProp = verticalDirection ? { height: `${percent}%` } : { width: `${percent}%` }; + size: 'small'; +} + +/** @internal */ +export const ProgressBar: React.FunctionComponent = ({ datum, barBackground, size }) => { + const { title, value, target, color, valueFormatter, targetFormatter, progressBarDirection } = datum; + const isBullet = isBulletMetric(datum); + const isVertical = progressBarDirection === LayoutDirection.Vertical; + const domain: GenericDomain = isBulletMetric(datum) ? datum.domain : [0, datum.domainMax]; + // TODO clamp and round values + const scale = scaleLinear().domain(domain).range([0, 100]); + + if (isBulletMetric(datum) && datum.niceDomain) { + scale.nice(); + } + + const updatedDomain = scale.domain() as GenericDomain; + const [domainMin, domainMax] = sortNumbers(updatedDomain) as ContinuousDomain; + const scaledValue = scale(value); + const [min, max] = sortNumbers([scale(0), scaledValue]); + const positionStyle = isVertical + ? { + bottom: `${min}%`, + top: `${100 - max}%`, + } + : { + left: `${min}%`, + right: `${100 - max}%`, + }; + + const targetPlacement = isNil(target) ? null : `calc(${scale(target)}% - ${TARGET_SIZE / 2}px)`; + const zeroPlacement = domainMin >= 0 || domainMax <= 0 ? null : `calc(${scale(0)}% - ${BASELINE_SIZE / 2}px)`; + + const labelType = isBullet ? 'Value' : 'Percentage'; return ( -
+
+ {targetPlacement && ( +
+ +
+ )} + {zeroPlacement && ( +
+ )}
); }; + +function getDirectionalClasses(element: string, isVertical: boolean, size: ProgressBarProps['size']) { + const base = `echSingleMetric${element}`; + return classNames(base, `${base}--${size}`, { + [`${base}--vertical`]: isVertical, + [`${base}--horizontal`]: !isVertical, + }); +} diff --git a/packages/charts/src/chart_types/metric/renderer/dom/text.tsx b/packages/charts/src/chart_types/metric/renderer/dom/text.tsx index ff8e1de6f4..89591a51c1 100644 --- a/packages/charts/src/chart_types/metric/renderer/dom/text.tsx +++ b/packages/charts/src/chart_types/metric/renderer/dom/text.tsx @@ -169,15 +169,16 @@ export const MetricText: React.FunctionComponent<{ style: MetricStyle; onElementClick?: () => void; highContrastTextColor: Color; + progressBarSize: 'small'; locale: string; -}> = ({ id, datum, panel, style, onElementClick, highContrastTextColor, locale }) => { +}> = ({ id, datum, panel, style, onElementClick, highContrastTextColor, progressBarSize, locale }) => { const { extra, value } = datum; const size = findRange(WIDTH_BP, panel.width); const hasProgressBar = isMetricWProgress(datum); const progressBarDirection = isMetricWProgress(datum) ? datum.progressBarDirection : undefined; const containerClassName = classNames('echMetricText', { - 'echMetricText--small': hasProgressBar, + [`echMetricText--${progressBarSize}`]: hasProgressBar, 'echMetricText--vertical': progressBarDirection === LayoutDirection.Vertical, 'echMetricText--horizontal': progressBarDirection === LayoutDirection.Horizontal, }); diff --git a/packages/charts/src/chart_types/metric/specs/index.ts b/packages/charts/src/chart_types/metric/specs/index.ts index b50fda01c2..9d1341898f 100644 --- a/packages/charts/src/chart_types/metric/specs/index.ts +++ b/packages/charts/src/chart_types/metric/specs/index.ts @@ -14,7 +14,9 @@ import { Color } from '../../../common/colors'; import { Spec } from '../../../specs'; import { SpecType } from '../../../specs/constants'; import { specComponentFactory } from '../../../state/spec_factory'; -import { LayoutDirection } from '../../../utils/common'; +import { LayoutDirection, ValueFormatter } from '../../../utils/common'; +import { GenericDomain } from '../../../utils/domain'; +import { BulletValueLabels } from '../../bullet_graph/spec'; /** @alpha */ export type MetricBase = { @@ -35,7 +37,12 @@ export type MetricWText = MetricBase & { /** @alpha */ export type MetricWNumber = MetricBase & { value: number; - valueFormatter: (d: number) => string; + target?: number; + valueFormatter: ValueFormatter; + /** + * Used for header display only, defaults to `valueFormatter` + */ + targetFormatter?: ValueFormatter; }; /** @alpha */ @@ -44,6 +51,18 @@ export type MetricWProgress = MetricWNumber & { progressBarDirection: LayoutDirection; }; +/** + * Type used internally by bullet + * TODO - discuss usage of this externally + * + * @internal + */ +export type BulletMetricWProgress = Omit & { + domain: GenericDomain; + niceDomain?: boolean; + valueLabels: Omit; +}; + /** @alpha */ export const MetricTrendShape = Object.freeze({ Bars: 'bars' as const, @@ -85,6 +104,11 @@ export const Metric = specComponentFactory()( /** @alpha */ export type MetricSpecProps = ComponentProps; +/** @internal */ +export function isBulletMetric(datum: MetricDatum): datum is BulletMetricWProgress { + return Array.isArray((datum as BulletMetricWProgress).domain); +} + /** @internal */ export function isMetricWNumber(datum: MetricDatum): datum is MetricWNumber { return typeof datum.value === 'number' && datum.hasOwnProperty('valueFormatter'); @@ -96,7 +120,10 @@ export function isMetricWText(datum: MetricDatum): datum is MetricWNumber { /** @internal */ export function isMetricWProgress(datum: MetricDatum): datum is MetricWProgress { - return isMetricWNumber(datum) && datum.hasOwnProperty('domainMax') && !datum.hasOwnProperty('trend'); + return ( + (isMetricWNumber(datum) && datum.hasOwnProperty('domainMax') && !datum.hasOwnProperty('trend')) || + isBulletMetric(datum) + ); } /** @internal */ diff --git a/packages/charts/src/chart_types/specs.ts b/packages/charts/src/chart_types/specs.ts index dc9df8b980..c95e647525 100644 --- a/packages/charts/src/chart_types/specs.ts +++ b/packages/charts/src/chart_types/specs.ts @@ -47,3 +47,6 @@ export { MetricTrendShape, MetricDatum, } from './metric/specs'; + +export { BulletGraph, BulletGraphSpec, BulletDatum, BulletGraphSubtype, BulletValueLabels } from './bullet_graph/spec'; +export { BulletGraphStyle } from './bullet_graph/theme'; diff --git a/packages/charts/src/chart_types/xy_chart/renderer/canvas/lines.ts b/packages/charts/src/chart_types/xy_chart/renderer/canvas/lines.ts index 3e66a1cafa..c51968c6e5 100644 --- a/packages/charts/src/chart_types/xy_chart/renderer/canvas/lines.ts +++ b/packages/charts/src/chart_types/xy_chart/renderer/canvas/lines.ts @@ -12,12 +12,14 @@ import { renderLinePaths } from './primitives/path'; import { buildLineStyles } from './styles/line'; import { withPanelTransform } from './utils/panel_transform'; import { colorToRgba, overrideOpacity } from '../../../../common/color_library_wrappers'; +import { Radian } from '../../../../common/geometry'; import { LegendItem } from '../../../../common/legend'; import { Rect, Stroke } from '../../../../geoms/types'; import { withContext } from '../../../../renderers/canvas'; import { ColorVariant, Rotation } from '../../../../utils/common'; import { Dimensions } from '../../../../utils/dimensions'; import { LineGeometry, PerPanel } from '../../../../utils/geometry'; +import { Point } from '../../../../utils/point'; import { SharedGeometryStateStyle } from '../../../../utils/themes/theme'; import { getGeometryStateStyle } from '../../rendering/utils'; @@ -95,3 +97,23 @@ function renderLine( shouldClip && style.fit.line.visible, ); } + +/** + * Draws line along a polar axis + * @internal + */ +export function drawPolarLine( + ctx: CanvasRenderingContext2D, + angle: Radian, + radius: number, + length: number, + center: Point = { x: 0, y: 0 }, +) { + const y1 = Math.sin(angle) * (radius - length / 2); + const x1 = Math.cos(angle) * (radius - length / 2); + const y2 = Math.sin(angle) * (radius + length / 2); + const x2 = Math.cos(angle) * (radius + length / 2); + + ctx.moveTo(center.x + x1, center.y + y1); + ctx.lineTo(center.x + x2, center.y + y2); +} diff --git a/packages/charts/src/chart_types/xy_chart/renderer/canvas/utils/debug.ts b/packages/charts/src/chart_types/xy_chart/renderer/canvas/utils/debug.ts index 7997018c6f..980b73f611 100644 --- a/packages/charts/src/chart_types/xy_chart/renderer/canvas/utils/debug.ts +++ b/packages/charts/src/chart_types/xy_chart/renderer/canvas/utils/debug.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { RGBATupleToString } from '../../../../../common/color_library_wrappers'; import { Fill, Stroke, Rect } from '../../../../../geoms/types'; import { withContext } from '../../../../../renderers/canvas'; import { degToRad } from '../../../../../utils/common'; @@ -24,8 +25,8 @@ const DEFAULT_DEBUG_STROKE: Stroke = { export function renderDebugRect( ctx: CanvasRenderingContext2D, rect: Rect, - rotation: number = 0, - fill = DEFAULT_DEBUG_FILL, // violet + rotation = 0, + fill = DEFAULT_DEBUG_FILL, stroke = DEFAULT_DEBUG_STROKE, ) { withContext(ctx, () => { @@ -53,3 +54,25 @@ export function renderDebugRectCenterRotated( renderRect(ctx, { ...rect, x: x - rect.width / 2, y: y - rect.height / 2 }, fill, stroke); }); } + +/** @internal */ +export function renderDebugPoint( + ctx: CanvasRenderingContext2D, + x: number, + y: number, + size = 16, + stroke = DEFAULT_DEBUG_STROKE, +) { + withContext(ctx, () => { + ctx.lineWidth = stroke.width; + ctx.strokeStyle = RGBATupleToString(stroke.color); + + ctx.moveTo(x - size, y); + ctx.lineTo(x + size, y); + + ctx.moveTo(x, y - size); + ctx.lineTo(x, y + size); + + ctx.stroke(); + }); +} diff --git a/packages/charts/src/common/color_library_wrappers.ts b/packages/charts/src/common/color_library_wrappers.ts index da4309b1f0..f0d674c3f3 100644 --- a/packages/charts/src/common/color_library_wrappers.ts +++ b/packages/charts/src/common/color_library_wrappers.ts @@ -56,7 +56,12 @@ export function isValid(color: Color): chroma.Color | false { } /** @internal */ -export function getChromaColor(color: RgbaTuple): chroma.Color { +export function getChromaColor(color: string): chroma.Color; +/** @internal */ +export function getChromaColor(color: RgbaTuple): chroma.Color; +/** @internal */ +export function getChromaColor(color: string | RgbaTuple): chroma.Color { + if (typeof color === 'string') return chroma(color.toLowerCase()); // chroma mutates the input return chroma(...color); } diff --git a/packages/charts/src/common/colors.tsx b/packages/charts/src/common/colors.tsx index 332379c78d..d60a0f69c6 100644 --- a/packages/charts/src/common/colors.tsx +++ b/packages/charts/src/common/colors.tsx @@ -6,6 +6,8 @@ * Side Public License, v 1. */ +import chroma from 'chroma-js'; + import { RgbaTuple } from './color_library_wrappers'; /** @@ -14,6 +16,12 @@ import { RgbaTuple } from './color_library_wrappers'; */ export type Color = string; // todo static/runtime type it this for proper color string content; several places in the code, and ultimate use, dictate it not be an empty string +/** @internal */ +export type ColorScale = (value: number) => T; + +/** @internal */ +export type ChromaColorScale = ColorScale; + /** @internal */ export interface ColorDefinition { keyword: Color; diff --git a/packages/charts/src/common/fill_text_color.ts b/packages/charts/src/common/fill_text_color.ts index 68b2371264..79ee92a62d 100644 --- a/packages/charts/src/common/fill_text_color.ts +++ b/packages/charts/src/common/fill_text_color.ts @@ -7,7 +7,7 @@ */ import { ColorContrastOptions, HighContrastResult, combineColors, highContrastColor } from './color_calcs'; -import { colorToRgba, RGBATupleToString } from './color_library_wrappers'; +import { colorToRgba } from './color_library_wrappers'; import { Color, Colors } from './colors'; /** @@ -42,17 +42,3 @@ export function fillTextColor( return highContrastColor(backgroundRGBA); } - -/** @internal */ -export function getResolvedBackgroundColor( - fallbackBGColor: Color, - background: Color = Colors.Transparent.keyword, -): Color { - let backgroundRGBA = colorToRgba(background); - - if (backgroundRGBA[3] < TRANSPARENT_LIMIT) { - backgroundRGBA = colorToRgba(fallbackBGColor); - } - - return RGBATupleToString(backgroundRGBA); -} diff --git a/packages/charts/src/components/_index.scss b/packages/charts/src/components/_index.scss index ce2091babf..587bf75fa1 100644 --- a/packages/charts/src/components/_index.scss +++ b/packages/charts/src/components/_index.scss @@ -6,6 +6,8 @@ @import 'icons/index'; @import 'legend/index'; @import 'unavailable_chart'; +@import 'grid'; +@import 'grid/aligned_grid'; @import '../chart_types/xy_chart/renderer/index'; @import '../chart_types/partition_chart/renderer/index'; diff --git a/packages/charts/src/components/grid/_aligned_grid.scss b/packages/charts/src/components/grid/_aligned_grid.scss new file mode 100644 index 0000000000..aa889cf353 --- /dev/null +++ b/packages/charts/src/components/grid/_aligned_grid.scss @@ -0,0 +1,25 @@ +.echAlignedGrid { + display: grid; + align-content: stretch; + width: 100%; + height: 100%; +} +.echAlignedGrid--header { + width: 100%; + height: 100%; + margin: 0; + padding: 0; +} +.echAlignedGrid__borderRight { + border-right: 1px solid #edf0f5; +} +.echAlignedGrid__borderBottom { + border-bottom: 1px solid #edf0f5; +} + +.echAlignedGrid--content { + width: 100%; + min-height: 0; + margin: 0; + padding: 0; +} diff --git a/packages/charts/src/components/grid/_index.scss b/packages/charts/src/components/grid/_index.scss new file mode 100644 index 0000000000..0829b28c31 --- /dev/null +++ b/packages/charts/src/components/grid/_index.scss @@ -0,0 +1,25 @@ +.echGridContainer { + display: grid; + width: 100%; + height: 100%; + align-content: start; + justify-content: stretch; + align-items: stretch; + user-select: text; +} + +.echGridCell { + position: relative; + width: 100%; + height: 100%; + overflow: hidden; + transition: background-color ease-in-out 0.1s; + + &--rightBorder { + border-right: 1px solid #343741; + } + + &--bottomBorder { + border-bottom: 1px solid #343741; + } +} diff --git a/packages/charts/src/components/grid/aligned_grid.tsx b/packages/charts/src/components/grid/aligned_grid.tsx new file mode 100644 index 0000000000..92d62d6bf8 --- /dev/null +++ b/packages/charts/src/components/grid/aligned_grid.tsx @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import classNames from 'classnames'; +import React, { ComponentType, CSSProperties } from 'react'; + +interface AlignedGridProps { + data: Array>; + contentComponent: ComponentType<{ + datum: D; + stats: { rows: number; rowIndex: number; columns: number; columnIndex: number }; + }>; +} + +/** @internal */ +export function AlignedGrid({ data, contentComponent: ContentComponent }: AlignedGridProps) { + const rows = data.length; + const columns = data.reduce((acc, row) => { + return Math.max(acc, row.length); + }, 0); + + const gridStyle: CSSProperties = { + gridTemplateColumns: `repeat(${columns}, 1fr`, + gridTemplateRows: `repeat(${rows}, max-content 1fr)`, + }; + + return ( +
+ {data.map((row, rowIndex) => + row.map((cell, columnIndex) => { + const headerStyle: CSSProperties = { + gridRow: rowIndex * 2 + 1, + gridColumn: columnIndex + 1, + }; + const contentStyle: CSSProperties = { + gridRow: rowIndex * 2 + 2, + gridColumn: columnIndex + 1, + }; + const headerClassName = classNames('echAlignedGrid--header', { + echAlignedGrid__borderRight: columnIndex < columns - 1, + }); + const contentClassName = classNames('echAlignedGrid--content', { + echAlignedGrid__borderRight: columnIndex < columns - 1, + echAlignedGrid__borderBottom: rowIndex < rows - 1, + }); + if (!cell) { + return ( + <> +
+
+ + ); + } + + return ( +
+ +
+ ); + }), + )} +
+ ); +} diff --git a/packages/charts/src/components/icons/assets/down_arrow.tsx b/packages/charts/src/components/icons/assets/down_arrow.tsx new file mode 100644 index 0000000000..8c798aea7b --- /dev/null +++ b/packages/charts/src/components/icons/assets/down_arrow.tsx @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; + +import { IconComponentProps } from '../icon'; + +/** @internal */ +export function DownArrowIcon(extraProps: IconComponentProps) { + return ( + + + + ); +} diff --git a/packages/charts/src/components/icons/icon.tsx b/packages/charts/src/components/icons/icon.tsx index 321f845d62..6647b992a6 100644 --- a/packages/charts/src/components/icons/icon.tsx +++ b/packages/charts/src/components/icons/icon.tsx @@ -11,6 +11,7 @@ import React, { SVGAttributes, memo } from 'react'; import { AlertIcon } from './assets/alert'; import { DotIcon } from './assets/dot'; +import { DownArrowIcon } from './assets/down_arrow'; import { EmptyIcon } from './assets/empty'; import { EyeIcon } from './assets/eye'; import { EyeClosedIcon } from './assets/eye_closed'; @@ -26,6 +27,7 @@ const typeToIconMap = { eyeClosed: EyeClosedIcon, list: ListIcon, questionInCircle: QuestionInCircle, + downArrow: DownArrowIcon, }; /** @internal */ diff --git a/packages/charts/src/index.ts b/packages/charts/src/index.ts index 0579c8b796..34398a0af9 100644 --- a/packages/charts/src/index.ts +++ b/packages/charts/src/index.ts @@ -31,7 +31,7 @@ export { } from './state/types'; export { toEntries } from './utils/common'; export { CurveType } from './utils/curves'; -export { ContinuousDomain, OrdinalDomain } from './utils/domain'; +export { ContinuousDomain, OrdinalDomain, GenericDomain, Range } from './utils/domain'; export { Dimensions, SimplePadding, Padding, PerSideDistance, Margins } from './utils/dimensions'; export { timeFormatter, niceTimeFormatter, niceTimeFormatByDay } from './utils/data/formatters'; export { SeriesCompareFn } from './utils/series_sort'; @@ -73,8 +73,9 @@ export { // theme export * from './utils/themes/theme'; export * from './utils/themes/theme_common'; -export { LIGHT_THEME, LIGHT_BASE_COLORS } from './utils/themes/light_theme'; -export { DARK_THEME, DARK_BASE_COLORS } from './utils/themes/dark_theme'; +export { LIGHT_THEME } from './utils/themes/light_theme'; +export { DARK_THEME } from './utils/themes/dark_theme'; +export { LIGHT_BASE_COLORS, DARK_BASE_COLORS } from './utils/themes/base_colors'; export { LEGACY_LIGHT_THEME } from './utils/themes/legacy_light_theme'; export { LEGACY_DARK_THEME } from './utils/themes/legacy_dark_theme'; @@ -144,3 +145,12 @@ export { TimeFunction } from './utils/time_functions'; export * from './chart_types/flame_chart/flame_api'; export * from './chart_types/timeslip/timeslip_api'; export { LegacyAnimationConfig } from './common/animation'; + +// Bullet +export { + ColorBandValue, + ColorBandConfig, + ColorBandSimpleConfig, + ColorBandComplexConfig, + BulletColorConfig, +} from './chart_types/bullet_graph/utils/color'; diff --git a/packages/charts/src/renderers/canvas/index.ts b/packages/charts/src/renderers/canvas/index.ts index 613ef347eb..ceabbc0f70 100644 --- a/packages/charts/src/renderers/canvas/index.ts +++ b/packages/charts/src/renderers/canvas/index.ts @@ -11,7 +11,7 @@ import { Rect } from '../../geoms/types'; import { ClippedRanges } from '../../utils/geometry'; /** @internal */ -export type CanvasRenderer = (ctx: CanvasRenderingContext2D) => void; +export type CanvasRenderer = (ctx: CanvasRenderingContext2D) => R; /** * withContext abstracts out the otherwise error-prone save/restore pairing; it can be nested and/or put into sequence @@ -22,10 +22,11 @@ export type CanvasRenderer = (ctx: CanvasRenderingContext2D) => void; * @param fun * @internal */ -export function withContext(ctx: CanvasRenderingContext2D, fun: CanvasRenderer) { +export function withContext(ctx: CanvasRenderingContext2D, fun: CanvasRenderer): R { ctx.save(); - fun(ctx); + const r = fun(ctx); ctx.restore(); + return r; } /** @internal */ diff --git a/packages/charts/src/specs/settings.tsx b/packages/charts/src/specs/settings.tsx index ca1dc797eb..4fe9e871fd 100644 --- a/packages/charts/src/specs/settings.tsx +++ b/packages/charts/src/specs/settings.tsx @@ -94,15 +94,15 @@ export interface XYBrushEvent { } /** @public */ -export type XYChartElementEvent = [GeometryValue, XYChartSeriesIdentifier]; +export type XYChartElementEvent = [geometry: GeometryValue, seriesIdentifier: XYChartSeriesIdentifier]; /** @public */ -export type PartitionElementEvent = [Array, SeriesIdentifier]; +export type PartitionElementEvent = [layers: Array, seriesIdentifier: SeriesIdentifier]; /** @public */ export type FlameElementEvent = FlameLayerValue; /** @public */ -export type HeatmapElementEvent = [Cell, SeriesIdentifier]; +export type HeatmapElementEvent = [cell: Cell, seriesIdentifier: SeriesIdentifier]; /** @public */ -export type WordCloudElementEvent = [WordModel, SeriesIdentifier]; +export type WordCloudElementEvent = [model: WordModel, seriesIdentifier: SeriesIdentifier]; /** * Describes a Metric element that is the subject of an interaction. diff --git a/packages/charts/src/state/chart_state.ts b/packages/charts/src/state/chart_state.ts index e6c3e8536b..611569a4af 100644 --- a/packages/charts/src/state/chart_state.ts +++ b/packages/charts/src/state/chart_state.ts @@ -24,6 +24,7 @@ import { LegendItemLabel } from './selectors/get_legend_items_labels'; import { DebugState } from './types'; import { getInitialPointerState, getInitialTooltipState } from './utils'; import { ChartType } from '../chart_types'; +import { BulletGraphState } from '../chart_types/bullet_graph/chart_state'; import { FlameState } from '../chart_types/flame_chart/internal_chart_state'; import { GoalState } from '../chart_types/goal_chart/state/chart_state'; import { HeatmapState } from '../chart_types/heatmap/state/chart_state'; @@ -497,6 +498,7 @@ const constructors: Record InternalChartState | null> = { [ChartType.Heatmap]: () => new HeatmapState(), [ChartType.Wordcloud]: () => new WordcloudState(), [ChartType.Metric]: () => new MetricState(), + [ChartType.BulletGraph]: () => new BulletGraphState(), [ChartType.Global]: () => null, }; // with no default, TS signals if a new chart type isn't added here too diff --git a/packages/charts/src/state/selectors/get_active_pointer_position.ts b/packages/charts/src/state/selectors/get_active_pointer_position.ts index c062aa74e2..3abd48002a 100644 --- a/packages/charts/src/state/selectors/get_active_pointer_position.ts +++ b/packages/charts/src/state/selectors/get_active_pointer_position.ts @@ -6,9 +6,10 @@ * Side Public License, v 1. */ +import { Point } from '../../utils/point'; import { GlobalChartState } from '../chart_state'; /** @internal */ -export const getActivePointerPosition = ({ interactions }: GlobalChartState) => { +export const getActivePointerPosition = ({ interactions }: GlobalChartState): Point => { return interactions.pointer.pinned?.position ?? interactions.pointer.current.position; }; diff --git a/packages/charts/src/state/selectors/get_resolved_background_color.ts b/packages/charts/src/state/selectors/get_resolved_background_color.ts new file mode 100644 index 0000000000..0af33c8012 --- /dev/null +++ b/packages/charts/src/state/selectors/get_resolved_background_color.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { getChartThemeSelector } from './get_chart_theme'; +import { colorToRgba, RGBATupleToString } from '../../common/color_library_wrappers'; +import { Color, Colors } from '../../common/colors'; +import { TRANSPARENT_LIMIT } from '../../common/fill_text_color'; +import { createCustomCachedSelector } from '../create_selector'; + +/** + * @internal + */ +export const getResolvedBackgroundColorSelector = createCustomCachedSelector( + [getChartThemeSelector], + ({ background: { fallbackColor, color = Colors.Transparent.keyword } }): Color => { + let backgroundRGBA = colorToRgba(color); + + if (backgroundRGBA[3] < TRANSPARENT_LIMIT) { + backgroundRGBA = colorToRgba(fallbackColor); + } + + return RGBATupleToString(backgroundRGBA); + }, +); diff --git a/packages/charts/src/utils/bbox/canvas_text_bbox_calculator.ts b/packages/charts/src/utils/bbox/canvas_text_bbox_calculator.ts index ae79846fa1..6f2a6145c9 100644 --- a/packages/charts/src/utils/bbox/canvas_text_bbox_calculator.ts +++ b/packages/charts/src/utils/bbox/canvas_text_bbox_calculator.ts @@ -7,6 +7,7 @@ */ import { cssFontShorthand, Font } from '../../common/text_utils'; +import { withContext } from '../../renderers/canvas'; import { Size } from '../dimensions'; /** @internal */ @@ -21,13 +22,14 @@ export type TextMeasure = (text: string, font: Omit, fontSize /** @internal */ export function measureText(ctx: CanvasRenderingContext2D): TextMeasure { - return (text, font, fontSize, lineHeight = 1) => { - if (text.length === 0) { - // TODO this is a temporary fix to make the multilayer time axis work - return { width: 0, height: fontSize * lineHeight }; - } - ctx.font = cssFontShorthand(font, fontSize); - const { width } = ctx.measureText(text); - return { width, height: fontSize * lineHeight }; - }; + return (text, font, fontSize, lineHeight = 1) => + withContext(ctx, (ctx): Size => { + if (text.length === 0) { + // TODO this is a temporary fix to make the multilayer time axis work + return { width: 0, height: fontSize * lineHeight }; + } + ctx.font = cssFontShorthand(font, fontSize); + const { width } = ctx.measureText(text); + return { width, height: fontSize * lineHeight }; + }); } diff --git a/packages/charts/src/utils/common.tsx b/packages/charts/src/utils/common.tsx index ffc368f3ef..0566646197 100644 --- a/packages/charts/src/utils/common.tsx +++ b/packages/charts/src/utils/common.tsx @@ -504,6 +504,14 @@ export function isUniqueArray(arr: B[], extractor?: (value: B) => T) { })(); } +/** + * Sorts array of numbers + * @internal + */ +export function sortNumbers(arr: T, descending = false): T { + return arr.slice().sort(descending ? (a, b) => b - 1 : (a, b) => a - b) as T; +} + /** * Returns true if _most_ chars in a string are rtl, exluding spaces and numbers * @internal @@ -564,6 +572,20 @@ export const round = (value: number, fractionDigits = 0): number => { return scaledValue / precision; }; +/** + * Returns rounded number to nearest/lowest/highest interval + * + * @internal + */ +export const roundTo = ( + value: number, + interval: number, + options: { min?: number; max?: number; type?: 'round' | 'ceil' | 'floor' } = {}, +): number => { + const roundedValue = Math[options.type ?? 'round'](value / interval) * interval; + return clamp(roundedValue, options?.min ?? -Infinity, options?.max ?? Infinity); +}; + /** * Get number/percentage value from string * @@ -674,6 +696,15 @@ export function stripUndefined>(source: R): R export const isBetween = (min: number, max: number, exclusive = false): ((n: number) => boolean) => exclusive ? (n) => n < max && n > min : (n) => n <= max && n >= min; +/** + * Returns `Array.filter` callback for values between two unordered values + * @internal + */ +export const isWithinRange = (range: [number, number], exclusive = false) => { + const [min, max] = sortNumbers(range); + return isBetween(min, max, exclusive); +}; + /** * Returns `Array.reduce` callback to clamp values and remove duplicates * @internal diff --git a/packages/charts/src/utils/domain.ts b/packages/charts/src/utils/domain.ts index 78e1a5d8de..eea7fe3163 100644 --- a/packages/charts/src/utils/domain.ts +++ b/packages/charts/src/utils/domain.ts @@ -16,6 +16,8 @@ export type OrdinalDomain = (number | string)[]; /** @public */ export type ContinuousDomain = [min: number, max: number]; /** @public */ +export type GenericDomain = [start: number, end: number]; +/** @public */ export type Range = [min: number, max: number]; function constrainPadding( diff --git a/packages/charts/src/utils/themes/base_colors.ts b/packages/charts/src/utils/themes/base_colors.ts new file mode 100644 index 0000000000..b760897514 --- /dev/null +++ b/packages/charts/src/utils/themes/base_colors.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ChartBaseColors } from './theme'; + +/** @public */ +export const LIGHT_BASE_COLORS: ChartBaseColors = { + emptyShade: '#FFF', + lightestShade: '#F1F4FA', + lightShade: '#D3DAE6', + mediumShade: '#98A2B3', + darkShade: '#69707D', + darkestShade: '#343741', + title: '#1A1C21', +}; + +/** @public */ +export const DARK_BASE_COLORS: ChartBaseColors = { + emptyShade: '#1D1E24', + lightestShade: '#25262E', + lightShade: '#343741', + mediumShade: '#535966', + darkShade: '#98A2B3', + darkestShade: '#D4DAE5', + title: '#DFE5EF', +}; diff --git a/packages/charts/src/utils/themes/dark_theme.ts b/packages/charts/src/utils/themes/dark_theme.ts index ef83eb9cf9..4653de47f3 100644 --- a/packages/charts/src/utils/themes/dark_theme.ts +++ b/packages/charts/src/utils/themes/dark_theme.ts @@ -6,25 +6,16 @@ * Side Public License, v 1. */ +import { DARK_BASE_COLORS } from './base_colors'; import { palettes } from './colors'; -import { ChartBaseColors, Theme } from './theme'; +import { Theme } from './theme'; import { DEFAULT_CHART_MARGINS, DEFAULT_CHART_PADDING, DEFAULT_GEOMETRY_STYLES } from './theme_common'; +import { DARK_THEME_BULLET_STYLE } from '../../chart_types/bullet_graph/theme'; import { Colors } from '../../common/colors'; import { TAU } from '../../common/constants'; import { DEFAULT_FONT_FAMILY } from '../../common/default_theme_attributes'; import { ColorVariant } from '../common'; -/** @public */ -export const DARK_BASE_COLORS: ChartBaseColors = { - emptyShade: '#1D1E24', - lightestShade: '#25262E', - lightShade: '#343741', - mediumShade: '#535966', - darkShade: '#98A2B3', - darkestShade: '#D4DAE5', - title: '#DFE5EF', -}; - /** @public */ export const DARK_THEME: Theme = { chartPaddings: DEFAULT_CHART_PADDING, @@ -422,6 +413,7 @@ export const DARK_THEME: Theme = { nonFiniteText: 'N/A', minHeight: 64, }, + bulletGraph: DARK_THEME_BULLET_STYLE, tooltip: { maxWidth: 260, maxTableHeight: 120, diff --git a/packages/charts/src/utils/themes/legacy_dark_theme.ts b/packages/charts/src/utils/themes/legacy_dark_theme.ts index 3648d762d3..a23ad13981 100644 --- a/packages/charts/src/utils/themes/legacy_dark_theme.ts +++ b/packages/charts/src/utils/themes/legacy_dark_theme.ts @@ -14,6 +14,7 @@ import { DEFAULT_GEOMETRY_STYLES, DEFAULT_MISSING_COLOR, } from './theme_common'; +import { DARK_THEME_BULLET_STYLE } from '../../chart_types/bullet_graph/theme'; import { Colors } from '../../common/colors'; import { GOLDEN_RATIO, TAU } from '../../common/constants'; import { ColorVariant } from '../common'; @@ -418,6 +419,7 @@ export const LEGACY_DARK_THEME: Theme = { nonFiniteText: 'N/A', minHeight: 64, }, + bulletGraph: DARK_THEME_BULLET_STYLE, tooltip: { maxWidth: 260, maxTableHeight: 120, diff --git a/packages/charts/src/utils/themes/legacy_light_theme.ts b/packages/charts/src/utils/themes/legacy_light_theme.ts index b4a40e3991..0f7c2263b5 100644 --- a/packages/charts/src/utils/themes/legacy_light_theme.ts +++ b/packages/charts/src/utils/themes/legacy_light_theme.ts @@ -14,6 +14,7 @@ import { DEFAULT_GEOMETRY_STYLES, DEFAULT_MISSING_COLOR, } from './theme_common'; +import { LIGHT_THEME_BULLET_STYLE } from '../../chart_types/bullet_graph/theme'; import { Colors } from '../../common/colors'; import { GOLDEN_RATIO, TAU } from '../../common/constants'; import { ColorVariant } from '../common'; @@ -417,6 +418,7 @@ export const LEGACY_LIGHT_THEME: Theme = { nonFiniteText: 'N/A', minHeight: 64, }, + bulletGraph: LIGHT_THEME_BULLET_STYLE, tooltip: { maxWidth: 260, maxTableHeight: 120, diff --git a/packages/charts/src/utils/themes/light_theme.ts b/packages/charts/src/utils/themes/light_theme.ts index 6bbb716065..d414f757cd 100644 --- a/packages/charts/src/utils/themes/light_theme.ts +++ b/packages/charts/src/utils/themes/light_theme.ts @@ -6,25 +6,16 @@ * Side Public License, v 1. */ +import { LIGHT_BASE_COLORS } from './base_colors'; import { palettes } from './colors'; -import { ChartBaseColors, Theme } from './theme'; +import { Theme } from './theme'; import { DEFAULT_CHART_MARGINS, DEFAULT_CHART_PADDING, DEFAULT_GEOMETRY_STYLES } from './theme_common'; +import { LIGHT_THEME_BULLET_STYLE } from '../../chart_types/bullet_graph/theme'; import { Colors } from '../../common/colors'; import { TAU } from '../../common/constants'; import { DEFAULT_FONT_FAMILY } from '../../common/default_theme_attributes'; import { ColorVariant } from '../common'; -/** @public */ -export const LIGHT_BASE_COLORS: ChartBaseColors = { - emptyShade: '#FFF', - lightestShade: '#F1F4FA', - lightShade: '#D3DAE6', - mediumShade: '#98A2B3', - darkShade: '#69707D', - darkestShade: '#343741', - title: '#1A1C21', -}; - /** @public */ export const LIGHT_THEME: Theme = { chartPaddings: DEFAULT_CHART_PADDING, @@ -421,6 +412,7 @@ export const LIGHT_THEME: Theme = { nonFiniteText: 'N/A', minHeight: 64, }, + bulletGraph: LIGHT_THEME_BULLET_STYLE, tooltip: { maxWidth: 260, maxTableHeight: 120, diff --git a/packages/charts/src/utils/themes/theme.ts b/packages/charts/src/utils/themes/theme.ts index a4138a981c..20374575dc 100644 --- a/packages/charts/src/utils/themes/theme.ts +++ b/packages/charts/src/utils/themes/theme.ts @@ -10,6 +10,7 @@ import { CSSProperties } from 'react'; import { $Values } from 'utility-types'; import { PartitionStyle } from './partition'; +import { BulletGraphStyle } from '../../chart_types/bullet_graph/theme'; import { Color } from '../../common/colors'; import { Pixels, Radian, Ratio } from '../../common/geometry'; import { Font, FontStyle } from '../../common/text_utils'; @@ -500,6 +501,11 @@ export interface Theme { * Theme styles for metric chart types */ metric: MetricStyle; + + /** + * Theme styles for bullet graph types + */ + bulletGraph: BulletGraphStyle; /** * Theme styles for tooltip */ diff --git a/storybook/stories/bullet_graph/1_single.story.tsx b/storybook/stories/bullet_graph/1_single.story.tsx new file mode 100644 index 0000000000..6d487e2150 --- /dev/null +++ b/storybook/stories/bullet_graph/1_single.story.tsx @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { text, number, boolean } from '@storybook/addon-knobs'; +import numeral from 'numeral'; +import React from 'react'; + +import { Chart, BulletGraph, BulletGraphSubtype, Settings } from '@elastic/charts'; + +import { ChartsStory } from '../../types'; +import { useBaseTheme } from '../../use_base_theme'; +import { customKnobs } from '../utils/knobs'; +import { getKnobFromEnum } from '../utils/knobs/utils'; + +export const Example: ChartsStory = (_, { title, description }) => { + const debug = boolean('debug', false); + const bulletTitle = text('title', 'Error rate', 'General'); + const subtitle = text('subtitle', '', 'General'); + const value = number('value', 56, { range: true, min: -200, max: 200 }, 'General'); + const target = number('target', 75, { range: true, min: -200, max: 200 }, 'General'); + const start = number('start', 0, { range: true, min: -200, max: 200 }, 'General'); + const end = number('end', 100, { range: true, min: -200, max: 200 }, 'General'); + const format = text('format (numeraljs)', '0.[0]', 'General'); + const formatter = (d: number) => numeral(d).format(format); + const subtype = getKnobFromEnum('subtype', BulletGraphSubtype, BulletGraphSubtype.horizontal, { group: 'General' }); + + const niceDomain = boolean('niceDomain', false, 'Ticks'); + const tickStrategy = customKnobs.multiSelect( + 'tick strategy', + { + Auto: 'auto', + TickCount: 'count', + TickPlacements: 'placements', + }, + 'auto', + 'select', + 'Ticks', + ); + const ticks = number('ticks(approx. count)', 5, { min: 0, step: 1 }, 'Ticks'); + const tickPlacements = customKnobs.numbersArray( + 'ticks(placements)', + [-200, -100, 0, 5, 10, 15, 20, 25, 50, 100, 200], + undefined, + 'Ticks', + ); + + return ( + + + tickPlacements + : undefined, + valueFormatter: formatter, + tickFormatter: formatter, + }, + ], + ]} + /> + + ); +}; + +Example.parameters = { + resize: { + width: 500, + height: 500, + boxShadow: '5px 5px 15px 5px rgba(0,0,0,0.29)', + borderRadius: '6px', + }, +}; diff --git a/storybook/stories/bullet_graph/2_angular.story.tsx b/storybook/stories/bullet_graph/2_angular.story.tsx new file mode 100644 index 0000000000..0af558bab4 --- /dev/null +++ b/storybook/stories/bullet_graph/2_angular.story.tsx @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { text, number, boolean } from '@storybook/addon-knobs'; +import numeral from 'numeral'; +import React from 'react'; + +import { Chart, BulletGraph, BulletGraphSubtype, Settings } from '@elastic/charts'; + +import { ChartsStory } from '../../types'; +import { useBaseTheme } from '../../use_base_theme'; +import { getKnobFromEnum } from '../utils/knobs/utils'; + +export const Example: ChartsStory = (_, { title, description }) => { + const debug = boolean('debug', false); + const bulletTitle = text('title', 'A Nice Title'); + const subtitle = text('subtitle', 'Subtitle'); + const value = number('value', 56, { range: true, min: -200, max: 200 }); + const target = number('target', 75, { range: true, min: -200, max: 200 }); + const start = number('start', 0, { range: true, min: -200, max: 200 }); + const end = number('end', 100, { range: true, min: -200, max: 200 }); + const tickSnapStep = number('active tick step', 0, { min: 0, max: 10 }); + const angularTickLabelPadding = number('tick label padding', 10, { range: true, min: 0, max: 50 }); + const subtype = getKnobFromEnum('subtype', BulletGraphSubtype, BulletGraphSubtype.twoThirdsCircle, { + exclude: ['vertical', 'horizontal'], + }); + const format = text('format', '0'); + const formatter = (d: number) => numeral(d).format(format); + + return ( + + + + + ); +}; + +Example.parameters = { + resize: { + width: 500, + height: 500, + boxShadow: '5px 5px 15px 5px rgba(0,0,0,0.29)', + borderRadius: '6px', + }, +}; diff --git a/storybook/stories/bullet_graph/3_color_bands.story.tsx b/storybook/stories/bullet_graph/3_color_bands.story.tsx new file mode 100644 index 0000000000..69eb32d705 --- /dev/null +++ b/storybook/stories/bullet_graph/3_color_bands.story.tsx @@ -0,0 +1,196 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { + euiPaletteColorBlind, + euiPaletteComplementary, + euiPaletteCool, + euiPaletteForDarkBackground, + euiPaletteForLightBackground, + euiPaletteForStatus, + euiPaletteForTemperature, + euiPaletteGray, + euiPaletteNegative, + euiPalettePositive, + euiPaletteWarm, +} from '@elastic/eui'; +import { number, boolean, object, color } from '@storybook/addon-knobs'; +import numeral from 'numeral'; +import React, { useCallback } from 'react'; + +import { + Chart, + BulletGraph, + BulletGraphSubtype, + Settings, + BulletColorConfig, + ColorBandSimpleConfig, + ColorBandComplexConfig, +} from '@elastic/charts'; + +import { ChartsStory } from '../../types'; +import { useBaseTheme, useThemeId } from '../../use_base_theme'; +import { customKnobs } from '../utils/knobs'; +import { getKnobFromEnum } from '../utils/knobs/utils'; + +export const Example: ChartsStory = (_, { title, description }) => { + const isDarkTheme = useThemeId().includes('dark'); + const getPallettes = useCallback( + (steps: number) => [ + ['#164863', '#427D9D', '#9BBEC8', '#DDF2FD'], + ['#F1EAFF', '#E5D4FF', '#DCBFFF', '#D0A2F7'], + ['#0802A3', '#FF4B91', '#FF7676', '#FFCD4B'], + euiPaletteColorBlind(), + isDarkTheme ? euiPaletteForDarkBackground() : euiPaletteForLightBackground(), + euiPaletteForTemperature(steps), + euiPaletteForStatus(steps), + euiPaletteComplementary(steps), + euiPaletteNegative(steps), + euiPalettePositive(steps), + euiPaletteCool(steps), + euiPaletteWarm(steps), + euiPaletteGray(steps), + ], + [isDarkTheme], + ); + + // Colors + const colorOptionIndex = getKnobFromEnum( + 'color config', + { + '1 Single Color': 1, + '2 Array of Colors via pallettes': 2, + '3 Array with options': 3, + '4 Fully custom bands': 4, + }, + 1, + { group: 'Color Bands' }, + ); + const colorBands1 = color('Config 1 - Color', 'RGBA(70, 130, 96, 1)', 'Color Bands'); + const colorBands2 = getKnobFromEnum( + 'Config 2 - Palette', + { + Navy: 0, + Pink: 1, + Mixed: 2, + 'eui Palette color blind': 3, + 'eui Palette For Temperature': 4, + 'eui Palette For Status': 5, + 'eui Palette Complementary': 6, + 'eui Palette Negative': 7, + 'eui Palette Positive': 8, + 'eui Palette Cool': 9, + 'eui Palette Warm': 10, + 'eui Palette Gray': 11, + }, + 0, + { group: 'Color Bands' }, + ); + const colorBands2Steps = number('Config 2 - Steps', 5, { min: 1, max: 10, range: true, step: 1 }, 'Color Bands'); + const colorBands2Reverse = boolean('Config 2 - Reverse', false, 'Color Bands'); + + const colorBands3 = object( + 'Config 3 - json', + { + classes: 5, + colors: ['pink', 'yellow', 'blue'], + }, + 'Color Bands', + ); + const colorBands4 = object( + 'Config 4 - json', + [ + { color: 'red', gte: 0, lt: 20 }, + { color: 'green', gte: 20, lte: 40 }, + { + color: 'blue', + gt: 40, + lte: { + type: 'percentage', + value: 100, + }, + }, + ], + 'Color Bands', + ); + const pallette = getPallettes(colorBands2Steps)[colorBands2]; + const colorOptions = [, [colorBands1], colorBands2Reverse ? pallette.reverse() : pallette, colorBands3, colorBands4]; + + // Domain + const start = number('start', 0, { range: true, min: -200, max: 200 }, 'Domain'); + const end = number('end', 100, { range: true, min: -200, max: 200 }, 'Domain'); + const value = number('value', 56, { range: true, min: -200, max: 200 }, 'Domain'); + const target = number('target', 75, { range: true, min: -200, max: 200 }, 'Domain'); + + // Ticks + const niceDomain = boolean('niceDomain', false, 'Ticks'); + const tickStrategy = customKnobs.multiSelect( + 'tick strategy', + { + Auto: 'auto', + TickCount: 'count', + TickPlacements: 'placements', + }, + 'auto', + 'select', + 'Ticks', + ); + const ticks = number('ticks(approx. count)', 5, { min: 0, step: 1 }, 'Ticks'); + const tickPlacements = customKnobs.numbersArray( + 'ticks(placements)', + [-200, -100, 0, 5, 10, 15, 20, 25, 50, 100, 200], + undefined, + 'Ticks', + ); + + // Other + const debug = boolean('debug', false); + const subtype = getKnobFromEnum('subtype', BulletGraphSubtype, BulletGraphSubtype.horizontal); + + const formatter = (d: number) => numeral(d).format('0.[0]'); + + return ( + + + tickPlacements + : undefined, + valueFormatter: formatter, + tickFormatter: formatter, + }, + ], + ]} + /> + + ); +}; + +Example.parameters = { + resize: { + width: 500, + height: 500, + boxShadow: '5px 5px 15px 5px rgba(0,0,0,0.29)', + borderRadius: '6px', + }, +}; diff --git a/storybook/stories/bullet_graph/4_single_row.story.tsx b/storybook/stories/bullet_graph/4_single_row.story.tsx new file mode 100644 index 0000000000..669fcfe61f --- /dev/null +++ b/storybook/stories/bullet_graph/4_single_row.story.tsx @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { boolean, text } from '@storybook/addon-knobs'; +import numeral from 'numeral'; +import React from 'react'; + +import { Chart, BulletGraph, BulletGraphSubtype, Settings } from '@elastic/charts'; + +import { ChartsStory } from '../../types'; +import { useBaseTheme } from '../../use_base_theme'; +import { getKnobFromEnum } from '../utils/knobs/utils'; + +export const Example: ChartsStory = (_, { title, description }) => { + const debug = boolean('debug', false); + const format = text('format', '0'); + const formatter = (d: number) => numeral(d).format(format); + const subtype = getKnobFromEnum('subtype', BulletGraphSubtype, BulletGraphSubtype.vertical); + + return ( + + + + + ); +}; + +Example.parameters = { + resize: { + width: 600, + height: 270, + boxShadow: '5px 5px 15px 5px rgba(0,0,0,0.29)', + borderRadius: '6px', + }, +}; diff --git a/storybook/stories/bullet_graph/5_single_column.story.tsx b/storybook/stories/bullet_graph/5_single_column.story.tsx new file mode 100644 index 0000000000..b6c35c2317 --- /dev/null +++ b/storybook/stories/bullet_graph/5_single_column.story.tsx @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { boolean, text } from '@storybook/addon-knobs'; +import numeral from 'numeral'; +import React from 'react'; + +import { Chart, BulletGraph, BulletGraphSubtype, Settings } from '@elastic/charts'; + +import { ChartsStory } from '../../types'; +import { useBaseTheme } from '../../use_base_theme'; +import { getKnobFromEnum } from '../utils/knobs/utils'; + +export const Example: ChartsStory = (_, { title, description }) => { + const debug = boolean('debug', false); + const format = text('format', '0'); + const formatter = (d: number) => numeral(d).format(format); + const subtype = getKnobFromEnum('subtype', BulletGraphSubtype, BulletGraphSubtype.horizontal); + + return ( + + + + + ); +}; + +Example.parameters = { + resize: { + width: 500, + height: 375, + boxShadow: '5px 5px 15px 5px rgba(0,0,0,0.29)', + borderRadius: '6px', + }, +}; diff --git a/storybook/stories/bullet_graph/6_grid.story.tsx b/storybook/stories/bullet_graph/6_grid.story.tsx new file mode 100644 index 0000000000..131256ff0a --- /dev/null +++ b/storybook/stories/bullet_graph/6_grid.story.tsx @@ -0,0 +1,107 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { boolean, number, text } from '@storybook/addon-knobs'; +import numeral from 'numeral'; +import React from 'react'; + +import { Chart, BulletGraph, BulletGraphSubtype, Settings, Tooltip } from '@elastic/charts'; + +import { ChartsStory } from '../../types'; +import { useBaseTheme } from '../../use_base_theme'; +import { getKnobFromEnum } from '../utils/knobs/utils'; + +export const Example: ChartsStory = (_, { title, description }) => { + const debug = boolean('debug', false); + const hideTooltip = boolean('hide tooltip', false); + const syncCursor = boolean('sync cursor', false); + const tickSnapStep = number('active tick step', 1, { min: 0, max: 10 }); + const valueFormat = text('valueFormat', '0'); + const targetFormat = text('targetFormat', ''); + const tickFormat = text('tickFormat', '0[.]00'); + const subtype = getKnobFromEnum('subtype', BulletGraphSubtype, BulletGraphSubtype.vertical); + + const valueFormatter = (d: number) => numeral(d).format(valueFormat); + const targetFormatter = targetFormat ? (d: number) => numeral(d).format(targetFormat) : undefined; + const tickFormatter = (d: number) => numeral(d).format(tickFormat); + + return ( + + + + + + ); +}; + +Example.parameters = { + markdown: `You can apply different formatter for ticks and values using + different formats for \`tickFormatter\` and \`valueFormatter\`. + +Use a [numeraljs](http://numeraljs.com/) format with the knobs to see the difference`, + resize: { + width: 550, + height: 640, + boxShadow: '5px 5px 15px 5px rgba(0,0,0,0.29)', + borderRadius: '6px', + }, +}; diff --git a/storybook/stories/bullet_graph/bullet_graph.stories.tsx b/storybook/stories/bullet_graph/bullet_graph.stories.tsx new file mode 100644 index 0000000000..eb1fa6c93e --- /dev/null +++ b/storybook/stories/bullet_graph/bullet_graph.stories.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export default { + title: 'Bullet Graph', +}; + +export { Example as single } from './1_single.story'; +export { Example as angular } from './2_angular.story'; +export { Example as colorBands } from './3_color_bands.story'; +export { Example as singleRow } from './4_single_row.story'; +export { Example as singleColumn } from './5_single_column.story'; +export { Example as grid } from './6_grid.story'; diff --git a/storybook/stories/utils/knobs/custom.ts b/storybook/stories/utils/knobs/custom.ts index c4e76688f2..c7d106e0bd 100644 --- a/storybook/stories/utils/knobs/custom.ts +++ b/storybook/stories/utils/knobs/custom.ts @@ -14,16 +14,36 @@ import { } from '@storybook/addon-knobs/dist/components/types'; import { OptionsKnobOptionsDisplay } from '@storybook/addon-knobs/dist/components/types/Options'; +import { isFiniteNumber } from '@elastic/charts/src/utils/common'; + /** * Fix default storybook behavior that does not correctly parse numbers/strings in arrays */ -export function getArrayKnob(name: string, values: (string | number)[]): (string | number)[] { +export function getArrayKnob( + name: string, + values: (string | number)[], + separator?: string, + group?: string, +): (string | number)[] { const stringifiedValues = values.map((d) => `${d}`); - return array(name, stringifiedValues).map((value: string) => + return array(name, stringifiedValues, separator, group).map((value: string) => Number.isFinite(parseFloat(value)) ? parseFloat(value) : value, ); } +/** + * Fix default storybook behavior that does not correctly parse numbers/strings in arrays + */ +export function getNumbersArrayKnob( + name: string, + values: T[], + separator?: string, + group?: string, +): T[] { + const stringifiedValues = values.map((d) => `${d}`); + return array(name, stringifiedValues, separator, group).map(parseFloat).filter(isFiniteNumber) as T[]; +} + export const getPositiveNumberKnob = (name: string, value: number, group?: string) => number(name, value, { min: 0 }, group); diff --git a/storybook/stories/utils/knobs/index.ts b/storybook/stories/utils/knobs/index.ts index 0fbe713fe8..c0360c5c62 100644 --- a/storybook/stories/utils/knobs/index.ts +++ b/storybook/stories/utils/knobs/index.ts @@ -12,6 +12,7 @@ import { getPositiveNumberKnob, getToggledNumberKnob, getMultiSelectKnob, + getNumbersArrayKnob, } from './custom'; import { enumKnobs } from './enums'; import { specialEnumKnobs } from './special_enums'; @@ -24,6 +25,7 @@ export const customKnobs = { }, fromEnum: getKnobFromEnum, array: getArrayKnob, + numbersArray: getNumbersArrayKnob, positiveNumber: getPositiveNumberKnob, toggledNumber: getToggledNumberKnob, numberSelect: getNumberSelectKnob, diff --git a/storybook/style.scss b/storybook/style.scss index f46b3eded2..ea8fb22ca5 100644 --- a/storybook/style.scss +++ b/storybook/style.scss @@ -188,3 +188,10 @@ body { .euiPopover__anchor { width: 100%; } + +.resizable { + resize: both; + overflow: auto; + width: 500px; + height: 600px; +} diff --git a/storybook/use_base_theme.ts b/storybook/use_base_theme.ts index d5176c3124..e6c923c8a5 100644 --- a/storybook/use_base_theme.ts +++ b/storybook/use_base_theme.ts @@ -56,8 +56,12 @@ const getBackground = (backgroundId?: string) => { return option?.background ?? option?.color; }; +export const useThemeId = (): ThemeId => { + return useContext(ThemeContext); +}; + export const useBaseTheme = (): Theme => { - const themeId = useContext(ThemeContext); + const themeId = useThemeId(); const backgroundId = useContext(BackgroundContext); const theme = themeMap[themeId] ?? LIGHT_THEME; const backgroundColor = getBackground(backgroundId); diff --git a/yarn.lock b/yarn.lock index a05347f7d2..38d542087d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6499,10 +6499,10 @@ dependencies: "@types/node" "*" -"@types/chroma-js@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@types/chroma-js/-/chroma-js-2.0.0.tgz#b0fc98c8625d963f14e8138e0a7961103303ab22" - integrity sha512-iomunXsXjDxhm2y1OeJt8NwmgC7RyNkPAOddlYVGsbGoX8+1jYt84SG4/tf6RWcwzROLx1kPXPE95by1s+ebIg== +"@types/chroma-js@^2.4.2": + version "2.4.2" + resolved "https://registry.yarnpkg.com/@types/chroma-js/-/chroma-js-2.4.2.tgz#5c57e9f9ce5343f134e376fb76e07fd3271f150f" + integrity sha512-gbiHvCuBS9aXkE3OEDfS69bscNLTYtbbx2TQf6WyOu+4eCH1AH1gPSiDGF2UzwkRFAbqKNsC5F0mY0xcaEHCbg== "@types/classnames@^2.2.7": version "2.2.9" @@ -9279,13 +9279,6 @@ chownr@^2.0.0: resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== -chroma-js@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/chroma-js/-/chroma-js-2.1.0.tgz#c0be48a21fe797ef8965608c1c4f911ef2da49d5" - integrity sha512-uiRdh4ZZy+UTPSrAdp8hqEdVb1EllLtTHOt5TMaOjJUvi+O54/83Fc5K2ld1P+TJX+dw5B+8/sCgzI6eaur/lg== - dependencies: - cross-env "^6.0.3" - chroma-js@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chroma-js/-/chroma-js-2.4.2.tgz#dffc214ed0c11fa8eefca2c36651d8e57cbfb2b0" @@ -10119,13 +10112,6 @@ create-react-context@0.3.0: gud "^1.0.0" warning "^4.0.3" -cross-env@^6.0.3: - version "6.0.3" - resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-6.0.3.tgz#4256b71e49b3a40637a0ce70768a6ef5c72ae941" - integrity sha512-+KqxF6LCvfhWvADcDPqo64yVIB31gv/jQulX2NGzKS/g3GEVz6/pt4wjHFtFWsHMddebWD/sDthJemzM4MaAag== - dependencies: - cross-spawn "^7.0.0" - cross-env@^7.0.2: version "7.0.2" resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.2.tgz#bd5ed31339a93a3418ac4f3ca9ca3403082ae5f9" @@ -11678,7 +11664,8 @@ eslint-module-utils@^2.8.0: debug "^3.2.7" "eslint-plugin-elastic-charts@link:./packages/eslint-plugin-elastic-charts": - version "1.0.0" + version "0.0.0" + uid "" eslint-plugin-eslint-comments@^3.2.0: version "3.2.0"