From 37b4df1c164d9c24d92abe96f8358ad87cf67bcc Mon Sep 17 00:00:00 2001 From: Rink Stiekema Date: Fri, 15 Mar 2024 17:33:04 +0100 Subject: [PATCH] Error bar domain ignore null (#4297) Duplicate of [this PR](https://github.com/recharts/recharts/pull/4192). I made this PR to also have this fix available in v2.x. Hope this is the correct way! ## Description The entry value for an error bar receives the default value `0` in case it was null. This would cause the domain to be calculated incorrectly, as `0` may not be a value in the data array. When using error bars, the domain accounts for any potential in the error values. Error values are expressed relative to the main entry value. In case the main entry value is nullish, its default value would be `0`. This may not be a value in the actual data. ## Related Issue No known related issues yet ## Motivation and Context Given this example data: ``` const data = [ { x: 1, y: 200, errorY: [10, 10] }, { x: 1, y: 300, errorY: [10, 10] }, { x: 1, y: 400, errorY: [10, 10] }, ] ``` The min value of the y-axis domain would be: `[min(y - errorY[0]), max(y + errorY[1])] = [200 - 10, 400 + 10] = [190, 410]` Now consider this other example: ``` const data = [ { x: 1, y: 200, errorY: [10, 10] }, { x: 1, y: null, errorY: [10, 10] }, { x: 1, y: 400, errorY: [10, 10] }, ] ``` The current implementation would report: `[min(y - errorY[0]), max(y + errorY[1])] = [0 - 10, 400 + 10] = [-10, 410]` While the expected behaviour is: `[min(y - errorY[0]), max(y + errorY[1])] = [200 - 10, 400 + 10] = [190, 410]` Some codesandbox examples can be found [here](https://codesandbox.io/p/sandbox/recharts-issue-template-forked-yvpjfs?layout=%257B%2522sidebarPanel%2522%253A%2522EXPLORER%2522%252C%2522rootPanelGroup%2522%253A%257B%2522direction%2522%253A%2522horizontal%2522%252C%2522contentType%2522%253A%2522UNKNOWN%2522%252C%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522id%2522%253A%2522ROOT_LAYOUT%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522UNKNOWN%2522%252C%2522direction%2522%253A%2522vertical%2522%252C%2522id%2522%253A%2522cls4stopf00073b6n4rx2oizn%2522%252C%2522sizes%2522%253A%255B70%252C30%255D%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522EDITOR%2522%252C%2522direction%2522%253A%2522horizontal%2522%252C%2522id%2522%253A%2522EDITOR%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522EDITOR%2522%252C%2522id%2522%253A%2522cls4stope00033b6nv9wlqt8t%2522%257D%255D%257D%252C%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522SHELLS%2522%252C%2522direction%2522%253A%2522horizontal%2522%252C%2522id%2522%253A%2522SHELLS%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522SHELLS%2522%252C%2522id%2522%253A%2522cls4stope00043b6n7de0duiv%2522%257D%255D%252C%2522sizes%2522%253A%255B100%255D%257D%255D%257D%252C%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522DEVTOOLS%2522%252C%2522direction%2522%253A%2522vertical%2522%252C%2522id%2522%253A%2522DEVTOOLS%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522DEVTOOLS%2522%252C%2522id%2522%253A%2522cls4stope00063b6nu6bf8195%2522%257D%255D%252C%2522sizes%2522%253A%255B100%255D%257D%255D%252C%2522sizes%2522%253A%255B50%252C50%255D%257D%252C%2522tabbedPanels%2522%253A%257B%2522cls4stope00033b6nv9wlqt8t%2522%253A%257B%2522tabs%2522%253A%255B%257B%2522id%2522%253A%2522cls4stope00023b6njx4kyzoc%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522type%2522%253A%2522FILE%2522%252C%2522filepath%2522%253A%2522%252Fsrc%252Findex.js%2522%252C%2522state%2522%253A%2522IDLE%2522%257D%255D%252C%2522id%2522%253A%2522cls4stope00033b6nv9wlqt8t%2522%252C%2522activeTabId%2522%253A%2522cls4stope00023b6njx4kyzoc%2522%257D%252C%2522cls4stope00063b6nu6bf8195%2522%253A%257B%2522tabs%2522%253A%255B%257B%2522id%2522%253A%2522cls4stope00053b6n46m3ry4p%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522type%2522%253A%2522UNASSIGNED_PORT%2522%252C%2522port%2522%253A0%252C%2522path%2522%253A%2522%252F%2522%257D%255D%252C%2522id%2522%253A%2522cls4stope00063b6nu6bf8195%2522%252C%2522activeTabId%2522%253A%2522cls4stope00053b6n46m3ry4p%2522%257D%252C%2522cls4stope00043b6n7de0duiv%2522%253A%257B%2522tabs%2522%253A%255B%255D%252C%2522id%2522%253A%2522cls4stope00043b6n7de0duiv%2522%257D%257D%252C%2522showDevtools%2522%253Atrue%252C%2522showShells%2522%253Atrue%252C%2522showSidebar%2522%253Atrue%252C%2522sidebarPanelSize%2522%253A15%257D). ## How Has This Been Tested? I have added unit tests for the domain creation. ## Types of changes - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [x] Breaking change (fix or feature that would cause existing functionality to change) ## Checklist: - [x] My code follows the code style of this project. - [ ] My change requires a change to the documentation. - [ ] I have updated the documentation accordingly. - [x] I have added tests to cover my changes. - [ ] I have added a storybook story or extended an existing story to show my changes - [x] All new and existing tests passed. --- src/util/ChartUtils.ts | 4 ++- test/util/ChartUtils.spec.tsx | 62 +++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/src/util/ChartUtils.ts b/src/util/ChartUtils.ts index 29f7ed5a93..4e1d0b54fa 100644 --- a/src/util/ChartUtils.ts +++ b/src/util/ChartUtils.ts @@ -467,7 +467,9 @@ export const getDomainOfErrorBars = ( return data.reduce( (result: NumberDomain, entry: object): NumberDomain => { - const entryValue = getValueByDataKey(entry, dataKey, 0); + const entryValue = getValueByDataKey(entry, dataKey); + if (isNil(entryValue)) return result; + const mainValue = Array.isArray(entryValue) ? [min(entryValue), max(entryValue)] : [entryValue, entryValue]; const errorDomain = keys.reduce( (prevErrorArr: [number, number], k: DataKey): NumberDomain => { diff --git a/test/util/ChartUtils.spec.tsx b/test/util/ChartUtils.spec.tsx index 41c110ef62..63967acd59 100644 --- a/test/util/ChartUtils.spec.tsx +++ b/test/util/ChartUtils.spec.tsx @@ -613,6 +613,68 @@ describe('getDomainOfErrorBars', () => { expect(getDomainOfErrorBars(data, line, 'y', 'horizontal', 'yAxis')).toEqual([85, 220]); }); }); + + describe('with null-entries in data array', () => { + const scatter = ( + + + + + ); + + it('should ignore null values for domain with direction y in yAxis domain', () => { + const valueNull = { + x: 3, + y: null, + error: 30, + error2: 15, + }; + expect(getDomainOfErrorBars([...data, valueNull], scatter, 'y', undefined, 'yAxis')).toEqual([90, 220]); + + const valueAndErrorNull = { + x: 3, + y: null, + error: null, + error2: null, + }; + expect(getDomainOfErrorBars([...data, valueAndErrorNull], scatter, 'y', undefined, 'yAxis')).toEqual([90, 220]); + + const errorNull = { + x: 3, + y: 300, + error: null, + error2: null, + }; + expect(getDomainOfErrorBars([...data, errorNull], scatter, 'y', undefined, 'yAxis')).toEqual([90, 300]); + }); + + it('should ignore null values for domain with direction x in xAxis domain', () => { + const valueNull = { + x: null, + y: 300, + error: 30, + error2: 15, + }; + + expect(getDomainOfErrorBars([...data, valueNull], scatter, 'x', undefined, 'xAxis')).toEqual([-14, 17]); + + const valueAndErrorNull = { + x: null, + y: 300, + error: 30, + error2: null, + }; + expect(getDomainOfErrorBars([...data, valueAndErrorNull], scatter, 'x', undefined, 'xAxis')).toEqual([-14, 17]); + + const errorNull = { + x: 3, + y: 300, + error: 30, + error2: null, + }; + expect(getDomainOfErrorBars([...data, errorNull], scatter, 'x', undefined, 'xAxis')).toEqual([-14, 17]); + }); + }); }); describe('exports for backwards-compatibility', () => {