@cdc/chart 4.23.6 → 4.23.8
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.
- package/dist/cdcchart.js +29981 -29995
- package/examples/feature/__data__/area-chart-date-apple.json +5122 -0
- package/examples/feature/__data__/city-temperature.json +2198 -0
- package/examples/feature/__data__/planet-example-data.json +1 -1
- package/examples/feature/area/area-chart-category.json +45 -45
- package/examples/feature/area/area-chart-date-apple.json +10376 -0
- package/examples/feature/area/area-chart-date-city-temperature.json +4528 -0
- package/examples/feature/area/area-chart-date.json +111 -3
- package/examples/feature/combo/right-issues.json +1 -1
- package/examples/feature/forecasting/combo-forecasting.json +72 -46
- package/examples/feature/forecasting/effective_reproduction.json +57 -8
- package/examples/feature/forecasting/forecasting.json +12 -3
- package/examples/feature/forest-plot/broken.json +700 -0
- package/examples/feature/forest-plot/data.csv +24 -0
- package/examples/feature/forest-plot/forest-plot.json +717 -0
- package/examples/feature/line/line-chart.json +11 -11
- package/examples/feature/pie/planet-pie-example-config.json +1 -1
- package/examples/gallery/bar-chart-vertical/vertical-bar-chart-categorical.json +167 -20
- package/examples/private/confidence_interval_test.json +248 -0
- package/examples/private/tooltip-issue.json +45275 -0
- package/index.html +13 -11
- package/package.json +4 -3
- package/src/CdcChart.jsx +78 -27
- package/src/components/AreaChart.jsx +65 -151
- package/src/components/BarChart.Horizontal.jsx +251 -0
- package/src/components/BarChart.StackedHorizontal.jsx +118 -0
- package/src/components/BarChart.StackedVertical.jsx +93 -0
- package/src/components/BarChart.Vertical.jsx +204 -0
- package/src/components/BarChart.jsx +17 -667
- package/src/components/BarChartType.jsx +15 -0
- package/src/components/BrushHandle.jsx +17 -0
- package/src/components/DataTable.jsx +67 -22
- package/src/components/EditorPanel.jsx +426 -358
- package/src/components/Forecasting.jsx +23 -86
- package/src/components/ForestPlot.jsx +191 -0
- package/src/components/ForestPlotSettings.jsx +508 -0
- package/src/components/Legend.jsx +10 -8
- package/src/components/LineChart.jsx +31 -6
- package/src/components/LinearChart.jsx +317 -230
- package/src/components/Series.jsx +40 -4
- package/src/data/initial-state.js +50 -3
- package/src/hooks/useBarChart.js +186 -0
- package/src/hooks/useEditorPermissions.js +218 -0
- package/src/hooks/useMinMax.js +18 -5
- package/src/hooks/useRightAxis.js +2 -1
- package/src/hooks/useScales.js +45 -2
- package/src/hooks/useTooltip.jsx +407 -0
- package/src/scss/main.scss +11 -17
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
import React, { useContext, memo, useState, useEffect } from 'react'
|
|
2
|
+
import ConfigContext from '../ConfigContext'
|
|
3
|
+
import { useDebounce } from 'use-debounce'
|
|
4
|
+
import WarningImage from '../images/warning.svg'
|
|
5
|
+
|
|
6
|
+
import { AccordionItem, AccordionItemHeading, AccordionItemPanel, AccordionItemButton } from 'react-accessible-accordion'
|
|
7
|
+
|
|
8
|
+
const Select = memo(({ label, value, options, fieldName, section = null, subsection = null, required = false, tooltip, updateField, initial: initialValue, ...attributes }) => {
|
|
9
|
+
let optionsJsx = options.map((optionName, index) => (
|
|
10
|
+
<option value={optionName} key={index}>
|
|
11
|
+
{optionName}
|
|
12
|
+
</option>
|
|
13
|
+
))
|
|
14
|
+
|
|
15
|
+
if (initialValue) {
|
|
16
|
+
optionsJsx.unshift(
|
|
17
|
+
<option value='' key='initial'>
|
|
18
|
+
{initialValue}
|
|
19
|
+
</option>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<label>
|
|
25
|
+
<span className='edit-label'>
|
|
26
|
+
{label}
|
|
27
|
+
{tooltip}
|
|
28
|
+
</span>
|
|
29
|
+
<select
|
|
30
|
+
className={required && !value ? 'warning' : ''}
|
|
31
|
+
name={fieldName}
|
|
32
|
+
value={value}
|
|
33
|
+
onChange={event => {
|
|
34
|
+
updateField(section, subsection, fieldName, event.target.value)
|
|
35
|
+
}}
|
|
36
|
+
{...attributes}
|
|
37
|
+
>
|
|
38
|
+
{optionsJsx}
|
|
39
|
+
</select>
|
|
40
|
+
</label>
|
|
41
|
+
)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const CheckBox = memo(({ label, value, fieldName, section = null, subsection = null, tooltip, updateField, ...attributes }) => (
|
|
45
|
+
<label className='checkbox column-heading'>
|
|
46
|
+
<input
|
|
47
|
+
type='checkbox'
|
|
48
|
+
name={fieldName}
|
|
49
|
+
checked={value}
|
|
50
|
+
onChange={e => {
|
|
51
|
+
updateField(section, subsection, fieldName, !value)
|
|
52
|
+
}}
|
|
53
|
+
{...attributes}
|
|
54
|
+
/>
|
|
55
|
+
<span className='edit-label'>
|
|
56
|
+
{label}
|
|
57
|
+
{tooltip}
|
|
58
|
+
</span>
|
|
59
|
+
</label>
|
|
60
|
+
))
|
|
61
|
+
|
|
62
|
+
/* eslint-disable react-hooks/rules-of-hooks */
|
|
63
|
+
const TextField = memo(({ label, tooltip, section = null, subsection = null, fieldName, updateField, value: stateValue, type = 'input', i = null, min = null, ...attributes }) => {
|
|
64
|
+
const [value, setValue] = useState(stateValue)
|
|
65
|
+
|
|
66
|
+
const [debouncedValue] = useDebounce(value, 500)
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if ('string' === typeof debouncedValue && stateValue !== debouncedValue) {
|
|
70
|
+
updateField(section, subsection, fieldName, debouncedValue, i)
|
|
71
|
+
}
|
|
72
|
+
}, [debouncedValue]) // eslint-disable-line
|
|
73
|
+
|
|
74
|
+
let name = subsection ? `${section}-${subsection}-${fieldName}` : `${section}-${subsection}-${fieldName}`
|
|
75
|
+
|
|
76
|
+
const onChange = e => {
|
|
77
|
+
if ('number' !== type || min === null) {
|
|
78
|
+
setValue(e.target.value)
|
|
79
|
+
} else {
|
|
80
|
+
if (!e.target.value || min <= parseFloat(e.target.value)) {
|
|
81
|
+
setValue(e.target.value)
|
|
82
|
+
} else {
|
|
83
|
+
setValue(min.toString())
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let formElement = <input type='text' name={name} onChange={onChange} {...attributes} value={value} />
|
|
89
|
+
|
|
90
|
+
if ('textarea' === type) {
|
|
91
|
+
formElement = <textarea name={name} onChange={onChange} {...attributes} value={value}></textarea>
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if ('number' === type) {
|
|
95
|
+
formElement = <input type='number' name={name} onChange={onChange} {...attributes} value={value} />
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if ('date' === type) {
|
|
99
|
+
formElement = <input type='date' name={name} onChange={onChange} {...attributes} value={value} />
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<label>
|
|
104
|
+
<span className='edit-label column-heading'>
|
|
105
|
+
{label}
|
|
106
|
+
{tooltip}
|
|
107
|
+
</span>
|
|
108
|
+
{formElement}
|
|
109
|
+
</label>
|
|
110
|
+
)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
const ForestPlotSettings = () => {
|
|
114
|
+
const { config, rawData: unfilteredData, updateConfig, isDebug } = useContext(ConfigContext)
|
|
115
|
+
|
|
116
|
+
const enforceRestrictions = updatedConfig => {
|
|
117
|
+
if (updatedConfig.orientation === 'horizontal') {
|
|
118
|
+
updatedConfig.labels = false
|
|
119
|
+
}
|
|
120
|
+
if (updatedConfig.table.show === undefined) {
|
|
121
|
+
updatedConfig.table.show = !isDashboard
|
|
122
|
+
}
|
|
123
|
+
// DEV-3293 - Force combo to always be vertical
|
|
124
|
+
if (updatedConfig.visualizationType === 'Combo') {
|
|
125
|
+
updatedConfig.orientation = 'vertical'
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const getColumns = (filter = true) => {
|
|
130
|
+
let columns = {}
|
|
131
|
+
unfilteredData.forEach(row => {
|
|
132
|
+
Object.keys(row).forEach(columnName => (columns[columnName] = true))
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
if (filter) {
|
|
136
|
+
Object.keys(columns).forEach(key => {
|
|
137
|
+
if (
|
|
138
|
+
(config.series && config.series.filter(series => series.dataKey === key).length > 0) ||
|
|
139
|
+
(config.confidenceKeys && Object.keys(config.confidenceKeys).includes(key))
|
|
140
|
+
/*
|
|
141
|
+
TODO: Resolve errors when config keys exist, but have no value
|
|
142
|
+
Proposal: (((confidenceUpper && confidenceLower) || confidenceUpper || confidenceLower) && Object.keys(config.confidenceKeys).includes(key))
|
|
143
|
+
*/
|
|
144
|
+
) {
|
|
145
|
+
delete columns[key]
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return Object.keys(columns)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const updateField = (section, subsection, fieldName, newValue) => {
|
|
154
|
+
if (section === 'boxplot' && subsection === 'legend') {
|
|
155
|
+
updateConfig({
|
|
156
|
+
...config,
|
|
157
|
+
[section]: {
|
|
158
|
+
...config[section],
|
|
159
|
+
[subsection]: {
|
|
160
|
+
...config.boxplot[subsection],
|
|
161
|
+
[fieldName]: newValue
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (section === 'boxplot' && subsection === 'labels') {
|
|
169
|
+
updateConfig({
|
|
170
|
+
...config,
|
|
171
|
+
[section]: {
|
|
172
|
+
...config[section],
|
|
173
|
+
[subsection]: {
|
|
174
|
+
...config.boxplot[subsection],
|
|
175
|
+
[fieldName]: newValue
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
})
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (section === 'forestPlot' && subsection) {
|
|
183
|
+
updateConfig({
|
|
184
|
+
...config,
|
|
185
|
+
[section]: {
|
|
186
|
+
...config[section],
|
|
187
|
+
[subsection]: {
|
|
188
|
+
...config.forestPlot[subsection],
|
|
189
|
+
[fieldName]: newValue
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
})
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (section === 'columns' && subsection !== '' && fieldName !== '') {
|
|
197
|
+
updateConfig({
|
|
198
|
+
...config,
|
|
199
|
+
[section]: {
|
|
200
|
+
...config[section],
|
|
201
|
+
[subsection]: {
|
|
202
|
+
...config[section][subsection],
|
|
203
|
+
[fieldName]: newValue
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
})
|
|
207
|
+
return
|
|
208
|
+
}
|
|
209
|
+
if (null === section && null === subsection) {
|
|
210
|
+
let updatedConfig = { ...config, [fieldName]: newValue }
|
|
211
|
+
enforceRestrictions(updatedConfig)
|
|
212
|
+
updateConfig(updatedConfig)
|
|
213
|
+
return
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const isArray = Array.isArray(config[section])
|
|
217
|
+
|
|
218
|
+
let sectionValue = isArray ? [...config[section], newValue] : { ...config[section], [fieldName]: newValue }
|
|
219
|
+
|
|
220
|
+
if (null !== subsection) {
|
|
221
|
+
if (isArray) {
|
|
222
|
+
sectionValue = [...config[section]]
|
|
223
|
+
sectionValue[subsection] = { ...sectionValue[subsection], [fieldName]: newValue }
|
|
224
|
+
} else if (typeof newValue === 'string') {
|
|
225
|
+
sectionValue[subsection] = newValue
|
|
226
|
+
} else {
|
|
227
|
+
sectionValue = { ...config[section], [subsection]: { ...config[section][subsection], [fieldName]: newValue } }
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
let updatedConfig = { ...config, [section]: sectionValue }
|
|
232
|
+
|
|
233
|
+
enforceRestrictions(updatedConfig)
|
|
234
|
+
|
|
235
|
+
updateConfig(updatedConfig)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return (
|
|
239
|
+
<AccordionItem>
|
|
240
|
+
<AccordionItemHeading>
|
|
241
|
+
<AccordionItemButton>
|
|
242
|
+
Forest Plot Settings
|
|
243
|
+
{(!config.forestPlot.estimateField || !config.forestPlot.upper || !config.forestPlot.lower) && <WarningImage width='25' className='warning-icon' />}
|
|
244
|
+
</AccordionItemButton>
|
|
245
|
+
</AccordionItemHeading>
|
|
246
|
+
<AccordionItemPanel>
|
|
247
|
+
<TextField type='text' value={config.forestPlot?.title || ''} updateField={updateField} section='forestPlot' fieldName='title' label='Plot Title' />
|
|
248
|
+
|
|
249
|
+
{/* width in center */}
|
|
250
|
+
{/* <label>
|
|
251
|
+
<span className='edit-label column-heading'>Forest Plot Width (%)</span>
|
|
252
|
+
<input
|
|
253
|
+
type='number'
|
|
254
|
+
min={0}
|
|
255
|
+
max={100}
|
|
256
|
+
value={config.forestPlot.width || ''}
|
|
257
|
+
onChange={e => {
|
|
258
|
+
updateConfig({
|
|
259
|
+
...config,
|
|
260
|
+
forestPlot: {
|
|
261
|
+
...config.forestPlot,
|
|
262
|
+
width: e.target.value
|
|
263
|
+
}
|
|
264
|
+
})
|
|
265
|
+
}}
|
|
266
|
+
/>
|
|
267
|
+
</label> */}
|
|
268
|
+
|
|
269
|
+
<label>
|
|
270
|
+
<span className='edit-label column-heading'>Chart Offset Left (%)</span>
|
|
271
|
+
<input
|
|
272
|
+
type='number'
|
|
273
|
+
min={0}
|
|
274
|
+
max={100}
|
|
275
|
+
value={config.forestPlot.leftWidthOffset || 0}
|
|
276
|
+
onChange={e => {
|
|
277
|
+
updateConfig({
|
|
278
|
+
...config,
|
|
279
|
+
forestPlot: {
|
|
280
|
+
...config.forestPlot,
|
|
281
|
+
leftWidthOffset: e.target.value
|
|
282
|
+
}
|
|
283
|
+
})
|
|
284
|
+
}}
|
|
285
|
+
/>
|
|
286
|
+
</label>
|
|
287
|
+
|
|
288
|
+
<label>
|
|
289
|
+
<span className='edit-label column-heading'>Chart Offset Left Mobile(%)</span>
|
|
290
|
+
<input
|
|
291
|
+
type='number'
|
|
292
|
+
min={0}
|
|
293
|
+
max={100}
|
|
294
|
+
value={config.forestPlot.leftWidthOffsetMobile || 0}
|
|
295
|
+
onChange={e => {
|
|
296
|
+
updateConfig({
|
|
297
|
+
...config,
|
|
298
|
+
forestPlot: {
|
|
299
|
+
...config.forestPlot,
|
|
300
|
+
leftWidthOffsetMobile: e.target.value
|
|
301
|
+
}
|
|
302
|
+
})
|
|
303
|
+
}}
|
|
304
|
+
/>
|
|
305
|
+
</label>
|
|
306
|
+
|
|
307
|
+
<label>
|
|
308
|
+
<span className='edit-label column-heading'>Chart Offset Right (%)</span>
|
|
309
|
+
<input
|
|
310
|
+
type='number'
|
|
311
|
+
min={0}
|
|
312
|
+
max={100}
|
|
313
|
+
value={config.forestPlot.rightWidthOffset || 0}
|
|
314
|
+
onChange={e => {
|
|
315
|
+
updateConfig({
|
|
316
|
+
...config,
|
|
317
|
+
forestPlot: {
|
|
318
|
+
...config.forestPlot,
|
|
319
|
+
rightWidthOffset: e.target.value
|
|
320
|
+
}
|
|
321
|
+
})
|
|
322
|
+
}}
|
|
323
|
+
/>
|
|
324
|
+
</label>
|
|
325
|
+
|
|
326
|
+
<label>
|
|
327
|
+
<span className='edit-label column-heading'>Chart Offset Right Mobile(%)</span>
|
|
328
|
+
<input
|
|
329
|
+
type='number'
|
|
330
|
+
min={0}
|
|
331
|
+
max={100}
|
|
332
|
+
value={config.forestPlot.rightWidthOffsetMobile || 0}
|
|
333
|
+
onChange={e => {
|
|
334
|
+
updateConfig({
|
|
335
|
+
...config,
|
|
336
|
+
forestPlot: {
|
|
337
|
+
...config.forestPlot,
|
|
338
|
+
rightWidthOffsetMobile: e.target.value
|
|
339
|
+
}
|
|
340
|
+
})
|
|
341
|
+
}}
|
|
342
|
+
/>
|
|
343
|
+
</label>
|
|
344
|
+
|
|
345
|
+
<Select
|
|
346
|
+
value={config.forestPlot.estimateField}
|
|
347
|
+
label='Point Estimate Column'
|
|
348
|
+
initial={'Select'}
|
|
349
|
+
required={true}
|
|
350
|
+
onChange={e => {
|
|
351
|
+
if (e.target.value !== '' && e.target.value !== 'Select') {
|
|
352
|
+
updateConfig({
|
|
353
|
+
...config,
|
|
354
|
+
forestPlot: {
|
|
355
|
+
...config.forestPlot,
|
|
356
|
+
estimateField: e.target.value
|
|
357
|
+
}
|
|
358
|
+
})
|
|
359
|
+
}
|
|
360
|
+
e.target.value = ''
|
|
361
|
+
}}
|
|
362
|
+
options={getColumns(false)}
|
|
363
|
+
/>
|
|
364
|
+
|
|
365
|
+
<Select
|
|
366
|
+
value={config.forestPlot.lower}
|
|
367
|
+
label='Lower CI Column'
|
|
368
|
+
required={true}
|
|
369
|
+
initial={'Select'}
|
|
370
|
+
onChange={e => {
|
|
371
|
+
if (e.target.value !== '' && e.target.value !== 'Select') {
|
|
372
|
+
updateConfig({
|
|
373
|
+
...config,
|
|
374
|
+
forestPlot: {
|
|
375
|
+
...config.forestPlot,
|
|
376
|
+
lower: e.target.value
|
|
377
|
+
}
|
|
378
|
+
})
|
|
379
|
+
}
|
|
380
|
+
e.target.value = ''
|
|
381
|
+
}}
|
|
382
|
+
options={getColumns(false)}
|
|
383
|
+
/>
|
|
384
|
+
<Select
|
|
385
|
+
value={config.forestPlot.upper}
|
|
386
|
+
label='Upper CI Column'
|
|
387
|
+
initial={'Select'}
|
|
388
|
+
required={true}
|
|
389
|
+
onChange={e => {
|
|
390
|
+
if (e.target.value !== '' && e.target.value !== 'Select') {
|
|
391
|
+
updateConfig({
|
|
392
|
+
...config,
|
|
393
|
+
forestPlot: {
|
|
394
|
+
...config.forestPlot,
|
|
395
|
+
upper: e.target.value
|
|
396
|
+
}
|
|
397
|
+
})
|
|
398
|
+
}
|
|
399
|
+
e.target.value = ''
|
|
400
|
+
}}
|
|
401
|
+
options={getColumns(false)}
|
|
402
|
+
/>
|
|
403
|
+
|
|
404
|
+
<CheckBox value={config.forestPlot.showZeroLine} section='forestPlot' fieldName='showZeroLine' label='Show Line on Zero' updateField={updateField} />
|
|
405
|
+
<Select
|
|
406
|
+
value={config.forestPlot.shape}
|
|
407
|
+
label='Point Estimate Shape'
|
|
408
|
+
initial={'Select'}
|
|
409
|
+
onChange={e => {
|
|
410
|
+
if (e.target.value !== '' && e.target.value !== 'Select') {
|
|
411
|
+
updateConfig({
|
|
412
|
+
...config,
|
|
413
|
+
forestPlot: {
|
|
414
|
+
...config.forestPlot,
|
|
415
|
+
shape: e.target.value
|
|
416
|
+
}
|
|
417
|
+
})
|
|
418
|
+
}
|
|
419
|
+
e.target.value = ''
|
|
420
|
+
}}
|
|
421
|
+
options={['text', 'circle', 'square', 'diamond']}
|
|
422
|
+
/>
|
|
423
|
+
<Select
|
|
424
|
+
value={config.forestPlot.radius.scalingColumn}
|
|
425
|
+
label='Scale Radius Column'
|
|
426
|
+
initial={'Select'}
|
|
427
|
+
onChange={e => {
|
|
428
|
+
if (e.target.value !== '' && e.target.value !== 'Select') {
|
|
429
|
+
updateConfig({
|
|
430
|
+
...config,
|
|
431
|
+
forestPlot: {
|
|
432
|
+
...config.forestPlot,
|
|
433
|
+
radius: {
|
|
434
|
+
...config.forestPlot.radius,
|
|
435
|
+
scalingColumn: e.target.value
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
})
|
|
439
|
+
}
|
|
440
|
+
e.target.value = ''
|
|
441
|
+
}}
|
|
442
|
+
options={getColumns(false)}
|
|
443
|
+
/>
|
|
444
|
+
<label>
|
|
445
|
+
<span className='edit-label column-heading'>Radius Minimum Size</span>
|
|
446
|
+
<input
|
|
447
|
+
min={1}
|
|
448
|
+
max={5}
|
|
449
|
+
value={config.forestPlot.radius.min}
|
|
450
|
+
onChange={e => {
|
|
451
|
+
updateConfig({
|
|
452
|
+
...config,
|
|
453
|
+
forestPlot: {
|
|
454
|
+
...config.forestPlot,
|
|
455
|
+
radius: {
|
|
456
|
+
...config.forestPlot.radius,
|
|
457
|
+
min: Number(e.target.value)
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
})
|
|
461
|
+
}}
|
|
462
|
+
type='number'
|
|
463
|
+
label='Radius Minimum'
|
|
464
|
+
placeholder=' 1'
|
|
465
|
+
/>
|
|
466
|
+
</label>
|
|
467
|
+
<label>
|
|
468
|
+
<span className='edit-label column-heading'>Radius Maximum Size</span>
|
|
469
|
+
<input
|
|
470
|
+
min={5}
|
|
471
|
+
max={10}
|
|
472
|
+
value={config.forestPlot.radius.max}
|
|
473
|
+
onChange={e => {
|
|
474
|
+
updateConfig({
|
|
475
|
+
...config,
|
|
476
|
+
forestPlot: {
|
|
477
|
+
...config.forestPlot,
|
|
478
|
+
radius: {
|
|
479
|
+
...config.forestPlot.radius,
|
|
480
|
+
max: Number(e.target.value)
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
})
|
|
484
|
+
}}
|
|
485
|
+
type='number'
|
|
486
|
+
label='Radius Minimum'
|
|
487
|
+
placeholder=' 1'
|
|
488
|
+
/>
|
|
489
|
+
</label>
|
|
490
|
+
<TextField type='number' min={20} max={45} value={config.forestPlot.rowHeight ? config.forestPlot.rowHeight : 10} updateField={updateField} section='forestPlot' fieldName='rowHeight' label='Row Height' placeholder='10' />
|
|
491
|
+
<br />
|
|
492
|
+
<hr />
|
|
493
|
+
<br />
|
|
494
|
+
<h4>Add Regression Line</h4>
|
|
495
|
+
<TextField type='number' value={config.forestPlot?.regression?.upper || ''} updateField={updateField} section='forestPlot' subsection='regression' fieldName='upper' label='Upper Value' />
|
|
496
|
+
<TextField type='number' value={config.forestPlot?.regression?.lower || ''} updateField={updateField} section='forestPlot' subsection='regression' fieldName='lower' label='Lower Value' />
|
|
497
|
+
<TextField type='number' value={config.forestPlot?.regression?.estimateField || ''} updateField={updateField} section='forestPlot' subsection='regression' fieldName='estimateField' label='Estimate Value' />
|
|
498
|
+
<TextField type='text' value={config.forestPlot?.regression?.baseLineColor || 'black'} updateField={updateField} section='forestPlot' subsection='regression' fieldName='baseLineColor' label='Base Color' />
|
|
499
|
+
<CheckBox value={config.forestPlot?.regression?.showBaseLine || false} section='forestPlot' subsection='regression' fieldName='showBaseLine' label='Show base line' updateField={updateField} />
|
|
500
|
+
<CheckBox value={config.forestPlot?.regression?.showDiamond || false} section='forestPlot' subsection='regression' fieldName='showDiamond' label='Show Diamond' updateField={updateField} />
|
|
501
|
+
<CheckBox value={config.forestPlot?.hideDateCategoryCol || false} section='forestPlot' fieldName='hideDateCategoryCol' label='Hide Date Category Column' updateField={updateField} />
|
|
502
|
+
<TextField type='text' value={config.forestPlot?.regression?.description || ''} updateField={updateField} section='forestPlot' subsection='regression' fieldName='description' label='Description' />
|
|
503
|
+
</AccordionItemPanel>
|
|
504
|
+
</AccordionItem>
|
|
505
|
+
)
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
export default ForestPlotSettings
|
|
@@ -33,6 +33,8 @@ const Legend = () => {
|
|
|
33
33
|
|
|
34
34
|
const { innerClasses, containerClasses } = useLegendClasses(config)
|
|
35
35
|
const { visualizationType, visualizationSubType, series, runtime, orientation } = config
|
|
36
|
+
// create fn to reverse labels while legend is Bottom. Legend-right , legend-left works by default.
|
|
37
|
+
const reverseLabels = labels => (config.legend.reverseLabelOrder && config.legend.position === 'bottom' ? labels.reverse() : labels)
|
|
36
38
|
|
|
37
39
|
const createLegendLabels = defaultLabels => {
|
|
38
40
|
const colorCode = config.legend?.colorCode
|
|
@@ -51,7 +53,7 @@ const Legend = () => {
|
|
|
51
53
|
value: aboveColor
|
|
52
54
|
}
|
|
53
55
|
|
|
54
|
-
return [labelBelow, labelAbove]
|
|
56
|
+
return reverseLabels([labelBelow, labelAbove])
|
|
55
57
|
}
|
|
56
58
|
if (visualizationType === 'Bar' && visualizationSubType === 'regular' && colorCode && series?.length === 1) {
|
|
57
59
|
let palette = colorPalettes[config.palette]
|
|
@@ -76,7 +78,7 @@ const Legend = () => {
|
|
|
76
78
|
return newLabel
|
|
77
79
|
})
|
|
78
80
|
|
|
79
|
-
return uniqueLabels
|
|
81
|
+
return reverseLabels(uniqueLabels)
|
|
80
82
|
}
|
|
81
83
|
|
|
82
84
|
// get forecasting items inside of combo
|
|
@@ -103,7 +105,7 @@ const Legend = () => {
|
|
|
103
105
|
|
|
104
106
|
// loop through bars for now to meet requirements.
|
|
105
107
|
config.runtime.barSeriesKeys &&
|
|
106
|
-
config.runtime.barSeriesKeys.
|
|
108
|
+
config.runtime.barSeriesKeys.forEach((bar, index) => {
|
|
107
109
|
let colorValue = colorPalettes[config.palette][index] ? colorPalettes[config.palette][index] : '#ccc'
|
|
108
110
|
|
|
109
111
|
const newLabel = {
|
|
@@ -116,7 +118,7 @@ const Legend = () => {
|
|
|
116
118
|
seriesLabels.push(newLabel)
|
|
117
119
|
})
|
|
118
120
|
|
|
119
|
-
return seriesLabels
|
|
121
|
+
return reverseLabels(seriesLabels)
|
|
120
122
|
}
|
|
121
123
|
|
|
122
124
|
// DEV-4161: replaceable series name in the legend
|
|
@@ -147,17 +149,17 @@ const Legend = () => {
|
|
|
147
149
|
return newLabel
|
|
148
150
|
})
|
|
149
151
|
|
|
150
|
-
return uniqueLabels
|
|
152
|
+
return reverseLabels(uniqueLabels)
|
|
151
153
|
}
|
|
152
154
|
|
|
153
|
-
return defaultLabels
|
|
155
|
+
return reverseLabels(defaultLabels)
|
|
154
156
|
}
|
|
155
157
|
|
|
156
|
-
const isBottomOrSmallViewport = legend.position === 'bottom' ||
|
|
158
|
+
const isBottomOrSmallViewport = legend.position === 'bottom' || ['sm', 'xs', 'xxs'].includes(currentViewport)
|
|
157
159
|
|
|
158
160
|
const legendClasses = {
|
|
159
161
|
marginBottom: isBottomOrSmallViewport ? '15px' : '0px',
|
|
160
|
-
marginTop: isBottomOrSmallViewport && orientation === 'horizontal' ? `${config.runtime.xAxis.size}px` :
|
|
162
|
+
marginTop: isBottomOrSmallViewport && orientation === 'horizontal' ? `${config.yAxis.label && config.isResponsiveTicks ? config.dynamicMarginTop : config.runtime.xAxis.size}px` : `0px`
|
|
161
163
|
}
|
|
162
164
|
|
|
163
165
|
const { HighLightedBarUtils } = useHighlightedBars(config)
|
|
@@ -2,22 +2,23 @@ import React, { useContext } from 'react'
|
|
|
2
2
|
|
|
3
3
|
import * as allCurves from '@visx/curve'
|
|
4
4
|
import { Group } from '@visx/group'
|
|
5
|
-
import { LinePath } from '@visx/shape'
|
|
5
|
+
import { LinePath, Bar } from '@visx/shape'
|
|
6
6
|
import { Text } from '@visx/text'
|
|
7
7
|
|
|
8
8
|
import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
9
9
|
import ConfigContext from '../ConfigContext'
|
|
10
10
|
import useRightAxis from '../hooks/useRightAxis'
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
const LineChart = ({ xScale, yScale, getXAxisData, getYAxisData, xMax, yMax, handleTooltipMouseOver, handleTooltipMouseOff, showTooltip, seriesStyle = 'Line', svgRef, handleTooltipClick, tooltipData }) => {
|
|
13
|
+
// Not sure why there's a redraw here.
|
|
14
14
|
|
|
15
|
+
const { colorPalettes, transformedData: data, colorScale, seriesHighlight, config, formatNumber, formatDate, parseDate, isNumber, updateConfig, handleLineType, dashboardConfig, tableData } = useContext(ConfigContext)
|
|
15
16
|
const { yScaleRight } = useRightAxis({ config, yMax, data, updateConfig })
|
|
16
17
|
|
|
18
|
+
if (!handleTooltipMouseOver) return
|
|
17
19
|
const handleAxisFormating = (axis = 'left', label, value) => {
|
|
18
20
|
// if this is an x axis category/date value return without doing any formatting.
|
|
19
21
|
// if (label === config.runtime.xAxis.label) return value
|
|
20
|
-
|
|
21
22
|
axis = String(axis).toLocaleLowerCase()
|
|
22
23
|
if (label) {
|
|
23
24
|
return `${label}: ${formatNumber(value, axis)}`
|
|
@@ -25,6 +26,7 @@ export default function LineChart({ xScale, yScale, getXAxisData, getYAxisData,
|
|
|
25
26
|
return `${formatNumber(value, axis)}`
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
const DEBUG = false
|
|
28
30
|
return (
|
|
29
31
|
<ErrorBoundary component='LineChart'>
|
|
30
32
|
<Group left={config.runtime.yAxis.size ? parseInt(config.runtime.yAxis.size) : 66}>
|
|
@@ -35,6 +37,8 @@ export default function LineChart({ xScale, yScale, getXAxisData, getYAxisData,
|
|
|
35
37
|
const seriesData = config.series.filter(item => item.dataKey === seriesKey)
|
|
36
38
|
const seriesAxis = seriesData[0].axis ? seriesData[0].axis : 'left'
|
|
37
39
|
|
|
40
|
+
let displayArea = config.legend.behavior === 'highlight' || seriesHighlight.length === 0 || seriesHighlight.indexOf(seriesKey) !== -1
|
|
41
|
+
|
|
38
42
|
return (
|
|
39
43
|
<Group
|
|
40
44
|
key={`series-${seriesKey}`}
|
|
@@ -71,7 +75,10 @@ export default function LineChart({ xScale, yScale, getXAxisData, getYAxisData,
|
|
|
71
75
|
d[seriesKey] !== '' &&
|
|
72
76
|
d[seriesKey] !== null &&
|
|
73
77
|
isNumber(d[seriesKey]) && (
|
|
74
|
-
<Group key={`series-${seriesKey}-point-${dataIndex}`}>
|
|
78
|
+
<Group key={`series-${seriesKey}-point-${dataIndex}`} className='checkwidth'>
|
|
79
|
+
{/* tooltips */}
|
|
80
|
+
<Bar key={'bars'} width={Number(xMax)} height={Number(yMax)} fill={DEBUG ? 'red' : 'transparent'} fillOpacity={0.05} onMouseMove={e => handleTooltipMouseOver(e, tableData)} onMouseOut={handleTooltipMouseOff} onClick={e => handleTooltipClick(e, data)} />
|
|
81
|
+
|
|
75
82
|
{/* Render legend */}
|
|
76
83
|
<Text display={config.labels ? 'block' : 'none'} x={xScale(getXAxisData(d))} y={seriesAxis === 'Right' ? yScaleRight(getYAxisData(d, seriesKey)) : yScale(getYAxisData(d, seriesKey))} fill={'#000'} textAnchor='middle'>
|
|
77
84
|
{formatNumber(d[seriesKey], 'left')}
|
|
@@ -83,10 +90,26 @@ export default function LineChart({ xScale, yScale, getXAxisData, getYAxisData,
|
|
|
83
90
|
cx={Number(xScale(getXAxisData(d)))}
|
|
84
91
|
cy={seriesAxis === 'Right' ? yScaleRight(getYAxisData(d, seriesKey)) : yScale(getYAxisData(d, seriesKey))}
|
|
85
92
|
fill={colorScale ? colorScale(config.runtime.seriesLabels ? config.runtime.seriesLabels[seriesKey] : seriesKey) : '#000'}
|
|
86
|
-
style={{
|
|
93
|
+
style={{
|
|
94
|
+
fill: colorScale ? colorScale(config.runtime.seriesLabels ? config.runtime.seriesLabels[seriesKey] : seriesKey) : '#000'
|
|
95
|
+
}}
|
|
87
96
|
data-tooltip-html={tooltip}
|
|
88
97
|
data-tooltip-id={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`}
|
|
89
98
|
/>
|
|
99
|
+
|
|
100
|
+
{/* circles that appear on hover */}
|
|
101
|
+
{/* todo: circle radii used here should be global with other circle radii */}
|
|
102
|
+
{/* {tooltipData && Object.entries(tooltipData.data).length > 0 && isNumber(tooltipData.data[seriesKey]) && config.lineDatapointStyle === 'hover' && config.series.filter(s => s.type === 'Line') && (
|
|
103
|
+
<circle
|
|
104
|
+
cx={config.xAxis.type === 'categorical' ? xScale(tooltipData.data[config.xAxis.dataKey]) : xScale(parseDate(tooltipData.data[config.xAxis.dataKey]))}
|
|
105
|
+
cy={yScale(tooltipData.data[seriesKey])}
|
|
106
|
+
r={4.5}
|
|
107
|
+
opacity={tooltipData[seriesKey] ? 1 : 0}
|
|
108
|
+
fillOpacity={1}
|
|
109
|
+
fill={displayArea ? (colorScale ? colorScale(config.runtime.seriesLabels ? config.runtime.seriesLabels[seriesKey] : seriesKey) : '#000') : 'transparent'}
|
|
110
|
+
style={{ filter: 'unset', opacity: 1 }}
|
|
111
|
+
/>
|
|
112
|
+
)} */}
|
|
90
113
|
</Group>
|
|
91
114
|
)
|
|
92
115
|
)
|
|
@@ -161,3 +184,5 @@ export default function LineChart({ xScale, yScale, getXAxisData, getYAxisData,
|
|
|
161
184
|
</ErrorBoundary>
|
|
162
185
|
)
|
|
163
186
|
}
|
|
187
|
+
|
|
188
|
+
export default LineChart
|