@cdc/chart 4.24.10 → 4.24.12-2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/dist/cdcchart.js +35019 -34301
  2. package/examples/feature/boxplot/boxplot-data.json +88 -22
  3. package/examples/feature/boxplot/boxplot.json +540 -16
  4. package/examples/feature/boxplot/testing.csv +7 -7
  5. package/examples/feature/sankey/sankey-example-data.json +126 -14
  6. package/examples/feature/tests-date-exclusions/date-exclusions-config.json +372 -12
  7. package/examples/private/DEV-8850-2.json +493 -0
  8. package/examples/private/DEV-9822.json +574 -0
  9. package/examples/private/DEV-9840.json +553 -0
  10. package/examples/private/DEV-9850-3.json +461 -0
  11. package/examples/private/chart.json +1084 -0
  12. package/examples/private/ci_formatted.json +202 -0
  13. package/examples/private/ci_issue.json +3016 -0
  14. package/examples/private/completed.json +634 -0
  15. package/examples/private/dem-data-long.csv +20 -0
  16. package/examples/private/dem-data-long.json +36 -0
  17. package/examples/private/demographic_data.csv +157 -0
  18. package/examples/private/demographic_data.json +2654 -0
  19. package/examples/private/demographic_dynamic.json +443 -0
  20. package/examples/private/demographic_standard.json +560 -0
  21. package/examples/private/ehdi.json +29939 -0
  22. package/examples/private/test.json +493 -0
  23. package/index.html +10 -7
  24. package/package.json +2 -2
  25. package/src/CdcChart.tsx +132 -152
  26. package/src/_stories/Chart.Anchors.stories.tsx +31 -0
  27. package/src/_stories/Chart.CustomColors.stories.tsx +19 -0
  28. package/src/_stories/Chart.DynamicSeries.stories.tsx +34 -0
  29. package/src/_stories/Chart.Legend.Gradient.stories.tsx +42 -1
  30. package/src/_stories/Chart.stories.tsx +37 -6
  31. package/src/_stories/ChartAxisLabels.stories.tsx +4 -1
  32. package/src/_stories/ChartEditor.stories.tsx +27 -0
  33. package/src/_stories/ChartLine.Suppression.stories.tsx +25 -0
  34. package/src/_stories/ChartPrefixSuffix.stories.tsx +8 -0
  35. package/{examples/feature/area/area-chart-date-city-temperature.json → src/_stories/_mock/area_chart_stacked.json} +125 -27
  36. package/src/_stories/_mock/boxplot_multiseries.json +647 -0
  37. package/src/_stories/_mock/dynamic_series_bar_config.json +723 -0
  38. package/src/_stories/_mock/dynamic_series_config.json +979 -0
  39. package/src/_stories/_mock/line_chart_dynamic_ci.json +493 -0
  40. package/src/_stories/_mock/line_chart_non_dynamic_ci.json +522 -0
  41. package/{examples/feature/scatterplot/scatterplot.json → src/_stories/_mock/scatterplot_mock.json} +62 -92
  42. package/src/_stories/_mock/short_dates.json +288 -0
  43. package/src/_stories/_mock/suppression_mock.json +1549 -0
  44. package/src/components/AreaChart/components/AreaChart.Stacked.jsx +15 -3
  45. package/src/components/Axis/Categorical.Axis.tsx +2 -2
  46. package/src/components/BarChart/components/BarChart.Horizontal.tsx +46 -37
  47. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +43 -9
  48. package/src/components/BarChart/components/BarChart.Vertical.tsx +53 -47
  49. package/src/components/BarChart/helpers/getBarData.ts +28 -0
  50. package/src/components/BarChart/helpers/index.ts +1 -2
  51. package/src/components/BarChart/helpers/tests/getBarData.test.ts +74 -0
  52. package/src/components/BoxPlot/BoxPlot.tsx +131 -0
  53. package/src/components/BoxPlot/helpers/index.ts +54 -0
  54. package/src/components/BrushChart.tsx +23 -26
  55. package/src/components/EditorPanel/EditorPanel.tsx +117 -139
  56. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +3 -3
  57. package/src/components/EditorPanel/components/Panels/Panel.BoxPlot.tsx +51 -6
  58. package/src/components/EditorPanel/components/Panels/Panel.Regions.tsx +40 -9
  59. package/src/components/EditorPanel/components/Panels/Panel.Sankey.tsx +3 -3
  60. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +122 -56
  61. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +1 -2
  62. package/src/components/EditorPanel/useEditorPermissions.ts +20 -2
  63. package/src/components/Legend/Legend.Component.tsx +11 -12
  64. package/src/components/Legend/Legend.tsx +16 -16
  65. package/src/components/Legend/helpers/getLegendClasses.ts +59 -0
  66. package/src/components/Legend/helpers/index.ts +2 -1
  67. package/src/components/Legend/tests/getLegendClasses.test.ts +115 -0
  68. package/src/components/LineChart/components/LineChart.Circle.tsx +1 -1
  69. package/src/components/LineChart/helpers.ts +49 -43
  70. package/src/components/LineChart/index.tsx +135 -83
  71. package/src/components/LinearChart.tsx +196 -181
  72. package/src/components/PieChart/PieChart.tsx +7 -1
  73. package/src/components/Sankey/components/ColumnList.tsx +19 -0
  74. package/src/components/Sankey/components/Sankey.tsx +479 -0
  75. package/src/components/Sankey/helpers/getSankeyTooltip.tsx +33 -0
  76. package/src/components/Sankey/index.tsx +1 -492
  77. package/src/components/Sankey/sankey.scss +22 -21
  78. package/src/components/Sankey/types/index.ts +1 -1
  79. package/src/components/Sankey/useSankeyAlert.tsx +60 -0
  80. package/src/components/ScatterPlot/ScatterPlot.jsx +20 -4
  81. package/src/data/initial-state.js +7 -12
  82. package/src/helpers/countNumOfTicks.ts +57 -0
  83. package/src/helpers/getQuartiles.ts +15 -18
  84. package/src/hooks/useMinMax.ts +44 -16
  85. package/src/hooks/useReduceData.ts +43 -10
  86. package/src/hooks/useScales.ts +90 -35
  87. package/src/hooks/useTooltip.tsx +59 -50
  88. package/src/scss/DataTable.scss +5 -0
  89. package/src/scss/main.scss +6 -20
  90. package/src/types/ChartConfig.ts +6 -19
  91. package/src/types/ChartContext.ts +4 -1
  92. package/src/types/ForestPlot.ts +8 -0
  93. package/src/components/BoxPlot/BoxPlot.jsx +0 -111
  94. package/src/hooks/useLegendClasses.ts +0 -72
