diff --git a/e2e/screenshots/all.test.ts-snapshots/baselines/test-cases/log-with-negative-values-chrome-linux.png b/e2e/screenshots/all.test.ts-snapshots/baselines/test-cases/log-with-negative-values-chrome-linux.png new file mode 100644 index 0000000000..8a3f11f067 Binary files /dev/null and b/e2e/screenshots/all.test.ts-snapshots/baselines/test-cases/log-with-negative-values-chrome-linux.png differ diff --git a/e2e/screenshots/bar_stories.test.ts-snapshots/bar-series-stories/should-not-render-min-bar-heights-for-log-scale-values-at-baseline-chrome-linux.png b/e2e/screenshots/bar_stories.test.ts-snapshots/bar-series-stories/should-not-render-min-bar-heights-for-log-scale-values-at-baseline-chrome-linux.png new file mode 100644 index 0000000000..bb695ab997 Binary files /dev/null and b/e2e/screenshots/bar_stories.test.ts-snapshots/bar-series-stories/should-not-render-min-bar-heights-for-log-scale-values-at-baseline-chrome-linux.png differ diff --git a/e2e/screenshots/test_cases_stories.test.ts-snapshots/test-cases-stories/log-scales/should-correctly-render-negative-values-from-baseline-when-banded-chrome-linux.png b/e2e/screenshots/test_cases_stories.test.ts-snapshots/test-cases-stories/log-scales/should-correctly-render-negative-values-from-baseline-when-banded-chrome-linux.png new file mode 100644 index 0000000000..266024b261 Binary files /dev/null and b/e2e/screenshots/test_cases_stories.test.ts-snapshots/test-cases-stories/log-scales/should-correctly-render-negative-values-from-baseline-when-banded-chrome-linux.png differ diff --git a/e2e/screenshots/test_cases_stories.test.ts-snapshots/test-cases-stories/log-scales/should-correctly-render-negative-values-from-baseline-when-stacked-chrome-linux.png b/e2e/screenshots/test_cases_stories.test.ts-snapshots/test-cases-stories/log-scales/should-correctly-render-negative-values-from-baseline-when-stacked-chrome-linux.png new file mode 100644 index 0000000000..f437748b8c Binary files /dev/null and b/e2e/screenshots/test_cases_stories.test.ts-snapshots/test-cases-stories/log-scales/should-correctly-render-negative-values-from-baseline-when-stacked-chrome-linux.png differ diff --git a/e2e/screenshots/test_cases_stories.test.ts-snapshots/test-cases-stories/log-scales/should-correctly-render-tooltip-values-for-banded-bars-chrome-linux.png b/e2e/screenshots/test_cases_stories.test.ts-snapshots/test-cases-stories/log-scales/should-correctly-render-tooltip-values-for-banded-bars-chrome-linux.png new file mode 100644 index 0000000000..7249b85912 Binary files /dev/null and b/e2e/screenshots/test_cases_stories.test.ts-snapshots/test-cases-stories/log-scales/should-correctly-render-tooltip-values-for-banded-bars-chrome-linux.png differ diff --git a/e2e/tests/bar_stories.test.ts b/e2e/tests/bar_stories.test.ts index 291bb66473..20472cfef8 100644 --- a/e2e/tests/bar_stories.test.ts +++ b/e2e/tests/bar_stories.test.ts @@ -256,4 +256,10 @@ test.describe('Bar series stories', () => { ); }); }); + + test('should not render min bar heights for log scale values at baseline', async ({ page }) => { + await common.expectChartAtUrlToMatchScreenshot(page)( + 'http://localhost:9001/?path=/story/bar-chart--min-height&globals=toggles.showHeader:true;toggles.showChartTitle:false;toggles.showChartDescription:false;toggles.showChartBoundary:false;theme:light&knob-Banded=true&knob-Custom No Results message=No Results&knob-Nice y ticks=true&knob-Scale Type=linear&knob-Series Type=bar&knob-Show positive data=true&knob-Split=true&knob-logMinLimit=1&knob-minBarHeight=5&knob-scale=log&knob-scaleType=log', + ); + }); }); diff --git a/e2e/tests/test_cases_stories.test.ts b/e2e/tests/test_cases_stories.test.ts index a8f4df0fe8..4df400c86b 100644 --- a/e2e/tests/test_cases_stories.test.ts +++ b/e2e/tests/test_cases_stories.test.ts @@ -115,4 +115,28 @@ test.describe('Test cases stories', () => { ); }); }); + + test.describe('Log scales', () => { + test('should correctly render negative values from baseline when banded', async ({ page }) => { + await common.expectChartAtUrlToMatchScreenshot(page)( + 'http://localhost:9001/?path=/story/test-cases--log-with-negative-values&globals=toggles.showHeader:true;toggles.showChartTitle:false;toggles.showChartDescription:false;toggles.showChartBoundary:false;theme:light&knob-Show legend=&knob-Scale Type=log&knob-Series Type=bar&knob-logMinLimit=1&knob-Nice y ticks=&knob-Banded=true&knob-Split=&knob-Stacked=false&knob-Show positive data=', + ); + }); + + test('should correctly render tooltip values for banded bars', async ({ page }) => { + await common.expectChartWithMouseAtUrlToMatchScreenshot(page)( + 'http://localhost:9001/?path=/story/test-cases--log-with-negative-values&globals=toggles.showHeader:true;toggles.showChartTitle:false;toggles.showChartDescription:false;toggles.showChartBoundary:false;theme:light&knob-Show legend=&knob-Scale Type=log&knob-Series Type=bar&knob-logMinLimit=1&knob-Nice y ticks=&knob-Banded=true&knob-Split=&knob-Stacked=false&knob-Show positive data=', + { + top: 240, + right: 240, + }, + ); + }); + + test('should correctly render negative values from baseline when stacked', async ({ page }) => { + await common.expectChartAtUrlToMatchScreenshot(page)( + 'http://localhost:9001/?path=/story/test-cases--log-with-negative-values&globals=toggles.showHeader:true;toggles.showChartTitle:false;toggles.showChartDescription:false;toggles.showChartBoundary:false;theme:light&knob-Show legend=&knob-Scale Type=log&knob-Series Type=bar&knob-logMinLimit=1&knob-Nice y ticks=&knob-Banded=&knob-Split=true&knob-Stacked=true&knob-Show positive data=', + ); + }); + }); }); diff --git a/packages/charts/src/chart_types/xy_chart/renderer/dom/highlighter.tsx b/packages/charts/src/chart_types/xy_chart/renderer/dom/highlighter.tsx index afcc41c7d3..a458660338 100644 --- a/packages/charts/src/chart_types/xy_chart/renderer/dom/highlighter.tsx +++ b/packages/charts/src/chart_types/xy_chart/renderer/dom/highlighter.tsx @@ -17,7 +17,7 @@ import { InitStatus, getInternalIsInitializedSelector } from '../../../../state/ import { isBrushingSelector } from '../../../../state/selectors/is_brushing'; import { getColorFromVariant, Rotation } from '../../../../utils/common'; import { Dimensions } from '../../../../utils/dimensions'; -import { isPointGeometry, IndexedGeometry, PointGeometry } from '../../../../utils/geometry'; +import { isPointGeometry, IndexedGeometry, PointGeometry, isBarGeometry } from '../../../../utils/geometry'; import { LIGHT_THEME } from '../../../../utils/themes/light_theme'; import { HighlighterStyle } from '../../../../utils/themes/theme'; import { computeChartDimensionsSelector } from '../../state/selectors/compute_chart_dimensions'; @@ -57,11 +57,20 @@ class HighlighterComponent extends React.Component { static displayName = 'Highlighter'; render() { - const { highlightedGeometries, chartDimensions, chartRotation, chartId, zIndex, isBrushing, style } = this.props; + const { chartDimensions, chartRotation, chartId, zIndex, isBrushing, style } = this.props; if (isBrushing) return null; const clipWidth = [90, -90].includes(chartRotation) ? chartDimensions.height : chartDimensions.width; const clipHeight = [90, -90].includes(chartRotation) ? chartDimensions.width : chartDimensions.height; const clipPathId = `echHighlighterClipPath__${chartId}`; + + const seenBarSeries = new Set(); + const highlightedGeometries = this.props.highlightedGeometries.filter((geom) => { + if (!isBarGeometry(geom)) return true; + const seen = seenBarSeries.has(geom.seriesIdentifier.key); + seenBarSeries.add(geom.seriesIdentifier.key); + return !seen; + }); + return ( @@ -100,11 +109,12 @@ class HighlighterComponent extends React.Component { ); } + return ( [ + { + "color": "#54B399", + "displayValue": undefined, + "height": 50, + "panel": { + "height": 100, + "left": 0, + "top": 0, + "width": 100, + }, + "seriesIdentifier": { + "key": "groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}", + "seriesKeys": [ + 1, + ], + "specId": "spec_1", + "splitAccessors": Map {}, + "xAccessor": 0, + "yAccessor": 1, + }, + "seriesStyle": { + "displayValue": { + "alignment": { + "horizontal": "center", + "vertical": "middle", + }, + "fill": { + "textBorder": 0, + }, + "fontFamily": "Inter, BlinkMacSystemFont, Helvetica, Arial, sans-serif", + "fontSize": 10, + "fontStyle": "normal", + "offsetX": 0, + "offsetY": 0, + "padding": 0, + }, + "rect": { + "opacity": 1, + }, + "rectBorder": { + "strokeWidth": 1, + "visible": false, + }, + }, + "transform": { + "x": 0, + "y": 0, + }, + "value": { + "accessor": "y0", + "datum": [ + 1, + -1000, + 0, + ], + "mark": null, + "x": 1, + "y": 0, + }, + "width": 4.166666666666666, + "x": 0.6944444444444429, + "y": 50, + }, + { + "color": "#54B399", + "displayValue": undefined, + "height": 50, + "panel": { + "height": 100, + "left": 0, + "top": 0, + "width": 100, + }, + "seriesIdentifier": { + "key": "groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}", + "seriesKeys": [ + 1, + ], + "specId": "spec_1", + "splitAccessors": Map {}, + "xAccessor": 0, + "yAccessor": 1, + }, + "seriesStyle": { + "displayValue": { + "alignment": { + "horizontal": "center", + "vertical": "middle", + }, + "fill": { + "textBorder": 0, + }, + "fontFamily": "Inter, BlinkMacSystemFont, Helvetica, Arial, sans-serif", + "fontSize": 10, + "fontStyle": "normal", + "offsetX": 0, + "offsetY": 0, + "padding": 0, + }, + "rect": { + "opacity": 1, + }, + "rectBorder": { + "strokeWidth": 1, + "visible": false, + }, + }, + "transform": { + "x": 0, + "y": 0, + }, + "value": { + "accessor": "y1", + "datum": [ + 1, + -1000, + 0, + ], + "mark": null, + "x": 1, + "y": -1000, + }, + "width": 4.166666666666666, + "x": 0.6944444444444429, + "y": 50, + }, + ], + 2 => [ + { + "color": "#54B399", + "displayValue": undefined, + "height": 5.000000000000007, + "panel": { + "height": 100, + "left": 0, + "top": 0, + "width": 100, + }, + "seriesIdentifier": { + "key": "groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}", + "seriesKeys": [ + 1, + ], + "specId": "spec_1", + "splitAccessors": Map {}, + "xAccessor": 0, + "yAccessor": 1, + }, + "seriesStyle": { + "displayValue": { + "alignment": { + "horizontal": "center", + "vertical": "middle", + }, + "fill": { + "textBorder": 0, + }, + "fontFamily": "Inter, BlinkMacSystemFont, Helvetica, Arial, sans-serif", + "fontSize": 10, + "fontStyle": "normal", + "offsetX": 0, + "offsetY": 0, + "padding": 0, + }, + "rect": { + "opacity": 1, + }, + "rectBorder": { + "strokeWidth": 1, + "visible": false, + }, + }, + "transform": { + "x": 0, + "y": 0, + }, + "value": { + "accessor": "y0", + "datum": [ + 2, + -100, + 0, + ], + "mark": null, + "x": 2, + "y": 0, + }, + "width": 4.166666666666666, + "x": 6.249999999999999, + "y": 50, + }, + { + "color": "#54B399", + "displayValue": undefined, + "height": 5.000000000000007, + "panel": { + "height": 100, + "left": 0, + "top": 0, + "width": 100, + }, + "seriesIdentifier": { + "key": "groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}", + "seriesKeys": [ + 1, + ], + "specId": "spec_1", + "splitAccessors": Map {}, + "xAccessor": 0, + "yAccessor": 1, + }, + "seriesStyle": { + "displayValue": { + "alignment": { + "horizontal": "center", + "vertical": "middle", + }, + "fill": { + "textBorder": 0, + }, + "fontFamily": "Inter, BlinkMacSystemFont, Helvetica, Arial, sans-serif", + "fontSize": 10, + "fontStyle": "normal", + "offsetX": 0, + "offsetY": 0, + "padding": 0, + }, + "rect": { + "opacity": 1, + }, + "rectBorder": { + "strokeWidth": 1, + "visible": false, + }, + }, + "transform": { + "x": 0, + "y": 0, + }, + "value": { + "accessor": "y1", + "datum": [ + 2, + -100, + 0, + ], + "mark": null, + "x": 2, + "y": -100, + }, + "width": 4.166666666666666, + "x": 6.249999999999999, + "y": 50, + }, + ], + 3 => [ + { + "color": "#54B399", + "displayValue": undefined, + "height": 0.5, + "panel": { + "height": 100, + "left": 0, + "top": 0, + "width": 100, + }, + "seriesIdentifier": { + "key": "groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}", + "seriesKeys": [ + 1, + ], + "specId": "spec_1", + "splitAccessors": Map {}, + "xAccessor": 0, + "yAccessor": 1, + }, + "seriesStyle": { + "displayValue": { + "alignment": { + "horizontal": "center", + "vertical": "middle", + }, + "fill": { + "textBorder": 0, + }, + "fontFamily": "Inter, BlinkMacSystemFont, Helvetica, Arial, sans-serif", + "fontSize": 10, + "fontStyle": "normal", + "offsetX": 0, + "offsetY": 0, + "padding": 0, + }, + "rect": { + "opacity": 1, + }, + "rectBorder": { + "strokeWidth": 1, + "visible": false, + }, + }, + "transform": { + "x": 0, + "y": 0, + }, + "value": { + "accessor": "y0", + "datum": [ + 3, + -10, + 0, + ], + "mark": null, + "x": 3, + "y": 0, + }, + "width": 4.166666666666666, + "x": 11.805555555555554, + "y": 50, + }, + { + "color": "#54B399", + "displayValue": undefined, + "height": 0.5, + "panel": { + "height": 100, + "left": 0, + "top": 0, + "width": 100, + }, + "seriesIdentifier": { + "key": "groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}", + "seriesKeys": [ + 1, + ], + "specId": "spec_1", + "splitAccessors": Map {}, + "xAccessor": 0, + "yAccessor": 1, + }, + "seriesStyle": { + "displayValue": { + "alignment": { + "horizontal": "center", + "vertical": "middle", + }, + "fill": { + "textBorder": 0, + }, + "fontFamily": "Inter, BlinkMacSystemFont, Helvetica, Arial, sans-serif", + "fontSize": 10, + "fontStyle": "normal", + "offsetX": 0, + "offsetY": 0, + "padding": 0, + }, + "rect": { + "opacity": 1, + }, + "rectBorder": { + "strokeWidth": 1, + "visible": false, + }, + }, + "transform": { + "x": 0, + "y": 0, + }, + "value": { + "accessor": "y1", + "datum": [ + 3, + -10, + 0, + ], + "mark": null, + "x": 3, + "y": -10, + }, + "width": 4.166666666666666, + "x": 11.805555555555554, + "y": 50, + }, + ], + 4.5 => [ + { + "color": "#54B399", + "displayValue": undefined, + "height": 1, + "panel": { + "height": 100, + "left": 0, + "top": 0, + "width": 100, + }, + "seriesIdentifier": { + "key": "groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}", + "seriesKeys": [ + 1, + ], + "specId": "spec_1", + "splitAccessors": Map {}, + "xAccessor": 0, + "yAccessor": 1, + }, + "seriesStyle": { + "displayValue": { + "alignment": { + "horizontal": "center", + "vertical": "middle", + }, + "fill": { + "textBorder": 0, + }, + "fontFamily": "Inter, BlinkMacSystemFont, Helvetica, Arial, sans-serif", + "fontSize": 10, + "fontStyle": "normal", + "offsetX": 0, + "offsetY": 0, + "padding": 0, + }, + "rect": { + "opacity": 1, + }, + "rectBorder": { + "strokeWidth": 1, + "visible": false, + }, + }, + "transform": { + "x": 0, + "y": 0, + }, + "value": { + "accessor": "y0", + "datum": [ + 4.5, + -10, + 10, + ], + "mark": null, + "x": 4.5, + "y": 10, + }, + "width": 4.166666666666666, + "x": 17.361111111111107, + "y": 49.5, + }, + { + "color": "#54B399", + "displayValue": undefined, + "height": 1, + "panel": { + "height": 100, + "left": 0, + "top": 0, + "width": 100, + }, + "seriesIdentifier": { + "key": "groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}", + "seriesKeys": [ + 1, + ], + "specId": "spec_1", + "splitAccessors": Map {}, + "xAccessor": 0, + "yAccessor": 1, + }, + "seriesStyle": { + "displayValue": { + "alignment": { + "horizontal": "center", + "vertical": "middle", + }, + "fill": { + "textBorder": 0, + }, + "fontFamily": "Inter, BlinkMacSystemFont, Helvetica, Arial, sans-serif", + "fontSize": 10, + "fontStyle": "normal", + "offsetX": 0, + "offsetY": 0, + "padding": 0, + }, + "rect": { + "opacity": 1, + }, + "rectBorder": { + "strokeWidth": 1, + "visible": false, + }, + }, + "transform": { + "x": 0, + "y": 0, + }, + "value": { + "accessor": "y1", + "datum": [ + 4.5, + -10, + 10, + ], + "mark": null, + "x": 4.5, + "y": -10, + }, + "width": 4.166666666666666, + "x": 17.361111111111107, + "y": 49.5, + }, + ], + 5 => [ + { + "color": "#54B399", + "displayValue": undefined, + "height": 0.04999999999999716, + "panel": { + "height": 100, + "left": 0, + "top": 0, + "width": 100, + }, + "seriesIdentifier": { + "key": "groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}", + "seriesKeys": [ + 1, + ], + "specId": "spec_1", + "splitAccessors": Map {}, + "xAccessor": 0, + "yAccessor": 1, + }, + "seriesStyle": { + "displayValue": { + "alignment": { + "horizontal": "center", + "vertical": "middle", + }, + "fill": { + "textBorder": 0, + }, + "fontFamily": "Inter, BlinkMacSystemFont, Helvetica, Arial, sans-serif", + "fontSize": 10, + "fontStyle": "normal", + "offsetX": 0, + "offsetY": 0, + "padding": 0, + }, + "rect": { + "opacity": 1, + }, + "rectBorder": { + "strokeWidth": 1, + "visible": false, + }, + }, + "transform": { + "x": 0, + "y": 0, + }, + "value": { + "accessor": "y0", + "datum": [ + 5, + -1, + 0, + ], + "mark": null, + "x": 5, + "y": 0, + }, + "width": 4.166666666666666, + "x": 22.916666666666664, + "y": 50, + }, + { + "color": "#54B399", + "displayValue": undefined, + "height": 0.04999999999999716, + "panel": { + "height": 100, + "left": 0, + "top": 0, + "width": 100, + }, + "seriesIdentifier": { + "key": "groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}", + "seriesKeys": [ + 1, + ], + "specId": "spec_1", + "splitAccessors": Map {}, + "xAccessor": 0, + "yAccessor": 1, + }, + "seriesStyle": { + "displayValue": { + "alignment": { + "horizontal": "center", + "vertical": "middle", + }, + "fill": { + "textBorder": 0, + }, + "fontFamily": "Inter, BlinkMacSystemFont, Helvetica, Arial, sans-serif", + "fontSize": 10, + "fontStyle": "normal", + "offsetX": 0, + "offsetY": 0, + "padding": 0, + }, + "rect": { + "opacity": 1, + }, + "rectBorder": { + "strokeWidth": 1, + "visible": false, + }, + }, + "transform": { + "x": 0, + "y": 0, + }, + "value": { + "accessor": "y1", + "datum": [ + 5, + -1, + 0, + ], + "mark": null, + "x": 5, + "y": -1, + }, + "width": 4.166666666666666, + "x": 22.916666666666664, + "y": 50, + }, + ], + 6 => [ + { + "color": "#54B399", + "displayValue": undefined, + "height": 0, + "panel": { + "height": 100, + "left": 0, + "top": 0, + "width": 100, + }, + "seriesIdentifier": { + "key": "groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}", + "seriesKeys": [ + 1, + ], + "specId": "spec_1", + "splitAccessors": Map {}, + "xAccessor": 0, + "yAccessor": 1, + }, + "seriesStyle": { + "displayValue": { + "alignment": { + "horizontal": "center", + "vertical": "middle", + }, + "fill": { + "textBorder": 0, + }, + "fontFamily": "Inter, BlinkMacSystemFont, Helvetica, Arial, sans-serif", + "fontSize": 10, + "fontStyle": "normal", + "offsetX": 0, + "offsetY": 0, + "padding": 0, + }, + "rect": { + "opacity": 1, + }, + "rectBorder": { + "strokeWidth": 1, + "visible": false, + }, + }, + "transform": { + "x": 0, + "y": 0, + }, + "value": { + "accessor": "y0", + "datum": [ + 6, + 0, + 0, + ], + "mark": null, + "x": 6, + "y": 0, + }, + "width": 4.166666666666666, + "x": 28.47222222222222, + "y": 50, + }, + { + "color": "#54B399", + "displayValue": undefined, + "height": 0, + "panel": { + "height": 100, + "left": 0, + "top": 0, + "width": 100, + }, + "seriesIdentifier": { + "key": "groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}", + "seriesKeys": [ + 1, + ], + "specId": "spec_1", + "splitAccessors": Map {}, + "xAccessor": 0, + "yAccessor": 1, + }, + "seriesStyle": { + "displayValue": { + "alignment": { + "horizontal": "center", + "vertical": "middle", + }, + "fill": { + "textBorder": 0, + }, + "fontFamily": "Inter, BlinkMacSystemFont, Helvetica, Arial, sans-serif", + "fontSize": 10, + "fontStyle": "normal", + "offsetX": 0, + "offsetY": 0, + "padding": 0, + }, + "rect": { + "opacity": 1, + }, + "rectBorder": { + "strokeWidth": 1, + "visible": false, + }, + }, + "transform": { + "x": 0, + "y": 0, + }, + "value": { + "accessor": "y1", + "datum": [ + 6, + 0, + 0, + ], + "mark": null, + "x": 6, + "y": 0, + }, + "width": 4.166666666666666, + "x": 28.47222222222222, + "y": 50, + }, + ], + 7 => [ + { + "color": "#54B399", + "displayValue": undefined, + "height": 0.04999999999999716, + "panel": { + "height": 100, + "left": 0, + "top": 0, + "width": 100, + }, + "seriesIdentifier": { + "key": "groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}", + "seriesKeys": [ + 1, + ], + "specId": "spec_1", + "splitAccessors": Map {}, + "xAccessor": 0, + "yAccessor": 1, + }, + "seriesStyle": { + "displayValue": { + "alignment": { + "horizontal": "center", + "vertical": "middle", + }, + "fill": { + "textBorder": 0, + }, + "fontFamily": "Inter, BlinkMacSystemFont, Helvetica, Arial, sans-serif", + "fontSize": 10, + "fontStyle": "normal", + "offsetX": 0, + "offsetY": 0, + "padding": 0, + }, + "rect": { + "opacity": 1, + }, + "rectBorder": { + "strokeWidth": 1, + "visible": false, + }, + }, + "transform": { + "x": 0, + "y": 0, + }, + "value": { + "accessor": "y0", + "datum": [ + 7, + -1, + 0, + ], + "mark": null, + "x": 7, + "y": 0, + }, + "width": 4.166666666666666, + "x": 34.02777777777777, + "y": 50, + }, + { + "color": "#54B399", + "displayValue": undefined, + "height": 0.04999999999999716, + "panel": { + "height": 100, + "left": 0, + "top": 0, + "width": 100, + }, + "seriesIdentifier": { + "key": "groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}", + "seriesKeys": [ + 1, + ], + "specId": "spec_1", + "splitAccessors": Map {}, + "xAccessor": 0, + "yAccessor": 1, + }, + "seriesStyle": { + "displayValue": { + "alignment": { + "horizontal": "center", + "vertical": "middle", + }, + "fill": { + "textBorder": 0, + }, + "fontFamily": "Inter, BlinkMacSystemFont, Helvetica, Arial, sans-serif", + "fontSize": 10, + "fontStyle": "normal", + "offsetX": 0, + "offsetY": 0, + "padding": 0, + }, + "rect": { + "opacity": 1, + }, + "rectBorder": { + "strokeWidth": 1, + "visible": false, + }, + }, + "transform": { + "x": 0, + "y": 0, + }, + "value": { + "accessor": "y1", + "datum": [ + 7, + -1, + 0, + ], + "mark": null, + "x": 7, + "y": -1, + }, + "width": 4.166666666666666, + "x": 34.02777777777777, + "y": 50, + }, + ], + 8 => [ + { + "color": "#54B399", + "displayValue": undefined, + "height": 0, + "panel": { + "height": 100, + "left": 0, + "top": 0, + "width": 100, + }, + "seriesIdentifier": { + "key": "groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}", + "seriesKeys": [ + 1, + ], + "specId": "spec_1", + "splitAccessors": Map {}, + "xAccessor": 0, + "yAccessor": 1, + }, + "seriesStyle": { + "displayValue": { + "alignment": { + "horizontal": "center", + "vertical": "middle", + }, + "fill": { + "textBorder": 0, + }, + "fontFamily": "Inter, BlinkMacSystemFont, Helvetica, Arial, sans-serif", + "fontSize": 10, + "fontStyle": "normal", + "offsetX": 0, + "offsetY": 0, + "padding": 0, + }, + "rect": { + "opacity": 1, + }, + "rectBorder": { + "strokeWidth": 1, + "visible": false, + }, + }, + "transform": { + "x": 0, + "y": 0, + }, + "value": { + "accessor": "y0", + "datum": [ + 8, + 0, + 0, + ], + "mark": null, + "x": 8, + "y": 0, + }, + "width": 4.166666666666666, + "x": 39.58333333333333, + "y": 50, + }, + { + "color": "#54B399", + "displayValue": undefined, + "height": 0, + "panel": { + "height": 100, + "left": 0, + "top": 0, + "width": 100, + }, + "seriesIdentifier": { + "key": "groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}", + "seriesKeys": [ + 1, + ], + "specId": "spec_1", + "splitAccessors": Map {}, + "xAccessor": 0, + "yAccessor": 1, + }, + "seriesStyle": { + "displayValue": { + "alignment": { + "horizontal": "center", + "vertical": "middle", + }, + "fill": { + "textBorder": 0, + }, + "fontFamily": "Inter, BlinkMacSystemFont, Helvetica, Arial, sans-serif", + "fontSize": 10, + "fontStyle": "normal", + "offsetX": 0, + "offsetY": 0, + "padding": 0, + }, + "rect": { + "opacity": 1, + }, + "rectBorder": { + "strokeWidth": 1, + "visible": false, + }, + }, + "transform": { + "x": 0, + "y": 0, + }, + "value": { + "accessor": "y1", + "datum": [ + 8, + 0, + 0, + ], + "mark": null, + "x": 8, + "y": 0, + }, + "width": 4.166666666666666, + "x": 39.58333333333333, + "y": 50, + }, + ], + 9 => [ + { + "color": "#54B399", + "displayValue": undefined, + "height": 0, + "panel": { + "height": 100, + "left": 0, + "top": 0, + "width": 100, + }, + "seriesIdentifier": { + "key": "groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}", + "seriesKeys": [ + 1, + ], + "specId": "spec_1", + "splitAccessors": Map {}, + "xAccessor": 0, + "yAccessor": 1, + }, + "seriesStyle": { + "displayValue": { + "alignment": { + "horizontal": "center", + "vertical": "middle", + }, + "fill": { + "textBorder": 0, + }, + "fontFamily": "Inter, BlinkMacSystemFont, Helvetica, Arial, sans-serif", + "fontSize": 10, + "fontStyle": "normal", + "offsetX": 0, + "offsetY": 0, + "padding": 0, + }, + "rect": { + "opacity": 1, + }, + "rectBorder": { + "strokeWidth": 1, + "visible": false, + }, + }, + "transform": { + "x": 0, + "y": 0, + }, + "value": { + "accessor": "y0", + "datum": [ + 9, + 0, + 0, + ], + "mark": null, + "x": 9, + "y": 0, + }, + "width": 4.166666666666666, + "x": 45.138888888888886, + "y": 50, + }, + { + "color": "#54B399", + "displayValue": undefined, + "height": 0, + "panel": { + "height": 100, + "left": 0, + "top": 0, + "width": 100, + }, + "seriesIdentifier": { + "key": "groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}", + "seriesKeys": [ + 1, + ], + "specId": "spec_1", + "splitAccessors": Map {}, + "xAccessor": 0, + "yAccessor": 1, + }, + "seriesStyle": { + "displayValue": { + "alignment": { + "horizontal": "center", + "vertical": "middle", + }, + "fill": { + "textBorder": 0, + }, + "fontFamily": "Inter, BlinkMacSystemFont, Helvetica, Arial, sans-serif", + "fontSize": 10, + "fontStyle": "normal", + "offsetX": 0, + "offsetY": 0, + "padding": 0, + }, + "rect": { + "opacity": 1, + }, + "rectBorder": { + "strokeWidth": 1, + "visible": false, + }, + }, + "transform": { + "x": 0, + "y": 0, + }, + "value": { + "accessor": "y1", + "datum": [ + 9, + 0, + 0, + ], + "mark": null, + "x": 9, + "y": 0, + }, + "width": 4.166666666666666, + "x": 45.138888888888886, + "y": 50, + }, + ], + 10 => [ + { + "color": "#54B399", + "displayValue": undefined, + "height": 0.2500000000000071, + "panel": { + "height": 100, + "left": 0, + "top": 0, + "width": 100, + }, + "seriesIdentifier": { + "key": "groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}", + "seriesKeys": [ + 1, + ], + "specId": "spec_1", + "splitAccessors": Map {}, + "xAccessor": 0, + "yAccessor": 1, + }, + "seriesStyle": { + "displayValue": { + "alignment": { + "horizontal": "center", + "vertical": "middle", + }, + "fill": { + "textBorder": 0, + }, + "fontFamily": "Inter, BlinkMacSystemFont, Helvetica, Arial, sans-serif", + "fontSize": 10, + "fontStyle": "normal", + "offsetX": 0, + "offsetY": 0, + "padding": 0, + }, + "rect": { + "opacity": 1, + }, + "rectBorder": { + "strokeWidth": 1, + "visible": false, + }, + }, + "transform": { + "x": 0, + "y": 0, + }, + "value": { + "accessor": "y0", + "datum": [ + 10, + 5, + 10, + ], + "mark": null, + "x": 10, + "y": 10, + }, + "width": 4.166666666666666, + "x": 50.69444444444444, + "y": 49.5, + }, + { + "color": "#54B399", + "displayValue": undefined, + "height": 0.2500000000000071, + "panel": { + "height": 100, + "left": 0, + "top": 0, + "width": 100, + }, + "seriesIdentifier": { + "key": "groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}", + "seriesKeys": [ + 1, + ], + "specId": "spec_1", + "splitAccessors": Map {}, + "xAccessor": 0, + "yAccessor": 1, + }, + "seriesStyle": { + "displayValue": { + "alignment": { + "horizontal": "center", + "vertical": "middle", + }, + "fill": { + "textBorder": 0, + }, + "fontFamily": "Inter, BlinkMacSystemFont, Helvetica, Arial, sans-serif", + "fontSize": 10, + "fontStyle": "normal", + "offsetX": 0, + "offsetY": 0, + "padding": 0, + }, + "rect": { + "opacity": 1, + }, + "rectBorder": { + "strokeWidth": 1, + "visible": false, + }, + }, + "transform": { + "x": 0, + "y": 0, + }, + "value": { + "accessor": "y1", + "datum": [ + 10, + 5, + 10, + ], + "mark": null, + "x": 10, + "y": 5, + }, + "width": 4.166666666666666, + "x": 50.69444444444444, + "y": 49.5, + }, + ], + 11 => [ + { + "color": "#54B399", + "displayValue": undefined, + "height": 0.5499999999999972, + "panel": { + "height": 100, + "left": 0, + "top": 0, + "width": 100, + }, + "seriesIdentifier": { + "key": "groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}", + "seriesKeys": [ + 1, + ], + "specId": "spec_1", + "splitAccessors": Map {}, + "xAccessor": 0, + "yAccessor": 1, + }, + "seriesStyle": { + "displayValue": { + "alignment": { + "horizontal": "center", + "vertical": "middle", + }, + "fill": { + "textBorder": 0, + }, + "fontFamily": "Inter, BlinkMacSystemFont, Helvetica, Arial, sans-serif", + "fontSize": 10, + "fontStyle": "normal", + "offsetX": 0, + "offsetY": 0, + "padding": 0, + }, + "rect": { + "opacity": 1, + }, + "rectBorder": { + "strokeWidth": 1, + "visible": false, + }, + }, + "transform": { + "x": 0, + "y": 0, + }, + "value": { + "accessor": "y0", + "datum": [ + 11, + 1, + -10, + ], + "mark": null, + "x": 11, + "y": -10, + }, + "width": 4.166666666666666, + "x": 56.25, + "y": 49.95, + }, + { + "color": "#54B399", + "displayValue": undefined, + "height": 0.5499999999999972, + "panel": { + "height": 100, + "left": 0, + "top": 0, + "width": 100, + }, + "seriesIdentifier": { + "key": "groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}", + "seriesKeys": [ + 1, + ], + "specId": "spec_1", + "splitAccessors": Map {}, + "xAccessor": 0, + "yAccessor": 1, + }, + "seriesStyle": { + "displayValue": { + "alignment": { + "horizontal": "center", + "vertical": "middle", + }, + "fill": { + "textBorder": 0, + }, + "fontFamily": "Inter, BlinkMacSystemFont, Helvetica, Arial, sans-serif", + "fontSize": 10, + "fontStyle": "normal", + "offsetX": 0, + "offsetY": 0, + "padding": 0, + }, + "rect": { + "opacity": 1, + }, + "rectBorder": { + "strokeWidth": 1, + "visible": false, + }, + }, + "transform": { + "x": 0, + "y": 0, + }, + "value": { + "accessor": "y1", + "datum": [ + 11, + 1, + -10, + ], + "mark": null, + "x": 11, + "y": 1, + }, + "width": 4.166666666666666, + "x": 56.25, + "y": 49.95, + }, + ], + 12 => [ + { + "color": "#54B399", + "displayValue": undefined, + "height": 0.04999999999999716, + "panel": { + "height": 100, + "left": 0, + "top": 0, + "width": 100, + }, + "seriesIdentifier": { + "key": "groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}", + "seriesKeys": [ + 1, + ], + "specId": "spec_1", + "splitAccessors": Map {}, + "xAccessor": 0, + "yAccessor": 1, + }, + "seriesStyle": { + "displayValue": { + "alignment": { + "horizontal": "center", + "vertical": "middle", + }, + "fill": { + "textBorder": 0, + }, + "fontFamily": "Inter, BlinkMacSystemFont, Helvetica, Arial, sans-serif", + "fontSize": 10, + "fontStyle": "normal", + "offsetX": 0, + "offsetY": 0, + "padding": 0, + }, + "rect": { + "opacity": 1, + }, + "rectBorder": { + "strokeWidth": 1, + "visible": false, + }, + }, + "transform": { + "x": 0, + "y": 0, + }, + "value": { + "accessor": "y0", + "datum": [ + 12, + 1, + 0, + ], + "mark": null, + "x": 12, + "y": 0, + }, + "width": 4.166666666666666, + "x": 61.80555555555555, + "y": 49.95, + }, + { + "color": "#54B399", + "displayValue": undefined, + "height": 0.04999999999999716, + "panel": { + "height": 100, + "left": 0, + "top": 0, + "width": 100, + }, + "seriesIdentifier": { + "key": "groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}", + "seriesKeys": [ + 1, + ], + "specId": "spec_1", + "splitAccessors": Map {}, + "xAccessor": 0, + "yAccessor": 1, + }, + "seriesStyle": { + "displayValue": { + "alignment": { + "horizontal": "center", + "vertical": "middle", + }, + "fill": { + "textBorder": 0, + }, + "fontFamily": "Inter, BlinkMacSystemFont, Helvetica, Arial, sans-serif", + "fontSize": 10, + "fontStyle": "normal", + "offsetX": 0, + "offsetY": 0, + "padding": 0, + }, + "rect": { + "opacity": 1, + }, + "rectBorder": { + "strokeWidth": 1, + "visible": false, + }, + }, + "transform": { + "x": 0, + "y": 0, + }, + "value": { + "accessor": "y1", + "datum": [ + 12, + 1, + 0, + ], + "mark": null, + "x": 12, + "y": 1, + }, + "width": 4.166666666666666, + "x": 61.80555555555555, + "y": 49.95, + }, + ], + 13 => [ + { + "color": "#54B399", + "displayValue": undefined, + "height": 0, + "panel": { + "height": 100, + "left": 0, + "top": 0, + "width": 100, + }, + "seriesIdentifier": { + "key": "groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}", + "seriesKeys": [ + 1, + ], + "specId": "spec_1", + "splitAccessors": Map {}, + "xAccessor": 0, + "yAccessor": 1, + }, + "seriesStyle": { + "displayValue": { + "alignment": { + "horizontal": "center", + "vertical": "middle", + }, + "fill": { + "textBorder": 0, + }, + "fontFamily": "Inter, BlinkMacSystemFont, Helvetica, Arial, sans-serif", + "fontSize": 10, + "fontStyle": "normal", + "offsetX": 0, + "offsetY": 0, + "padding": 0, + }, + "rect": { + "opacity": 1, + }, + "rectBorder": { + "strokeWidth": 1, + "visible": false, + }, + }, + "transform": { + "x": 0, + "y": 0, + }, + "value": { + "accessor": "y0", + "datum": [ + 13, + 0, + 0, + ], + "mark": null, + "x": 13, + "y": 0, + }, + "width": 4.166666666666666, + "x": 67.3611111111111, + "y": 50, + }, + { + "color": "#54B399", + "displayValue": undefined, + "height": 0, + "panel": { + "height": 100, + "left": 0, + "top": 0, + "width": 100, + }, + "seriesIdentifier": { + "key": "groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}", + "seriesKeys": [ + 1, + ], + "specId": "spec_1", + "splitAccessors": Map {}, + "xAccessor": 0, + "yAccessor": 1, + }, + "seriesStyle": { + "displayValue": { + "alignment": { + "horizontal": "center", + "vertical": "middle", + }, + "fill": { + "textBorder": 0, + }, + "fontFamily": "Inter, BlinkMacSystemFont, Helvetica, Arial, sans-serif", + "fontSize": 10, + "fontStyle": "normal", + "offsetX": 0, + "offsetY": 0, + "padding": 0, + }, + "rect": { + "opacity": 1, + }, + "rectBorder": { + "strokeWidth": 1, + "visible": false, + }, + }, + "transform": { + "x": 0, + "y": 0, + }, + "value": { + "accessor": "y1", + "datum": [ + 13, + 0, + 0, + ], + "mark": null, + "x": 13, + "y": 0, + }, + "width": 4.166666666666666, + "x": 67.3611111111111, + "y": 50, + }, + ], + 14 => [ + { + "color": "#54B399", + "displayValue": undefined, + "height": 0.04999999999999716, + "panel": { + "height": 100, + "left": 0, + "top": 0, + "width": 100, + }, + "seriesIdentifier": { + "key": "groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}", + "seriesKeys": [ + 1, + ], + "specId": "spec_1", + "splitAccessors": Map {}, + "xAccessor": 0, + "yAccessor": 1, + }, + "seriesStyle": { + "displayValue": { + "alignment": { + "horizontal": "center", + "vertical": "middle", + }, + "fill": { + "textBorder": 0, + }, + "fontFamily": "Inter, BlinkMacSystemFont, Helvetica, Arial, sans-serif", + "fontSize": 10, + "fontStyle": "normal", + "offsetX": 0, + "offsetY": 0, + "padding": 0, + }, + "rect": { + "opacity": 1, + }, + "rectBorder": { + "strokeWidth": 1, + "visible": false, + }, + }, + "transform": { + "x": 0, + "y": 0, + }, + "value": { + "accessor": "y0", + "datum": [ + 14, + 1, + 0, + ], + "mark": null, + "x": 14, + "y": 0, + }, + "width": 4.166666666666666, + "x": 72.91666666666666, + "y": 49.95, + }, + { + "color": "#54B399", + "displayValue": undefined, + "height": 0.04999999999999716, + "panel": { + "height": 100, + "left": 0, + "top": 0, + "width": 100, + }, + "seriesIdentifier": { + "key": "groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}", + "seriesKeys": [ + 1, + ], + "specId": "spec_1", + "splitAccessors": Map {}, + "xAccessor": 0, + "yAccessor": 1, + }, + "seriesStyle": { + "displayValue": { + "alignment": { + "horizontal": "center", + "vertical": "middle", + }, + "fill": { + "textBorder": 0, + }, + "fontFamily": "Inter, BlinkMacSystemFont, Helvetica, Arial, sans-serif", + "fontSize": 10, + "fontStyle": "normal", + "offsetX": 0, + "offsetY": 0, + "padding": 0, + }, + "rect": { + "opacity": 1, + }, + "rectBorder": { + "strokeWidth": 1, + "visible": false, + }, + }, + "transform": { + "x": 0, + "y": 0, + }, + "value": { + "accessor": "y1", + "datum": [ + 14, + 1, + 0, + ], + "mark": null, + "x": 14, + "y": 1, + }, + "width": 4.166666666666666, + "x": 72.91666666666666, + "y": 49.95, + }, + ], + 15 => [ + { + "color": "#54B399", + "displayValue": undefined, + "height": 1, + "panel": { + "height": 100, + "left": 0, + "top": 0, + "width": 100, + }, + "seriesIdentifier": { + "key": "groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}", + "seriesKeys": [ + 1, + ], + "specId": "spec_1", + "splitAccessors": Map {}, + "xAccessor": 0, + "yAccessor": 1, + }, + "seriesStyle": { + "displayValue": { + "alignment": { + "horizontal": "center", + "vertical": "middle", + }, + "fill": { + "textBorder": 0, + }, + "fontFamily": "Inter, BlinkMacSystemFont, Helvetica, Arial, sans-serif", + "fontSize": 10, + "fontStyle": "normal", + "offsetX": 0, + "offsetY": 0, + "padding": 0, + }, + "rect": { + "opacity": 1, + }, + "rectBorder": { + "strokeWidth": 1, + "visible": false, + }, + }, + "transform": { + "x": 0, + "y": 0, + }, + "value": { + "accessor": "y0", + "datum": [ + 15, + 10, + -10, + ], + "mark": null, + "x": 15, + "y": -10, + }, + "width": 4.166666666666666, + "x": 78.47222222222221, + "y": 49.5, + }, + { + "color": "#54B399", + "displayValue": undefined, + "height": 1, + "panel": { + "height": 100, + "left": 0, + "top": 0, + "width": 100, + }, + "seriesIdentifier": { + "key": "groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}", + "seriesKeys": [ + 1, + ], + "specId": "spec_1", + "splitAccessors": Map {}, + "xAccessor": 0, + "yAccessor": 1, + }, + "seriesStyle": { + "displayValue": { + "alignment": { + "horizontal": "center", + "vertical": "middle", + }, + "fill": { + "textBorder": 0, + }, + "fontFamily": "Inter, BlinkMacSystemFont, Helvetica, Arial, sans-serif", + "fontSize": 10, + "fontStyle": "normal", + "offsetX": 0, + "offsetY": 0, + "padding": 0, + }, + "rect": { + "opacity": 1, + }, + "rectBorder": { + "strokeWidth": 1, + "visible": false, + }, + }, + "transform": { + "x": 0, + "y": 0, + }, + "value": { + "accessor": "y1", + "datum": [ + 15, + 10, + -10, + ], + "mark": null, + "x": 15, + "y": 10, + }, + "width": 4.166666666666666, + "x": 78.47222222222221, + "y": 49.5, + }, + ], + 16 => [ + { + "color": "#54B399", + "displayValue": undefined, + "height": 0.5, + "panel": { + "height": 100, + "left": 0, + "top": 0, + "width": 100, + }, + "seriesIdentifier": { + "key": "groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}", + "seriesKeys": [ + 1, + ], + "specId": "spec_1", + "splitAccessors": Map {}, + "xAccessor": 0, + "yAccessor": 1, + }, + "seriesStyle": { + "displayValue": { + "alignment": { + "horizontal": "center", + "vertical": "middle", + }, + "fill": { + "textBorder": 0, + }, + "fontFamily": "Inter, BlinkMacSystemFont, Helvetica, Arial, sans-serif", + "fontSize": 10, + "fontStyle": "normal", + "offsetX": 0, + "offsetY": 0, + "padding": 0, + }, + "rect": { + "opacity": 1, + }, + "rectBorder": { + "strokeWidth": 1, + "visible": false, + }, + }, + "transform": { + "x": 0, + "y": 0, + }, + "value": { + "accessor": "y0", + "datum": [ + 16, + 10, + 0, + ], + "mark": null, + "x": 16, + "y": 0, + }, + "width": 4.166666666666666, + "x": 84.02777777777777, + "y": 49.5, + }, + { + "color": "#54B399", + "displayValue": undefined, + "height": 0.5, + "panel": { + "height": 100, + "left": 0, + "top": 0, + "width": 100, + }, + "seriesIdentifier": { + "key": "groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}", + "seriesKeys": [ + 1, + ], + "specId": "spec_1", + "splitAccessors": Map {}, + "xAccessor": 0, + "yAccessor": 1, + }, + "seriesStyle": { + "displayValue": { + "alignment": { + "horizontal": "center", + "vertical": "middle", + }, + "fill": { + "textBorder": 0, + }, + "fontFamily": "Inter, BlinkMacSystemFont, Helvetica, Arial, sans-serif", + "fontSize": 10, + "fontStyle": "normal", + "offsetX": 0, + "offsetY": 0, + "padding": 0, + }, + "rect": { + "opacity": 1, + }, + "rectBorder": { + "strokeWidth": 1, + "visible": false, + }, + }, + "transform": { + "x": 0, + "y": 0, + }, + "value": { + "accessor": "y1", + "datum": [ + 16, + 10, + 0, + ], + "mark": null, + "x": 16, + "y": 10, + }, + "width": 4.166666666666666, + "x": 84.02777777777777, + "y": 49.5, + }, + ], + 17 => [ + { + "color": "#54B399", + "displayValue": undefined, + "height": 5.000000000000007, + "panel": { + "height": 100, + "left": 0, + "top": 0, + "width": 100, + }, + "seriesIdentifier": { + "key": "groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}", + "seriesKeys": [ + 1, + ], + "specId": "spec_1", + "splitAccessors": Map {}, + "xAccessor": 0, + "yAccessor": 1, + }, + "seriesStyle": { + "displayValue": { + "alignment": { + "horizontal": "center", + "vertical": "middle", + }, + "fill": { + "textBorder": 0, + }, + "fontFamily": "Inter, BlinkMacSystemFont, Helvetica, Arial, sans-serif", + "fontSize": 10, + "fontStyle": "normal", + "offsetX": 0, + "offsetY": 0, + "padding": 0, + }, + "rect": { + "opacity": 1, + }, + "rectBorder": { + "strokeWidth": 1, + "visible": false, + }, + }, + "transform": { + "x": 0, + "y": 0, + }, + "value": { + "accessor": "y0", + "datum": [ + 17, + 100, + 0, + ], + "mark": null, + "x": 17, + "y": 0, + }, + "width": 4.166666666666666, + "x": 89.58333333333333, + "y": 44.99999999999999, + }, + { + "color": "#54B399", + "displayValue": undefined, + "height": 5.000000000000007, + "panel": { + "height": 100, + "left": 0, + "top": 0, + "width": 100, + }, + "seriesIdentifier": { + "key": "groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}", + "seriesKeys": [ + 1, + ], + "specId": "spec_1", + "splitAccessors": Map {}, + "xAccessor": 0, + "yAccessor": 1, + }, + "seriesStyle": { + "displayValue": { + "alignment": { + "horizontal": "center", + "vertical": "middle", + }, + "fill": { + "textBorder": 0, + }, + "fontFamily": "Inter, BlinkMacSystemFont, Helvetica, Arial, sans-serif", + "fontSize": 10, + "fontStyle": "normal", + "offsetX": 0, + "offsetY": 0, + "padding": 0, + }, + "rect": { + "opacity": 1, + }, + "rectBorder": { + "strokeWidth": 1, + "visible": false, + }, + }, + "transform": { + "x": 0, + "y": 0, + }, + "value": { + "accessor": "y1", + "datum": [ + 17, + 100, + 0, + ], + "mark": null, + "x": 17, + "y": 100, + }, + "width": 4.166666666666666, + "x": 89.58333333333333, + "y": 44.99999999999999, + }, + ], + 18 => [ + { + "color": "#54B399", + "displayValue": undefined, + "height": 50, + "panel": { + "height": 100, + "left": 0, + "top": 0, + "width": 100, + }, + "seriesIdentifier": { + "key": "groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}", + "seriesKeys": [ + 1, + ], + "specId": "spec_1", + "splitAccessors": Map {}, + "xAccessor": 0, + "yAccessor": 1, + }, + "seriesStyle": { + "displayValue": { + "alignment": { + "horizontal": "center", + "vertical": "middle", + }, + "fill": { + "textBorder": 0, + }, + "fontFamily": "Inter, BlinkMacSystemFont, Helvetica, Arial, sans-serif", + "fontSize": 10, + "fontStyle": "normal", + "offsetX": 0, + "offsetY": 0, + "padding": 0, + }, + "rect": { + "opacity": 1, + }, + "rectBorder": { + "strokeWidth": 1, + "visible": false, + }, + }, + "transform": { + "x": 0, + "y": 0, + }, + "value": { + "accessor": "y0", + "datum": [ + 18, + 0, + 1000, + ], + "mark": null, + "x": 18, + "y": 1000, + }, + "width": 4.166666666666666, + "x": 95.13888888888889, + "y": 0, + }, + { + "color": "#54B399", + "displayValue": undefined, + "height": 50, + "panel": { + "height": 100, + "left": 0, + "top": 0, + "width": 100, + }, + "seriesIdentifier": { + "key": "groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}", + "seriesKeys": [ + 1, + ], + "specId": "spec_1", + "splitAccessors": Map {}, + "xAccessor": 0, + "yAccessor": 1, + }, + "seriesStyle": { + "displayValue": { + "alignment": { + "horizontal": "center", + "vertical": "middle", + }, + "fill": { + "textBorder": 0, + }, + "fontFamily": "Inter, BlinkMacSystemFont, Helvetica, Arial, sans-serif", + "fontSize": 10, + "fontStyle": "normal", + "offsetX": 0, + "offsetY": 0, + "padding": 0, + }, + "rect": { + "opacity": 1, + }, + "rectBorder": { + "strokeWidth": 1, + "visible": false, + }, + }, + "transform": { + "x": 0, + "y": 0, + }, + "value": { + "accessor": "y1", + "datum": [ + 18, + 0, + 1000, + ], + "mark": null, + "x": 18, + "y": 0, + }, + "width": 4.166666666666666, + "x": 95.13888888888889, + "y": 0, + }, + ], + }, + }, + "spatialMap": IndexedGeometrySpatialMap { + "map": null, + "maxRadius": -Infinity, + "pointGeometries": [], + "points": [], + "searchStartIndex": 0, + }, +} +`; + +exports[`Rendering bars should render two bars within domain 1`] = ` [ { - "color": "blue", + "color": "red", "displayValue": undefined, "height": 100, "panel": { @@ -271,11 +2591,11 @@ exports[`Rendering bars Multi series bar chart - ordinal can render second spec "width": 100, }, "seriesIdentifier": { - "key": "groupId{group_1}spec{bar2}yAccessor{1}splitAccessors{}", + "key": "groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}", "seriesKeys": [ 1, ], - "specId": "bar2", + "specId": "spec_1", "splitAccessors": Map {}, "xAccessor": 0, "yAccessor": 1, @@ -312,18 +2632,18 @@ exports[`Rendering bars Multi series bar chart - ordinal can render second spec "accessor": "y1", "datum": [ 0, - 20, + 10, ], "mark": null, "x": 0, - "y": 20, + "y": 10, }, - "width": 25, - "x": 25, + "width": 50, + "x": 0, "y": 0, }, { - "color": "blue", + "color": "red", "displayValue": undefined, "height": 50, "panel": { @@ -333,11 +2653,11 @@ exports[`Rendering bars Multi series bar chart - ordinal can render second spec "width": 100, }, "seriesIdentifier": { - "key": "groupId{group_1}spec{bar2}yAccessor{1}splitAccessors{}", + "key": "groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}", "seriesKeys": [ 1, ], - "specId": "bar2", + "specId": "spec_1", "splitAccessors": Map {}, "xAccessor": 0, "yAccessor": 1, @@ -374,14 +2694,14 @@ exports[`Rendering bars Multi series bar chart - ordinal can render second spec "accessor": "y1", "datum": [ 1, - 10, + 5, ], "mark": null, "x": 1, - "y": 10, + "y": 5, }, - "width": 25, - "x": 75, + "width": 50, + "x": 50, "y": 50, }, ] diff --git a/packages/charts/src/chart_types/xy_chart/rendering/bars.ts b/packages/charts/src/chart_types/xy_chart/rendering/bars.ts index 35e0cf71c2..9d00a2cc65 100644 --- a/packages/charts/src/chart_types/xy_chart/rendering/bars.ts +++ b/packages/charts/src/chart_types/xy_chart/rendering/bars.ts @@ -7,9 +7,9 @@ */ import { getDatumYValue } from './points'; +import { getY0ScaledValueFn, getY1ScaledValueFn } from './utils'; import { Color } from '../../../common/colors'; import { ScaleBand, ScaleContinuous } from '../../../scales'; -import { ScaleType } from '../../../scales/constants'; import { TextMeasure } from '../../../utils/bbox/canvas_text_bbox_calculator'; import { clamp, mergePartial } from '../../../utils/common'; import { Dimensions } from '../../../utils/dimensions'; @@ -43,6 +43,7 @@ export function renderBars( chartRotation: number, minBarHeight: number, color: Color, + isBandedSpec: boolean, sharedSeriesStyle: BarSeriesStyle, displayValueSettings?: DisplayValueSpec, styleAccessor?: BarStyleAccessor, @@ -50,33 +51,35 @@ export function renderBars( ): BarTuple { const { fontSize, fontFamily } = sharedSeriesStyle.displayValue; const initialBarTuple: BarTuple = { barGeometries: [], indexedGeometryMap: new IndexedGeometryMap() } as BarTuple; - const isLogY = yScale.type === ScaleType.Log; - const isInvertedY = yScale.isInverted; + const y1Fn = getY1ScaledValueFn(yScale); + const y0Fn = getY0ScaledValueFn(yScale); + return dataSeries.data.reduce((barTuple: BarTuple, datum) => { const xScaled = xScale.scale(datum.x); if (!xScale.isValueInDomain(datum.x) || Number.isNaN(xScaled)) { return barTuple; // don't create a bar if not within the xScale domain } const { barGeometries, indexedGeometryMap } = barTuple; - const { y0, y1, initialY1, filled } = datum; - const rawY = isLogY && (y1 === 0 || y1 === null) ? yScale.range[0] : yScale.scale(y1); - - const y0Scaled = isLogY - ? y0 === 0 || y0 === null - ? yScale.range[isInvertedY ? 1 : 0] - : yScale.scale(y0) - : yScale.scale(y0 === null ? 0 : y0); - - const finiteHeight = y0Scaled - rawY || 0; - const absHeight = Math.abs(finiteHeight); - const height = absHeight === 0 ? absHeight : Math.max(minBarHeight, absHeight); // extend nonzero bars - const heightExtension = height - absHeight; - const isUpsideDown = finiteHeight < 0; - const finiteY = Number.isNaN(y0Scaled + rawY) ? 0 : rawY; - const y = isUpsideDown ? finiteY - height + heightExtension : finiteY - heightExtension; + const { y1, initialY1, filled } = datum; - const seriesIdentifier: XYChartSeriesIdentifier = getSeriesIdentifierFromDataSeries(dataSeries); + const y1Scaled = y1Fn(datum); + const y0Scaled = y0Fn(datum); + + // orientation independent height + const yDiff = Math.abs(y1Scaled - y0Scaled); + // amount required to reach the minBarHeight requested + const addedMinBarHeight = yDiff === 0 || yDiff >= minBarHeight ? 0 : minBarHeight - yDiff; + + // the y coordinate in screen-space. + const yScreenSpaceCoord = + Math.min(y1Scaled, y0Scaled) + + // adding half of the required minBarHeight if banded bar chart + // and reduce the y coordinate if the Y value is positive to render correctly the increased bar height + (isBandedSpec ? -addedMinBarHeight / 2 : -addedMinBarHeight * ((y1 ?? 0) >= 0 ? 1 : 0)); + // the actual height of the bar + const height = yDiff + addedMinBarHeight; + const seriesIdentifier: XYChartSeriesIdentifier = getSeriesIdentifierFromDataSeries(dataSeries); const seriesStyle = getBarStyleOverrides(datum, seriesIdentifier, sharedSeriesStyle, styleAccessor); const maxPixelWidth = clamp(seriesStyle.rect.widthRatio ?? 1, 0, 1) * xScale.bandwidth; @@ -85,7 +88,7 @@ export function renderBars( const width = clamp(seriesStyle.rect.widthPixel ?? xScale.bandwidth, minPixelWidth, maxPixelWidth); const x = xScaled + xScale.bandwidth * orderIndex + xScale.bandwidth / 2 - width / 2; - const y1Value = getDatumYValue(datum, false, false, stackMode); + const y1Value = getDatumYValue(datum, false, isBandedSpec, stackMode); const formattedDisplayValue = displayValueSettings?.valueFormatter?.(y1Value); // only show displayValue for even bars if showOverlappingValue @@ -133,7 +136,7 @@ export function renderBars( const barGeometry: BarGeometry = { displayValue, x, - y, + y: yScreenSpaceCoord, transform: { x: 0, y: 0 }, width, height, @@ -143,6 +146,21 @@ export function renderBars( seriesStyle, panel, }; + + if (isBandedSpec) { + // index also the Y0 value with the same geometry + indexedGeometryMap.set({ + ...barGeometry, + value: { + x: datum.x, + y: getDatumYValue(datum, true, isBandedSpec, stackMode), + mark: null, + accessor: BandedAccessorType.Y0, + datum: datum.datum, + }, + }); + } + indexedGeometryMap.set(barGeometry); if (y1 !== null && initialY1 !== null && filled?.y1 === undefined) { diff --git a/packages/charts/src/chart_types/xy_chart/rendering/points.ts b/packages/charts/src/chart_types/xy_chart/rendering/points.ts index 3e5c91e86d..1f09cc5688 100644 --- a/packages/charts/src/chart_types/xy_chart/rendering/points.ts +++ b/packages/charts/src/chart_types/xy_chart/rendering/points.ts @@ -42,7 +42,7 @@ export function renderPoints( color: Color, pointStyle: PointStyle, isolatedPointThemeStyle: PointStyle, - isBandChart: boolean, + isBandedSpec: boolean, markSizeOptions: MarkSizeOptions, useSpatialIndex: boolean, allowIsolated: boolean, @@ -74,12 +74,12 @@ export function renderPoints( if (Number.isNaN(x)) return acc; const points: PointGeometry[] = []; - const yDatumKeyNames: Array> = isBandChart ? ['y0', 'y1'] : ['y1']; + const yDatumKeyNames: Array> = isBandedSpec ? ['y0', 'y1'] : ['y1']; yDatumKeyNames.forEach((yDatumKeyName, keyIndex) => { const valueAccessor = getYDatumValueFn(yDatumKeyName); const y = yDatumKeyName === 'y1' ? y1Fn(datum) : y0Fn(datum); - const originalY = getDatumYValue(datum, keyIndex === 0, isBandChart, dataSeries.stackMode); + const originalY = getDatumYValue(datum, keyIndex === 0, isBandedSpec, dataSeries.stackMode); const seriesIdentifier: XYChartSeriesIdentifier = getSeriesIdentifierFromDataSeries(dataSeries); const styleOverrides = getPointStyleOverrides(datum, seriesIdentifier, styleAccessor); const style = buildPointGeometryStyles(color, pointStyle, styleOverrides); @@ -102,7 +102,7 @@ export function renderPoints( x: xValue, y: originalY, mark, - accessor: isBandChart && keyIndex === 0 ? BandedAccessorType.Y0 : BandedAccessorType.Y1, + accessor: isBandedSpec && keyIndex === 0 ? BandedAccessorType.Y0 : BandedAccessorType.Y1, datum: datum.datum, }, transform: { @@ -157,17 +157,17 @@ export function getPointStyleOverrides( * Get the original/initial Y value from the datum * @param datum a DataSeriesDatum * @param lookingForY0 if we are interested in the y0 value, false for y1 - * @param isBandChart if the chart is a band chart + * @param isBandedSpec if the chart is a band chart * @param stackMode an optional stack mode * @internal */ export function getDatumYValue( { y1, y0, initialY1, initialY0 }: DataSeriesDatum, lookingForY0: boolean, - isBandChart: boolean, + isBandedSpec: boolean, stackMode?: StackMode, ) { - if (isBandChart) { + if (isBandedSpec) { // on band stacked charts in percentage mode, the values I'm looking for are the percentage value // that are already computed and available on y0 and y1 // in all other cases for band charts, I want to get back the original/initial value of y0 and y1 diff --git a/packages/charts/src/chart_types/xy_chart/rendering/rendering.bars.test.ts b/packages/charts/src/chart_types/xy_chart/rendering/rendering.bars.test.ts index fc83f7e41e..049bfba56a 100644 --- a/packages/charts/src/chart_types/xy_chart/rendering/rendering.bars.test.ts +++ b/packages/charts/src/chart_types/xy_chart/rendering/rendering.bars.test.ts @@ -9,13 +9,14 @@ import { MockGlobalSpec, MockSeriesSpec } from '../../../mocks/specs'; import { MockStore } from '../../../mocks/store'; import { ScaleType } from '../../../scales/constants'; +import { BarGeometry } from '../../../utils/geometry'; import { computeSeriesGeometriesSelector } from '../state/selectors/compute_series_geometries'; const SPEC_ID = 'spec_1'; const GROUP_ID = 'group_1'; describe('Rendering bars', () => { - test('Can render two bars within domain', () => { + it('should render two bars within domain', () => { const store = MockStore.default({ width: 100, height: 100, top: 0, left: 0 }); const spec = MockSeriesSpec.bar({ id: SPEC_ID, @@ -40,7 +41,7 @@ describe('Rendering bars', () => { }); describe('Single series bar chart - ordinal', () => { - test('Can render bars with value labels', () => { + it('should render bars with value labels', () => { const store = MockStore.default({ width: 100, height: 100, top: 0, left: 0 }); MockStore.addSpecs( [ @@ -70,7 +71,7 @@ describe('Rendering bars', () => { expect(geometries.bars[0]?.value[0]?.displayValue).toBeDefined(); }); - test('Can hide value labels if no formatter or showValueLabels is false/undefined', () => { + it('should hide value labels if no formatter or showValueLabels is false/undefined', () => { const store = MockStore.default({ width: 100, height: 100, top: 0, left: 0 }); MockStore.addSpecs( [ @@ -100,7 +101,7 @@ describe('Rendering bars', () => { expect(geometries.bars[0]?.value[0]?.displayValue).toBeUndefined(); }); - test('Can render bars with alternating value labels', () => { + it('should render bars with alternating value labels', () => { const store = MockStore.default({ width: 100, height: 100, top: 0, left: 0 }); MockStore.addSpecs( [ @@ -180,4 +181,49 @@ describe('Rendering bars', () => { expect(bars[1]?.value).toMatchSnapshot(); }); }); + + describe('Negative, minBarHeight, flipped and banded datasets', () => { + it('should render bars with alternating value labels', () => { + const store = MockStore.default({ width: 100, height: 100, top: 0, left: 0 }); + MockStore.addSpecs( + [ + MockSeriesSpec.bar({ + id: SPEC_ID, + groupId: GROUP_ID, + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + xAccessor: 0, + yAccessors: [1], + y0Accessors: [2], + data: [ + [1, -1000, 0], + [2, -100, 0], + [3, -10, 0], + [4.5, -10, 10], + [5, -1, 0], + [6, 0, 0], + [7, -1, 0], + [8, 0, 0], + [9, 0, 0], + [10, 5, 10], + [11, 1, -10], + [12, 1, 0], + [13, 0, 0], + [14, 1, 0], + [15, 10, -10], + [16, 10, 0], + [17, 100, 0], + [18, 0, 1000], + ], + }), + ], + store, + ); + const { geometriesIndex } = computeSeriesGeometriesSelector(store.getState()); + const indexedBarGeometries = geometriesIndex.getMergeData().linearGeometries as BarGeometry[][]; + + expect(indexedBarGeometries).toSatisfyAll(({ length }) => length === 2); + expect(geometriesIndex).toMatchSnapshot(); + }); + }); }); diff --git a/packages/charts/src/chart_types/xy_chart/rendering/utils.ts b/packages/charts/src/chart_types/xy_chart/rendering/utils.ts index c701f278e4..72a07bc26c 100644 --- a/packages/charts/src/chart_types/xy_chart/rendering/utils.ts +++ b/packages/charts/src/chart_types/xy_chart/rendering/utils.ts @@ -10,7 +10,7 @@ import { LegendItem } from '../../../common/legend'; import { ScaleBand, ScaleContinuous } from '../../../scales'; import { isLogarithmicScale } from '../../../scales/types'; import { MarkBuffer } from '../../../specs'; -import { getDistance } from '../../../utils/common'; +import { getDistance, isWithinRange } from '../../../utils/common'; import { BarGeometry, ClippedRanges, isPointGeometry, PointGeometry } from '../../../utils/geometry'; import { GeometryStateStyle, SharedGeometryStateStyle } from '../../../utils/themes/theme'; import { DataSeriesDatum, FilledValues, XYChartSeriesIdentifier } from '../utils/series'; @@ -137,7 +137,8 @@ export function isPointOnGeometry( return distance <= radius + radiusBuffer; } const { width, height } = indexedGeometry; - return yCoordinate >= y && yCoordinate <= y + height && xCoordinate >= x && xCoordinate <= x + width; + if (!isWithinRange([x, x + width])(xCoordinate)) return false; + return isWithinRange([y, y + height])(yCoordinate); } const getScaleTypeValueValidator = (yScale: ScaleContinuous): ((n: number) => boolean) => { diff --git a/packages/charts/src/chart_types/xy_chart/state/selectors/get_tooltip_values_highlighted_geoms.ts b/packages/charts/src/chart_types/xy_chart/state/selectors/get_tooltip_values_highlighted_geoms.ts index f21c37e70c..e903cedec0 100644 --- a/packages/charts/src/chart_types/xy_chart/state/selectors/get_tooltip_values_highlighted_geoms.ts +++ b/packages/charts/src/chart_types/xy_chart/state/selectors/get_tooltip_values_highlighted_geoms.ts @@ -129,58 +129,63 @@ function getTooltipAndHighlightFromValue( const highlightedGeometries: IndexedGeometry[] = []; const xValues = new Set(); const hideNullValues = !tooltip.showNullValues; - const values = matchingGeoms.reduce((acc, indexedGeometry) => { - if (hideNullValues && indexedGeometry.value.y === null) { - return acc; - } - const { - seriesIdentifier: { specId }, - } = indexedGeometry; - const spec = getSpecsById(seriesSpecs, specId); - - // safe guard check - if (!spec) { - return acc; - } - const { xAxis, yAxis } = getAxesSpecForSpecId(axesSpecs, spec.groupId, chartRotation); - - // yScales is ensured by the enclosing if - const yScale = scales.yScales.get(getSpecDomainGroupId(spec)); - if (!yScale) { - return acc; - } - - // check if the pointer is on the geometry (avoid checking if using external pointer event) - let isHighlighted = false; - if ( - (!externalPointerEvent || isPointerOutEvent(externalPointerEvent)) && - isPointOnGeometry(x, y, indexedGeometry, settings.pointBuffer) - ) { - isHighlighted = true; - highlightedGeometries.push(indexedGeometry); - } - - // format the tooltip values - const formattedTooltip = formatTooltipValue( - indexedGeometry, - spec, - isHighlighted, - hasSingleSeries, - isBandedSpec(spec), - yAxis, - ); - - // format only one time the x value - if (!header) { - // if we have a tooltipHeaderFormatter, then don't pass in the xAxis as the user will define a formatter - const formatterAxis = tooltip.headerFormatter ? undefined : xAxis; - header = formatTooltipHeader(indexedGeometry, spec, formatterAxis); - } - - xValues.add(indexedGeometry.value.x); - - return [...acc, formattedTooltip]; - }, []); + const values = matchingGeoms + .toSorted((a, b) => { + // presort matchingGeoms to group by series then y value to prevent flipping + return b.seriesIdentifier.key.localeCompare(a.seriesIdentifier.key) || b.value.y - a.value.y; + }) + .reduce((acc, indexedGeometry) => { + if (hideNullValues && indexedGeometry.value.y === null) { + return acc; + } + const { + seriesIdentifier: { specId }, + } = indexedGeometry; + const spec = getSpecsById(seriesSpecs, specId); + + // safe guard check + if (!spec) { + return acc; + } + const { xAxis, yAxis } = getAxesSpecForSpecId(axesSpecs, spec.groupId, chartRotation); + + // yScales is ensured by the enclosing if + const yScale = scales.yScales.get(getSpecDomainGroupId(spec)); + if (!yScale) { + return acc; + } + + // check if the pointer is on the geometry (avoid checking if using external pointer event) + let isHighlighted = false; + if ( + (!externalPointerEvent || isPointerOutEvent(externalPointerEvent)) && + isPointOnGeometry(x, y, indexedGeometry, settings.pointBuffer) + ) { + isHighlighted = true; + highlightedGeometries.push(indexedGeometry); + } + + // format the tooltip values + const formattedTooltip = formatTooltipValue( + indexedGeometry, + spec, + isHighlighted, + hasSingleSeries, + isBandedSpec(spec), + yAxis, + ); + + // format only one time the x value + if (!header) { + // if we have a tooltipHeaderFormatter, then don't pass in the xAxis as the user will define a formatter + const formatterAxis = tooltip.headerFormatter ? undefined : xAxis; + header = formatTooltipHeader(indexedGeometry, spec, formatterAxis); + } + + xValues.add(indexedGeometry.value.x); + + return [...acc, formattedTooltip]; + }, []); if (values.length > 1 && xValues.size === values.length) { // TODO: remove after tooltip redesign diff --git a/packages/charts/src/chart_types/xy_chart/state/utils/utils.ts b/packages/charts/src/chart_types/xy_chart/state/utils/utils.ts index 54375e005e..ca0da97733 100644 --- a/packages/charts/src/chart_types/xy_chart/state/utils/utils.ts +++ b/packages/charts/src/chart_types/xy_chart/state/utils/utils.ts @@ -393,6 +393,7 @@ function renderGeometries( chartRotation, spec.minBarHeight ?? 0, color, + isBandedSpec(spec), barSeriesStyle, displayValueSettings, spec.styleAccessor, diff --git a/packages/charts/src/chart_types/xy_chart/tooltip/tooltip.ts b/packages/charts/src/chart_types/xy_chart/tooltip/tooltip.ts index b837242c95..87537213c7 100644 --- a/packages/charts/src/chart_types/xy_chart/tooltip/tooltip.ts +++ b/packages/charts/src/chart_types/xy_chart/tooltip/tooltip.ts @@ -42,12 +42,12 @@ export function formatTooltipValue( spec: BasicSeriesSpec, isHighlighted: boolean, hasSingleSeries: boolean, - isBanded: boolean, + isBandedSpec: boolean, axisSpec?: AxisSpec, ): TooltipValue { let label = getSeriesName(seriesIdentifier, hasSingleSeries, true, spec); - if (isBanded && (isAreaSeriesSpec(spec) || isBarSeriesSpec(spec))) { + if (isBandedSpec && (isAreaSeriesSpec(spec) || isBarSeriesSpec(spec))) { const { y0AccessorFormat = Y0_ACCESSOR_POSTFIX, y1AccessorFormat = Y1_ACCESSOR_POSTFIX } = spec; const formatter = accessor === BandedAccessorType.Y0 ? y0AccessorFormat : y1AccessorFormat; label = getAccessorFormatLabel(formatter, label); diff --git a/packages/charts/src/utils/common.test.ts b/packages/charts/src/utils/common.test.ts index b1f617db46..a59bb234eb 100644 --- a/packages/charts/src/utils/common.test.ts +++ b/packages/charts/src/utils/common.test.ts @@ -24,6 +24,7 @@ import { clampAll, sortNumbers, isSorted, + inRange, } from './common'; describe('common utilities', () => { @@ -1163,4 +1164,48 @@ describe('#isDefinedFrom', () => { expect([0, 100, 200, 300, 400].reduce(...clampAll(100, 300))).toEqual([100, 200, 300]); }); }); + + describe('#inRange', () => { + describe('#firstHalf', () => { + it.each<{ start: number; end: number; value: number; expected: boolean; exclusive?: boolean }>([ + { start: 0, end: 100, value: 25, expected: true }, + { start: 0, end: 100, value: 75, expected: false }, + { start: 100, end: 0, value: 25, expected: false }, + { start: 100, end: 0, value: 75, expected: true }, + { start: -10, end: 100, value: 0, expected: true }, + { start: -100, end: 100, value: 50, expected: false }, + { start: -100, end: 100, value: -50, expected: true }, + { start: -100, end: 100, value: -110, expected: false }, + { start: 100, end: -100, value: 110, expected: false }, + { start: 10, end: 20, value: 10, expected: false, exclusive: true }, + { start: 10, end: 20, value: 15, expected: false, exclusive: true }, + ])( + 'should return $expected for $value, given a range of [$start, $end]', + ({ start, end, value, expected, exclusive }) => { + expect(inRange(start, end, exclusive).firstHalf(value)).toBe(expected); + }, + ); + }); + describe('#lastHalf', () => { + it.each<{ start: number; end: number; value: number; expected: boolean; exclusive?: boolean }>([ + { start: 0, end: 100, value: 25, expected: false }, + { start: 0, end: 100, value: 75, expected: true }, + { start: 100, end: 0, value: 25, expected: true }, + { start: 100, end: 0, value: 75, expected: false }, + { start: 100, end: 10, value: 0, expected: false }, + { start: -100, end: 10, value: 0, expected: true }, + { start: -100, end: 100, value: 50, expected: true }, + { start: -100, end: 100, value: -50, expected: false }, + { start: -100, end: 100, value: 110, expected: false }, + { start: 100, end: -100, value: -110, expected: false }, + { start: 10, end: 20, value: 20, expected: false, exclusive: true }, + { start: 10, end: 20, value: 15, expected: false, exclusive: true }, + ])( + 'should return $expected for $value, given a range of [$start, $end]', + ({ start, end, value, expected, exclusive }) => { + expect(inRange(start, end, exclusive).lastHalf(value)).toBe(expected); + }, + ); + }); + }); }); diff --git a/packages/charts/src/utils/common.tsx b/packages/charts/src/utils/common.tsx index fa21db9ac7..975a8c0eaf 100644 --- a/packages/charts/src/utils/common.tsx +++ b/packages/charts/src/utils/common.tsx @@ -727,11 +727,44 @@ export const isBetween = (min: number, max: number, exclusive = false): ((n: num * Returns `Array.filter` callback for values between two unordered values * @internal */ -export const isWithinRange = (range: [number, number], exclusive = false) => { - const [min, max] = sortNumbers(range); +export const isWithinRange = (r: [number, number], exclusive = false) => { + const [min, max] = sortNumbers(r); return isBetween(min, max, exclusive); }; +/** + * Returns utilities for a given range from start to end + * @internal + */ +export const inRange = (start: number, end: number, exclusive = false) => { + const diff = Math.abs(start - end); + const [min, max] = sortNumbers([start, end]); + const isHalfFromMin = isBetween(min, max - diff / 2, exclusive); + const isHalfFromMax = isBetween(min + diff / 2, max, exclusive); + const isWithin = isBetween(min, max, exclusive); + + return { + /** + * Returns true if values are within the first half of range, from start halfway to end + */ + firstHalf: (n: number) => { + return start === min ? isHalfFromMin(n) : isHalfFromMax(n); + }, + /** + * Returns true if values are within the last half of range, from end halfway to start + */ + lastHalf: (n: number) => { + return end === max ? isHalfFromMax(n) : isHalfFromMin(n); + }, + /** + * Returns true if value is within the entire range + */ + within: (n: number) => { + return isWithin(n); + }, + }; +}; + /** * Returns `Array.reduce` callback to clamp values and remove duplicates * @internal diff --git a/storybook/stories/bar/45_min_height.story.tsx b/storybook/stories/bar/45_min_height.story.tsx index 9384ef901f..b673ea18a9 100644 --- a/storybook/stories/bar/45_min_height.story.tsx +++ b/storybook/stories/bar/45_min_height.story.tsx @@ -13,9 +13,13 @@ import { Axis, BarSeries, Chart, Position, ScaleType, Settings } from '@elastic/ import { ChartsStory } from '../../types'; import { useBaseTheme } from '../../use_base_theme'; +import { customKnobs } from '../utils/knobs'; export const Example: ChartsStory = (_, { title, description }) => { const minBarHeight = number('minBarHeight', 5); + const yScaleType = customKnobs.enum.scaleType(undefined, 'linear', { + include: ['LinearBinary', 'Linear', 'Time', 'Log', 'Sqrt'], + }); const data = [ [1, 100000], [2, 10000], @@ -35,7 +39,7 @@ export const Example: ChartsStory = (_, { title, description }) => { { } }; -const seriesMap = { - line: LineSeries, - area: AreaSeries, -}; - -const getSeriesType = () => - select( - 'Series Type', - { - Line: 'line', - Area: 'area', - }, - 'line', - ); - const getInitalData = (rows: number) => { const quart = Math.round(rows / 4); return [...range(quart, -quart, -1), ...range(-quart, quart + 1, 1)]; @@ -118,9 +93,8 @@ export const Example: ChartsStory = (_, { title, description }) => { const yLogKnobs = getLogKnobs(false); const xLogKnobs = getLogKnobs(true); const data = getData(rows, yLogKnobs, xLogKnobs); - const type = getSeriesType(); + const [Series] = customKnobs.enum.xySeries('Series Type', 'line', { exclude: ['bubble', 'bar'] }); const curve = customKnobs.enum.curve('Curve type'); - const Series = seriesMap[type]; return ( diff --git a/storybook/stories/test_cases/12_log_with_negative_values.story.tsx b/storybook/stories/test_cases/12_log_with_negative_values.story.tsx new file mode 100644 index 0000000000..b924254a01 --- /dev/null +++ b/storybook/stories/test_cases/12_log_with_negative_values.story.tsx @@ -0,0 +1,68 @@ +/* + * 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 } from '@storybook/addon-knobs'; +import numeral from 'numeral'; +import React from 'react'; + +import { Axis, Chart, Position, ScaleType, Settings } from '@elastic/charts'; +import { getRandomNumberGenerator } from '@elastic/charts/src/mocks/utils'; + +import { ChartsStory } from '../../types'; +import { useBaseTheme } from '../../use_base_theme'; +import { customKnobs } from '../utils/knobs'; + +const rng = getRandomNumberGenerator(); + +const data = new Array(20).fill(1).flatMap((_, x) => + ['A', 'B', 'C'].map((g) => { + const y1 = rng(30, 100); + const y0 = rng(0.01, 20); + return { + x, + g, + y1Pos: y1, + y1Neg: -y1, + y0Pos: y0, + y0Neg: -y0, + }; + }), +); + +export const Example: ChartsStory = (_, { title, description }) => { + const showLegend = boolean('Show legend', false); + const yScaleType = customKnobs.enum.scaleType('Scale Type', ScaleType.Log, { include: ['Linear', 'Log'] }); + const [Series] = customKnobs.enum.xySeries('Series Type', 'bar', { exclude: ['bubble'] }); + const logMinLimit = number('logMinLimit', 1, { min: 0 }); + const yNice = boolean('Nice y ticks', false); + const banded = boolean('Banded', false); + const split = boolean('Split', false); + const stacked = boolean('Stacked', true); + const showPosData = boolean('Show positive data', false); + + return ( + + + + + numeral(d).format('0.[0]')} + data={split ? data : data.filter(({ g }) => g === 'A')} + yNice={yNice} + xAccessor="x" + yAccessors={showPosData ? ['y1Pos'] : ['y1Neg']} + y0Accessors={!banded ? [] : showPosData ? ['y0Pos'] : ['y0Neg']} + splitSeriesAccessors={split ? ['g'] : []} + stackAccessors={stacked ? ['g'] : []} + /> + + ); +}; diff --git a/storybook/stories/test_cases/test_cases.stories.tsx b/storybook/stories/test_cases/test_cases.stories.tsx index 629816de66..81302336b1 100644 --- a/storybook/stories/test_cases/test_cases.stories.tsx +++ b/storybook/stories/test_cases/test_cases.stories.tsx @@ -22,3 +22,4 @@ export { Example as duplicateLabelsInPartitionLegend } from './9_duplicate_label export { Example as highlighterZIndex } from './10_highlighter_z_index.story'; export { Example as domainEdges } from './21_domain_edges.story'; export { Example as startDayOfWeek } from './11_start_day_of_week.story'; +export { Example as logWithNegativeValues } from './12_log_with_negative_values.story';