@@ -49,7 +49,7 @@ export default {
49
49
  labelColor: '#333',
50
50
  tickLabelColor: '#333',
51
51
  tickColor: '#333',
52
- rightHideAxis: true,
52
+ rightHideAxis: false,
53
53
  rightAxisSize: 0,
54
54
  rightLabel: '',
55
55
  rightLabelOffsetSize: 0,
@@ -68,15 +68,8 @@ export default {
68
68
  boxplot: {
69
69
  plots: [],
70
70
  borders: 'true',
71
- firstQuartilePercentage: 25,
72
- thirdQuartilePercentage: 75,
73
- boxWidthPercentage: 40,
74
71
  plotOutlierValues: false,
75
72
  plotNonOutlierValues: true,
76
- legend: {
77
- showHowToReadText: false,
78
- howToReadText: ''
79
- },
80
73
  labels: {
81
74
  q1: 'Lower Quartile',
82
75
  q2: 'q2',
@@ -88,7 +81,7 @@ export default {
88
81
  median: 'Median',
89
82
  sd: 'Standard Deviation',
90
83
  iqr: 'Interquartile Range',
91
- total: 'Total',
84
+ count: 'Count',
92
85
  outliers: 'Outliers',
93
86
  values: 'Values',
94
87
  lowerBounds: 'Lower Bounds',
@@ -128,7 +121,8 @@ export default {
128
121
  target: 0,
129
122
  maxTickRotation: 0,
130
123
  padding: 5,
131
- showYearsOnce: false
124
+ showYearsOnce: false,
125
+ sortByRecentDate: false
132
126
  },
133
127
  table: {
134
128
  label: 'Data Table',
@@ -177,10 +171,11 @@ export default {
177
171
  hideBorder: {
178
172
  side: false,
179
173
  topBottom: true
180
- }
174
+ },
175
+ position: 'right'
181
176
  },
182
177
  brush: {
183
- height: 25,
178
+ height: 45,
184
179
  active: false
185
180
  },
186
181
  exclusions: {
@@ -0,0 +1,57 @@
1
+ export const countNumOfTicks = ({ axis, max, runtime, currentViewport, isHorizontal, data, config, min }) => {
2
+ let { numTicks } = runtime[axis]
3
+ if (runtime[axis].viewportNumTicks && runtime[axis].viewportNumTicks[currentViewport]) {
4
+ numTicks = runtime[axis].viewportNumTicks[currentViewport]
5
+ }
6
+ let tickCount = undefined
7
+
8
+ if (axis === 'yAxis') {
9
+ tickCount =
10
+ isHorizontal && !numTicks
11
+ ? data.length
12
+ : isHorizontal && numTicks
13
+ ? numTicks
14
+ : !isHorizontal && !numTicks
15
+ ? undefined
16
+ : !isHorizontal && numTicks && numTicks
17
+ // to fix edge case of small numbers with decimals
18
+ if (tickCount === undefined && !config.dataFormat.roundTo) {
19
+ // then it is set to Auto
20
+ if (Number(max) <= 3) {
21
+ tickCount = 2
22
+ } else {
23
+ tickCount = 4 // same default as standalone components
24
+ }
25
+ }
26
+ if (Number(tickCount) > Number(max) && !isHorizontal) {
27
+ // cap it and round it so its an integer
28
+ tickCount = Number(min) < 0 ? Math.round(max) * 2 : Math.round(max)
29
+ }
30
+ }
31
+
32
+ if (axis === 'xAxis') {
33
+ tickCount =
34
+ isHorizontal && !numTicks
35
+ ? undefined
36
+ : isHorizontal && numTicks
37
+ ? numTicks
38
+ : !isHorizontal && !numTicks
39
+ ? undefined
40
+ : !isHorizontal && numTicks && numTicks
41
+ if (isHorizontal && tickCount === undefined && !config.dataFormat.roundTo) {
42
+ // then it is set to Auto
43
+ // - check for small numbers situation
44
+ if (max <= 3) {
45
+ tickCount = 2
46
+ } else {
47
+ tickCount = 4 // same default as standalone components
48
+ }
49
+ }
50
+
51
+ if (config.visualizationType === 'Forest Plot') {
52
+ tickCount = config.yAxis.numTicks !== '' ? config.yAxis.numTicks : 4
53
+ }
54
+ }
55
+
56
+ return tickCount
57
+ }
@@ -4,27 +4,24 @@
4
4
  * @param {Array} arr - The array of integers or decimals.
5
5
  * @returns {Object} An object containing the q1 and q3 values.
6
6
  */
7
- export const getQuartiles = arr => {
8
- arr.sort((a, b) => a - b)
7
+ import _ from 'lodash'
9
8
 
10
- // Calculate the index of the median value of the array
11
- const medianIndex = Math.floor(arr.length / 2)
9
+ export const getQuartiles = (values: number[]): { q1: number; q3: number } => {
10
+ const sortedData: number[] = _.sortBy(values)
12
11
 
13
- // Check if the length of the array is even or odd
14
- const isEvenLength = arr.length % 2 === 0
12
+ const quantile = (sortedData: number[], q: number): number => {
13
+ const position: number = (sortedData.length - 1) * q
14
+ const base: number = Math.floor(position)
15
+ const rest: number = position - base
16
+ if (sortedData[base + 1] !== undefined) {
17
+ return sortedData[base] + rest * (sortedData[base + 1] - sortedData[base])
18
+ } else {
19
+ return sortedData[base]
20
+ }
21
+ }
15
22
 
16
- // Split the array into two subarrays based on the median index
17
- const q1Array = isEvenLength ? arr.slice(0, medianIndex) : arr.slice(0, medianIndex + 1)
18
- const q3Array = isEvenLength ? arr.slice(medianIndex) : arr.slice(medianIndex + 1)
23
+ const q1: number = quantile(sortedData, 0.25)
24
+ const q3: number = quantile(sortedData, 0.75)
19
25
 
20
- // Calculate the median of the first subarray to get the q1 value
21
- const q1Index = Math.floor(q1Array.length / 2)
22
- const q1 = isEvenLength ? (q1Array[q1Index - 1] + q1Array[q1Index]) / 2 : q1Array[q1Index]
23
-
24
- // Calculate the median of the second subarray to get the q3 value
25
- const q3Index = Math.floor(q3Array.length / 2)
26
- const q3 = isEvenLength ? (q3Array[q3Index - 1] + q3Array[q3Index]) / 2 : q3Array[q3Index]
27
-
28
- // Return an object containing the q1 and q3 values
29
26
  return { q1, q3 }
30
27
  }
@@ -37,21 +37,25 @@ const useMinMax = ({ config, minValue, maxValue, existPositiveValue, data, isAll
37
37
 
38
38
  const { visualizationType, series } = config
39
39
  const { max: enteredMaxValue, min: enteredMinValue } = config.runtime.yAxis
40
- const minRequiredCIPadding = 1.15 // regardless of Editor if CI data, there must be 10% padding added
40
+ const paddingAddedToAxis = config.yAxis.enablePadding ? 1 + config.yAxis.scalePadding / 100 : 1
41
41
  const isLogarithmicAxis = config.yAxis.type === 'logarithmic'
42
42
  // do validation bafore applying t0 charts
43
- const isMaxValid = existPositiveValue ? enteredMaxValue >= maxValue : enteredMaxValue >= 0
44
- const isMinValid = isLogarithmicAxis ? enteredMinValue >= 0 : (enteredMinValue <= 0 && minValue >= 0) || (enteredMinValue <= minValue && minValue < 0)
43
+ const isMaxValid = existPositiveValue ? Number(enteredMaxValue) >= maxValue : Number(enteredMaxValue) >= 0
44
+ const isMinValid = isLogarithmicAxis
45
+ ? Number(enteredMinValue) >= 0
46
+ : (Number(enteredMinValue) <= 0 && minValue >= 0) || (Number(enteredMinValue) <= minValue && minValue < 0)
45
47
 
46
- min = enteredMinValue && isMinValid ? enteredMinValue : minValue
47
- max = enteredMaxValue && isMaxValid ? enteredMaxValue : Number.MIN_VALUE
48
+ min = enteredMinValue && isMinValid ? Number(enteredMinValue) : minValue
49
+ max = enteredMaxValue && isMaxValid ? Number(enteredMaxValue) : Number.MIN_VALUE
48
50
 
49
51
  const { lower, upper } = config?.confidenceKeys || {}
50
52
 
51
53
  if (lower && upper && config.visualizationType === 'Bar') {
52
54
  const buffer = min < 0 ? 1.1 : 0
53
- max = Math.max(maxValue, Math.max(...data.flatMap(d => [d[upper], d[lower]])) * 1.15)
54
- min = Math.min(minValue, Math.min(...data.flatMap(d => [d[upper], d[lower]])) * 1.15) * buffer
55
+ const maxValueWithCI = Math.max(...data.flatMap(d => [d[upper], d[lower]])) * paddingAddedToAxis
56
+ const minValueWithCIPlusBuffer = Math.min(...data.flatMap(d => [d[upper], d[lower]])) * paddingAddedToAxis * buffer
57
+ max = max > maxValueWithCI ? max : maxValueWithCI
58
+ min = min < minValueWithCIPlusBuffer ? min : minValueWithCIPlusBuffer
55
59
  }
56
60
 
57
61
  if (config.series.filter(s => s?.type === 'Forecasting')) {
@@ -122,8 +126,8 @@ const useMinMax = ({ config, minValue, maxValue, existPositiveValue, data, isAll
122
126
  leftMax = findMaxFromSeriesKeys(data, leftAxisSeriesItems, leftMax, 'left')
123
127
  rightMax = findMaxFromSeriesKeys(data, rightAxisSeriesItems, rightMax, 'right')
124
128
 
125
- if (leftMax < enteredMaxValue) {
126
- leftMax = enteredMaxValue
129
+ if (leftMax < Number(enteredMaxValue)) {
130
+ leftMax = Number(enteredMaxValue)
127
131
  }
128
132
  } catch (e) {
129
133
  console.error(e.message)
@@ -131,10 +135,18 @@ const useMinMax = ({ config, minValue, maxValue, existPositiveValue, data, isAll
131
135
  }
132
136
 
133
137
  // this should not apply to bar charts if there is negative CI data
134
- if ((visualizationType === 'Bar' || checkLineToBarGraph() || (visualizationType === 'Combo' && !isAllLine)) && min > 0) {
138
+ if (
139
+ (visualizationType === 'Bar' || checkLineToBarGraph() || (visualizationType === 'Combo' && !isAllLine)) &&
140
+ min > 0
141
+ ) {
135
142
  min = 0
136
143
  }
137
- if ((config.visualizationType === 'Bar' || checkLineToBarGraph() || (config.visualizationType === 'Combo' && !isAllLine)) && min < 0) {
144
+ if (
145
+ (config.visualizationType === 'Bar' ||
146
+ checkLineToBarGraph() ||
147
+ (config.visualizationType === 'Combo' && !isAllLine)) &&
148
+ min < 0
149
+ ) {
138
150
  min = min * 1.1
139
151
  }
140
152
 
@@ -143,18 +155,22 @@ const useMinMax = ({ config, minValue, maxValue, existPositiveValue, data, isAll
143
155
  min = 0
144
156
  }
145
157
  if (enteredMinValue) {
146
- const isMinValid = isLogarithmicAxis ? enteredMinValue >= 0 && enteredMinValue < minValue : enteredMinValue < minValue
147
- min = enteredMinValue && isMinValid ? enteredMinValue : minValue
158
+ const isMinValid = isLogarithmicAxis
159
+ ? Number(enteredMinValue) >= 0 && Number(enteredMinValue) < minValue
160
+ : Number(enteredMinValue) < minValue
161
+ min = Number(enteredMinValue) && isMinValid ? Number(enteredMinValue) : minValue
148
162
  }
149
163
  }
150
164
 
151
165
  if (config.visualizationType === 'Deviation Bar' && min > 0) {
152
166
  const isMinValid = Number(enteredMinValue) < Math.min(minValue, Number(config.xAxis.target))
153
- min = enteredMinValue && isMinValid ? enteredMinValue : 0
167
+ min = Number(enteredMinValue) && isMinValid ? Number(enteredMinValue) : 0
154
168
  }
155
169
 
156
170
  if (config.visualizationType === 'Line' && !checkLineToBarGraph()) {
157
- const isMinValid = isLogarithmicAxis ? enteredMinValue >= 0 && enteredMinValue < minValue : enteredMinValue < minValue
171
+ const isMinValid = isLogarithmicAxis
172
+ ? Number(enteredMinValue) >= 0 && Number(enteredMinValue) < minValue
173
+ : Number(enteredMinValue) < minValue
158
174
  // update minValue for (0) Suppression points
159
175
  const suppressedMinValue = tableData?.some((dataItem, index) => {
160
176
  return config.preliminaryData?.some(pd => {
@@ -171,7 +187,15 @@ const useMinMax = ({ config, minValue, maxValue, existPositiveValue, data, isAll
171
187
  return valueMatch && (index === 0 || index === tableData.length - 1)
172
188
  })
173
189
  })
174
- min = enteredMinValue && isMinValid ? enteredMinValue : suppressedMinValue ? 0 : minValue
190
+ let isCategoricalAxis = config.yAxis.type === 'categorical'
191
+ min =
192
+ enteredMinValue !== '' && isMinValid
193
+ ? Number(enteredMinValue)
194
+ : suppressedMinValue
195
+ ? 0
196
+ : isCategoricalAxis
197
+ ? 0
198
+ : minValue
175
199
  }
176
200
  //If data value max wasn't provided, calculate it
177
201
  if (max === Number.MIN_VALUE) {
@@ -212,6 +236,10 @@ const useMinMax = ({ config, minValue, maxValue, existPositiveValue, data, isAll
212
236
  }
213
237
  }
214
238
 
239
+ if (config.visualizationType === 'Area Chart' && config.visualizationSubType === 'stacked') {
240
+ min = 0
241
+ }
242
+
215
243
  return { min, max, leftMax, rightMax }
216
244
  }
217
245
  export default useMinMax
@@ -3,12 +3,28 @@ import isNumber from '@cdc/core/helpers/isNumber'
3
3
  function useReduceData(config, data) {
4
4
  const isBar = config.series.every(({ type }) => type === 'Bar')
5
5
  const isAllLine = config.series.every(({ type }) => ['Line', 'dashed-sm', 'dashed-md', 'dashed-lg'].includes(type))
6
- const sumYValues = seriesKeys => xValue => seriesKeys.reduce((yTotal, k) => (isNaN(Number(xValue[k])) ? yTotal : yTotal + Number(xValue[k])), 0)
7
-
6
+ const sumYValues = seriesKeys => xValue =>
7
+ seriesKeys.reduce((yTotal, k) => (isNaN(Number(xValue[k])) ? yTotal : yTotal + Number(xValue[k])), 0)
8
+ const getSeriesKey = seriesKey => {
9
+ const series = config.runtime.series.find(item => item.dataKey === seriesKey)
10
+ return series?.dynamicCategory ? series.originalDataKey : seriesKey
11
+ }
8
12
  const getMaxValueFromData = () => {
9
- let max = Math.max(...data.map(d => Math.max(...config.runtime.seriesKeys.map(key => (isNumber(d[key]) ? Number(cleanChars(d[key])) : 0)))))
10
-
11
- if ((config.visualizationType === 'Bar' || (config.visualizationType === 'Combo' && isBar)) && config.visualizationSubType === 'stacked') {
13
+ let max = Math.max(
14
+ ...data.map(d =>
15
+ Math.max(
16
+ ...config.runtime.seriesKeys.map(key => {
17
+ const seriesKey = getSeriesKey(key)
18
+ return isNumber(d[seriesKey]) ? Number(cleanChars(d[seriesKey])) : 0
19
+ })
20
+ )
21
+ )
22
+ )
23
+
24
+ if (
25
+ (config.visualizationType === 'Bar' || (config.visualizationType === 'Combo' && isBar)) &&
26
+ config.visualizationSubType === 'stacked'
27
+ ) {
12
28
  const yTotals = data.map(sumYValues(config.runtime.seriesKeys)).filter(num => !isNaN(num))
13
29
  max = Math.max(...yTotals)
14
30
  }
@@ -18,15 +34,23 @@ function useReduceData(config, data) {
18
34
  max = Math.max(...yTotals)
19
35
  }
20
36
 
21
- if ((config.visualizationType === 'Bar' || config.visualizationType === 'Deviation Bar') && config.series && config.series.dataKey) {
22
- max = Math.max(...data.map(d => (isNumber(d[config.series.dataKey]) ? Number(cleanChars(d[config.series.dataKey])) : 0)))
37
+ if (
38
+ (config.visualizationType === 'Bar' || config.visualizationType === 'Deviation Bar') &&
39
+ config.series &&
40
+ config.series.dataKey
41
+ ) {
42
+ max = Math.max(
43
+ ...data.map(d => (isNumber(d[config.series.dataKey]) ? Number(cleanChars(d[config.series.dataKey])) : 0))
44
+ )
23
45
  }
24
46
 
25
47
  if (config.visualizationType === 'Combo' && config.visualizationSubType === 'stacked' && !isBar) {
26
48
  if (config.runtime.barSeriesKeys && config.runtime.lineSeriesKeys) {
27
49
  const yTotals = data.map(sumYValues(config.runtime.barSeriesKeys))
28
50
 
29
- const lineMax = Math.max(...data.map(d => Math.max(...config.runtime.lineSeriesKeys.map(key => Number(cleanChars(d[key]))))))
51
+ const lineMax = Math.max(
52
+ ...data.map(d => Math.max(...config.runtime.lineSeriesKeys.map(key => Number(cleanChars(d[key])))))
53
+ )
30
54
  const barMax = Math.max(...yTotals)
31
55
 
32
56
  max = Math.max(barMax, lineMax)
@@ -37,7 +61,16 @@ function useReduceData(config, data) {
37
61
  }
38
62
 
39
63
  const getMinValueFromData = () => {
40
- const minNumberFromData = Math.min(...data.map(d => Math.min(...config.runtime.seriesKeys.map(key => (isNumber(d[key]) ? Number(cleanChars(d[key])) : Infinity)))))
64
+ const minNumberFromData = Math.min(
65
+ ...data.map(d =>
66
+ Math.min(
67
+ ...config.runtime.seriesKeys.map(key => {
68
+ const seriesKey = getSeriesKey(key)
69
+ return isNumber(d[seriesKey]) ? Number(cleanChars(d[seriesKey])) : Infinity
70
+ })
71
+ )
72
+ )
73
+ )
41
74
 
42
75
  return String(minNumberFromData)
43
76
  }
@@ -46,7 +79,7 @@ function useReduceData(config, data) {
46
79
  if (!config.runtime.seriesKeys) {
47
80
  return false
48
81
  }
49
- return config.runtime.seriesKeys.some(key => data.some(d => d[key] >= 0))
82
+ return config.runtime.seriesKeys.some(key => data.some(d => d[getSeriesKey(key)] >= 0))
50
83
  }
51
84
 
52
85
  const cleanChars = value => {
@@ -1,9 +1,19 @@
1
- import { LogScaleConfig, scaleBand, scaleLinear, scaleLog, scalePoint, scaleTime } from '@visx/scale'
1
+ import {
2
+ LinearScaleConfig,
3
+ LogScaleConfig,
4
+ scaleBand,
5
+ scaleLinear,
6
+ scaleLog,
7
+ scalePoint,
8
+ scaleTime,
9
+ getTicks
10
+ } from '@visx/scale'
2
11
  import { useContext } from 'react'
3
12
  import ConfigContext from '../ConfigContext'
4
13
  import { ChartConfig } from '../types/ChartConfig'
5
14
  import { ChartContext } from '../types/ChartContext'
6
- import * as d3 from 'd3'
15
+ import _ from 'lodash'
16
+
7
17
  const scaleTypes = {
8
18
  TIME: 'time',
9
19
  LOG: 'log',
@@ -31,8 +41,7 @@ const useScales = (properties: useScaleProps) => {
31
41
  const seriesDomain = config.runtime.barSeriesKeys || config.runtime.seriesKeys
32
42
  const xAxisType = config.runtime.xAxis.type
33
43
  const isHorizontal = config.orientation === 'horizontal'
34
-
35
- const { visualizationType } = config
44
+ const { visualizationType, xAxis, forestPlot } = config
36
45
 
37
46
  // define scales
38
47
  let xScale = null
@@ -64,11 +73,11 @@ const useScales = (properties: useScaleProps) => {
64
73
 
65
74
  // handle Linear scaled viz
66
75
  if (config.xAxis.type === 'date' && !isHorizontal) {
67
- const xAxisDataMappedSorted = xAxisDataMapped ? xAxisDataMapped.sort() : []
76
+ const xAxisDataMappedSorted = sortXAxisData(xAxisDataMapped, config.xAxis.sortByRecentDate)
68
77
  xScale = composeScaleBand(xAxisDataMappedSorted, [0, xMax], 1 - config.barThickness)
69
78
  }
70
79
 
71
- if (config.xAxis.type === 'date-time') {
80
+ if (xAxis.type === 'date-time' || xAxis.type === 'continuous') {
72
81
  let xAxisMin = Math.min(...xAxisDataMapped.map(Number))
73
82
  let xAxisMax = Math.max(...xAxisDataMapped.map(Number))
74
83
  xAxisMin -= (config.xAxis.padding ? config.xAxis.padding * 0.01 : 0) * (xAxisMax - xAxisMin)
@@ -76,15 +85,17 @@ const useScales = (properties: useScaleProps) => {
76
85
  visualizationType === 'Line'
77
86
  ? 0
78
87
  : (config.xAxis.padding ? config.xAxis.padding * 0.01 : 0) * (xAxisMax - xAxisMin)
88
+ const range = config.xAxis.sortByRecentDate ? [xMax, 0] : [0, xMax]
79
89
  xScale = scaleTime({
80
90
  domain: [xAxisMin, xAxisMax],
81
- range: [0, xMax]
91
+ range: range
82
92
  })
83
93
 
84
94
  xScale.type = scaleTypes.TIME
85
95
 
86
96
  let minDistance = Number.MAX_VALUE
87
- let xAxisDataMappedSorted = xAxisDataMapped ? xAxisDataMapped.sort() : []
97
+ let xAxisDataMappedSorted = sortXAxisData(xAxisDataMapped, config.xAxis.sortByRecentDate)
98
+
88
99
  for (let i = 0; i < xAxisDataMappedSorted.length - 1; i++) {
89
100
  let distance = xScale(xAxisDataMappedSorted[i + 1]) - xScale(xAxisDataMappedSorted[i])
90
101
 
@@ -106,7 +117,7 @@ const useScales = (properties: useScaleProps) => {
106
117
  range: [0, yMax]
107
118
  })
108
119
  xScale = scaleLinear({
109
- domain: [min * leftOffset, Math.max(Number(config.xAxis.target), max)],
120
+ domain: [min * leftOffset, Math.max(Number(xAxis.target), max)],
110
121
  range: [0, xMax],
111
122
  round: true,
112
123
  nice: true
@@ -116,9 +127,11 @@ const useScales = (properties: useScaleProps) => {
116
127
 
117
128
  // handle Scatter plot
118
129
  if (config.visualizationType === 'Scatter Plot') {
119
- if (config.xAxis.type === 'continuous') {
130
+ if (xAxis.type === 'continuous') {
131
+ let min = xAxis.min ? xAxis.min : Math.min.apply(null, xScale.domain())
132
+ let max = xAxis.max ? xAxis.max : Math.max.apply(null, xScale.domain())
120
133
  xScale = scaleLinear({
121
- domain: [0, Math.max.apply(null, xScale.domain())],
134
+ domain: [min, max],
122
135
  range: [0, xMax]
123
136
  })
124
137
  xScale.type = scaleTypes.LINEAR
@@ -150,19 +163,21 @@ const useScales = (properties: useScaleProps) => {
150
163
  if (highestFence > max) max = highestFence
151
164
 
152
165
  // Set Scales
166
+
167
+ const categories = _.uniq(data.map(d => d[config.xAxis.dataKey]))
168
+ const range = [0, config.barThickness * 100 || 1]
169
+ const domain = _.map(config.series, 'dataKey')
153
170
  yScale = scaleLinear({
154
171
  range: [yMax, 0],
155
172
  round: true,
156
173
  domain: [min, max]
157
174
  })
158
-
159
175
  xScale = scaleBand({
160
176
  range: [0, xMax],
161
- round: true,
162
- domain: config.boxplot.categories,
163
- padding: 0.4
177
+ domain: categories
164
178
  })
165
179
  xScale.type = scaleTypes.BAND
180
+ seriesScale = composeScaleBand(domain, range)
166
181
  }
167
182
 
168
183
  // handle Paired bar
@@ -193,10 +208,10 @@ const useScales = (properties: useScaleProps) => {
193
208
 
194
209
  if (visualizationType === 'Forest Plot') {
195
210
  const resolvedYRange = () => {
196
- if (config.forestPlot.regression.showDiamond || config.forestPlot.regression.description) {
197
- return [0 + config.forestPlot.rowHeight * 2, yMax - config.forestPlot.rowHeight]
211
+ if (forestPlot.regression.showDiamond || forestPlot.regression.description) {
212
+ return [0 + forestPlot.rowHeight * 2, yMax - forestPlot.rowHeight]
198
213
  } else {
199
- return [0 + config.forestPlot.rowHeight * 2, yMax]
214
+ return [0 + forestPlot.rowHeight * 2, yMax]
200
215
  }
201
216
  }
202
217
 
@@ -207,26 +222,26 @@ const useScales = (properties: useScaleProps) => {
207
222
 
208
223
  const xAxisPadding = 5
209
224
 
210
- const leftWidthOffset = (Number(config.forestPlot.leftWidthOffset) / 100) * xMax
211
- const rightWidthOffset = (Number(config.forestPlot.rightWidthOffset) / 100) * xMax
225
+ const leftWidthOffset = (Number(forestPlot.leftWidthOffset) / 100) * xMax
226
+ const rightWidthOffset = (Number(forestPlot.rightWidthOffset) / 100) * xMax
212
227
 
213
- const rightWidthOffsetMobile = (Number(config.forestPlot.rightWidthOffsetMobile) / 100) * xMax
214
- const leftWidthOffsetMobile = (Number(config.forestPlot.leftWidthOffsetMobile) / 100) * xMax
228
+ const rightWidthOffsetMobile = (Number(forestPlot.rightWidthOffsetMobile) / 100) * xMax
229
+ const leftWidthOffsetMobile = (Number(forestPlot.leftWidthOffsetMobile) / 100) * xMax
215
230
 
216
231
  if (screenWidth > 480) {
217
- if (config.forestPlot.type === 'Linear') {
232
+ if (forestPlot.type === 'Linear') {
218
233
  xScale = scaleLinear({
219
234
  domain: [
220
- Math.min(...data.map(d => parseFloat(d[config.forestPlot.lower]))) - xAxisPadding,
221
- Math.max(...data.map(d => parseFloat(d[config.forestPlot.upper]))) + xAxisPadding
235
+ Math.min(...data.map(d => parseFloat(d[forestPlot.lower]))) - xAxisPadding,
236
+ Math.max(...data.map(d => parseFloat(d[forestPlot.upper]))) + xAxisPadding
222
237
  ],
223
238
  range: [leftWidthOffset, Number(screenWidth) - rightWidthOffset]
224
239
  })
225
240
  xScale.type = scaleTypes.LINEAR
226
241
  }
227
- if (config.forestPlot.type === 'Logarithmic') {
228
- let max = Math.max(...data.map(d => parseFloat(d[config.forestPlot.upper])))
229
- let fp_min = Math.min(...data.map(d => parseFloat(d[config.forestPlot.lower])))
242
+ if (forestPlot.type === 'Logarithmic') {
243
+ let max = Math.max(...data.map(d => parseFloat(d[forestPlot.upper])))
244
+ let fp_min = Math.min(...data.map(d => parseFloat(d[forestPlot.lower])))
230
245
 
231
246
  xScale = scaleLog<LogScaleConfig>({
232
247
  domain: [fp_min, max],
@@ -236,20 +251,20 @@ const useScales = (properties: useScaleProps) => {
236
251
  xScale.type = scaleTypes.LOG
237
252
  }
238
253
  } else {
239
- if (config.forestPlot.type === 'Linear') {
240
- xScale = scaleLinear({
254
+ if (forestPlot.type === 'Linear') {
255
+ xScale = scaleLinear<LinearScaleConfig>({
241
256
  domain: [
242
- Math.min(...data.map(d => parseFloat(d[config.forestPlot.lower]))) - xAxisPadding,
243
- Math.max(...data.map(d => parseFloat(d[config.forestPlot.upper]))) + xAxisPadding
257
+ Math.min(...data.map(d => parseFloat(d[forestPlot.lower]))) - xAxisPadding,
258
+ Math.max(...data.map(d => parseFloat(d[forestPlot.upper]))) + xAxisPadding
244
259
  ],
245
260
  range: [leftWidthOffsetMobile, xMax - rightWidthOffsetMobile],
246
261
  type: scaleTypes.LINEAR
247
262
  })
248
263
  }
249
264
 
250
- if (config.forestPlot.type === 'Logarithmic') {
251
- let max = Math.max(...data.map(d => parseFloat(d[config.forestPlot.upper])))
252
- let fp_min = Math.min(...data.map(d => parseFloat(d[config.forestPlot.lower])))
265
+ if (forestPlot.type === 'Logarithmic') {
266
+ let max = Math.max(...data.map(d => parseFloat(d[forestPlot.upper])))
267
+ let fp_min = Math.min(...data.map(d => parseFloat(d[forestPlot.lower])))
253
268
 
254
269
  xScale = scaleLog<LogScaleConfig>({
255
270
  domain: [fp_min, max],
@@ -324,6 +339,28 @@ export const getTickValues = (xAxisDataMapped, xScale, num, config) => {
324
339
  }
325
340
  }
326
341
 
342
+ // Ensure that the last tick is shown for charts with a "Date (Linear Scale)" scale
343
+ export const filterAndShiftLinearDateTicks = (config, axisProps, xAxisDataMapped, formatDate) => {
344
+ let ticks = axisProps.ticks
345
+ const filteredTickValues = getTicks(axisProps.scale, axisProps.numTicks)
346
+ if (filteredTickValues.length < xAxisDataMapped.length) {
347
+ let shift = 0
348
+ const lastIdx = xAxisDataMapped.indexOf(filteredTickValues[filteredTickValues.length - 1])
349
+ if (lastIdx < xAxisDataMapped.length - 1) {
350
+ shift = !config.xAxis.sortByRecentDate
351
+ ? xAxisDataMapped.length - 1 - lastIdx
352
+ : xAxisDataMapped.indexOf(filteredTickValues[0]) * -1
353
+ }
354
+ ticks = filteredTickValues.map(value => {
355
+ return axisProps.ticks[axisProps.ticks.findIndex(tick => tick.value === value) + shift]
356
+ })
357
+ }
358
+ ticks.forEach((tick, i) => {
359
+ tick.formattedValue = formatDate(tick.value, i, ticks)
360
+ })
361
+ return ticks
362
+ }
363
+
327
364
  /// helper functions
328
365
  const composeXScale = ({ min, max, xMax, config }) => {
329
366
  // Adjust min value if using logarithmic scale
@@ -386,3 +423,21 @@ const composeScaleBand = (domain, range, padding = 0) => {
386
423
  padding: padding
387
424
  })
388
425
  }
426
+
427
+ const sortXAxisData = (xAxisData, sortByRecentDate) => {
428
+ if (!xAxisData || xAxisData.length === 0) {
429
+ return []
430
+ }
431
+
432
+ // Check if the array has only one item
433
+ if (xAxisData.length === 1) {
434
+ return xAxisData
435
+ }
436
+ if (sortByRecentDate) {
437
+ // Sort from newest to oldes (recent dates first)
438
+ return xAxisData.sort((a, b) => Number(b) - Number(a))
439
+ } else {
440
+ // Sort from oldest to newest
441
+ return xAxisData.sort((a, b) => Number(a) - Number(b))
442
+ }
443
+ }