@cdc/chart 4.24.1 → 4.24.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.
- package/dist/cdcchart.js +30014 -29757
- package/examples/feature/line/line-chart-preliminary.json +84 -37
- package/examples/feature/regions/index.json +9 -5
- package/index.html +4 -2
- package/package.json +2 -2
- package/src/CdcChart.tsx +41 -24
- package/src/_stories/ChartEditor.stories.tsx +1 -1
- package/src/_stories/_mock/pie_config.json +4 -3
- package/src/components/AreaChart/components/AreaChart.jsx +1 -25
- package/src/components/BarChart/components/BarChart.StackedVertical.tsx +7 -5
- package/src/components/BarChart/components/BarChart.Vertical.tsx +12 -13
- package/src/components/BoxPlot/BoxPlot.jsx +9 -8
- package/src/components/EditorPanel/EditorPanel.tsx +1563 -1959
- package/src/components/EditorPanel/EditorPanelContext.ts +40 -0
- package/src/components/EditorPanel/components/Panels/Panel.BoxPlot.tsx +148 -0
- package/src/components/EditorPanel/components/{Panel.ForestPlotSettings.tsx → Panels/Panel.ForestPlotSettings.tsx} +16 -7
- package/src/components/EditorPanel/components/Panels/Panel.General.tsx +160 -0
- package/src/components/EditorPanel/components/{Panel.Regions.tsx → Panels/Panel.Regions.tsx} +5 -5
- package/src/components/EditorPanel/components/{Panel.Series.tsx → Panels/Panel.Series.tsx} +4 -4
- package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +297 -0
- package/src/components/EditorPanel/components/Panels/index.tsx +17 -0
- package/src/components/EditorPanel/editor-panel.scss +1 -13
- package/src/components/EditorPanel/useEditorPermissions.js +5 -0
- package/src/components/Legend/Legend.Component.tsx +199 -0
- package/src/components/Legend/Legend.tsx +5 -324
- package/src/components/Legend/helpers/createFormatLabels.tsx +140 -0
- package/src/components/LineChart/LineChartProps.ts +1 -1
- package/src/components/LineChart/components/LineChart.Circle.tsx +85 -52
- package/src/components/LineChart/helpers.ts +2 -2
- package/src/components/LineChart/index.tsx +97 -21
- package/src/components/LinearChart.jsx +3 -3
- package/src/components/PairedBarChart.jsx +4 -2
- package/src/components/PieChart/PieChart.tsx +78 -25
- package/src/components/Regions/components/Regions.tsx +14 -5
- package/src/data/initial-state.js +5 -2
- package/src/helpers/computeMarginBottom.ts +2 -2
- package/src/hooks/useHighlightedBars.js +1 -1
- package/src/hooks/useMinMax.ts +3 -3
- package/src/hooks/useScales.ts +18 -5
- package/src/hooks/useTooltip.tsx +11 -7
- package/src/scss/main.scss +0 -67
- package/src/types/ChartConfig.ts +17 -8
- package/src/types/ChartContext.ts +10 -4
- package/src/types/Label.ts +7 -0
- package/examples/private/chart-t.json +0 -3740
- package/examples/private/combo.json +0 -369
- package/examples/private/epi-data.csv +0 -13
- package/examples/private/epi-data.json +0 -62
- package/examples/private/epi.json +0 -403
- package/examples/private/occupancy.json +0 -109283
- package/examples/private/prod-line-config.json +0 -401
- package/examples/private/region-data.json +0 -822
- package/examples/private/region-testing.json +0 -312
- package/examples/private/scaling.json +0 -45325
- package/examples/private/testing-data.json +0 -1739
- package/examples/private/testing.json +0 -816
- package/src/components/EditorPanel/components/Panel.DateHighlighting.tsx +0 -109
- package/src/components/EditorPanel/components/Panels.tsx +0 -13
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { useContext, FC } from 'react'
|
|
2
|
+
|
|
3
|
+
// external libraries
|
|
4
|
+
import { AccordionItem, AccordionItemHeading, AccordionItemPanel, AccordionItemButton } from 'react-accessible-accordion'
|
|
5
|
+
|
|
6
|
+
// core
|
|
7
|
+
import { TextField, Select, CheckBox } from '@cdc/core/components/EditorPanel/Inputs'
|
|
8
|
+
import Tooltip from '@cdc/core/components/ui/Tooltip'
|
|
9
|
+
import Icon from '@cdc/core/components/ui/Icon'
|
|
10
|
+
import InputToggle from '@cdc/core/components/inputs/InputToggle'
|
|
11
|
+
|
|
12
|
+
// contexts
|
|
13
|
+
import { useColorPalette } from '../../../../hooks/useColorPalette'
|
|
14
|
+
import { ChartContext } from './../../../../types/ChartContext.js'
|
|
15
|
+
|
|
16
|
+
import { useEditorPermissions } from '../../useEditorPermissions.js'
|
|
17
|
+
import { useEditorPanelContext } from '../../EditorPanelContext.js'
|
|
18
|
+
import ConfigContext from '../../../../ConfigContext.js'
|
|
19
|
+
import { PanelProps } from '../PanelProps'
|
|
20
|
+
|
|
21
|
+
const PanelVisual: FC<PanelProps> = props => {
|
|
22
|
+
const { config, updateConfig, colorPalettes, twoColorPalette } = useContext<ChartContext>(ConfigContext)
|
|
23
|
+
const { visual } = config
|
|
24
|
+
const { setLollipopShape, updateField } = useEditorPanelContext()
|
|
25
|
+
const { visHasBarBorders, visCanAnimate, visSupportsNonSequentialPallete, headerColors, visSupportsTooltipOpacity, visSupportsTooltipLines, visSupportsBarSpace, visSupportsBarThickness, visHasDataCutoff, visSupportsSequentialPallete, visSupportsReverseColorPalette } = useEditorPermissions()
|
|
26
|
+
const { twoColorPalettes, sequential, nonSequential } = useColorPalette(config, updateConfig)
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<AccordionItem>
|
|
30
|
+
<AccordionItemHeading>
|
|
31
|
+
<AccordionItemButton>Visual</AccordionItemButton>
|
|
32
|
+
</AccordionItemHeading>
|
|
33
|
+
<AccordionItemPanel>
|
|
34
|
+
{config.isLollipopChart && (
|
|
35
|
+
<>
|
|
36
|
+
<fieldset className='header'>
|
|
37
|
+
<legend className='edit-label'>Lollipop Shape</legend>
|
|
38
|
+
<div
|
|
39
|
+
onChange={e => {
|
|
40
|
+
setLollipopShape(e.target.value)
|
|
41
|
+
}}
|
|
42
|
+
>
|
|
43
|
+
<label className='radio-label'>
|
|
44
|
+
<input type='radio' name='lollipopShape' value='circle' checked={config.lollipopShape === 'circle'} />
|
|
45
|
+
Circle
|
|
46
|
+
</label>
|
|
47
|
+
<label className='radio-label'>
|
|
48
|
+
<input type='radio' name='lollipopShape' value='square' checked={config.lollipopShape === 'square'} />
|
|
49
|
+
Square
|
|
50
|
+
</label>
|
|
51
|
+
</div>
|
|
52
|
+
</fieldset>
|
|
53
|
+
<Select value={config.lollipopColorStyle ? config.lollipopColorStyle : 'two-tone'} fieldName='lollipopColorStyle' label='Lollipop Color Style' updateField={updateField} options={['regular', 'two-tone']} />
|
|
54
|
+
<Select value={config.lollipopSize ? config.lollipopSize : 'small'} fieldName='lollipopSize' label='Lollipop Size' updateField={updateField} options={['small', 'medium', 'large']} />
|
|
55
|
+
</>
|
|
56
|
+
)}
|
|
57
|
+
|
|
58
|
+
{config.visualizationType === 'Box Plot' && (
|
|
59
|
+
<fieldset className='fieldset fieldset--boxplot'>
|
|
60
|
+
<legend className=''>Box Plot Settings</legend>
|
|
61
|
+
<Select value={config.boxplot.borders} fieldName='borders' section='boxplot' label='Box Plot Borders' updateField={updateField} options={['true', 'false']} />
|
|
62
|
+
<CheckBox value={config.boxplot.plotOutlierValues} fieldName='plotOutlierValues' section='boxplot' label='Plot Outliers' updateField={updateField} />
|
|
63
|
+
<CheckBox value={config.boxplot.plotNonOutlierValues} fieldName='plotNonOutlierValues' section='boxplot' label='Plot non-outlier values' updateField={updateField} />
|
|
64
|
+
</fieldset>
|
|
65
|
+
)}
|
|
66
|
+
|
|
67
|
+
<Select value={config.fontSize} fieldName='fontSize' label='Font Size' updateField={updateField} options={['small', 'medium', 'large']} />
|
|
68
|
+
{visHasBarBorders() && <Select value={config.barHasBorder} fieldName='barHasBorder' label='Bar Borders' updateField={updateField} options={['true', 'false']} />}
|
|
69
|
+
{visCanAnimate() && <CheckBox value={config.animate} fieldName='animate' label='Animate Visualization' updateField={updateField} />}
|
|
70
|
+
|
|
71
|
+
{/*<CheckBox value={config.animateReplay} fieldName="animateReplay" label="Replay Animation When Filters Are Changed" updateField={updateField} />*/}
|
|
72
|
+
|
|
73
|
+
{((config.series?.some(series => series.type === 'Line' || series.type === 'dashed-lg' || series.type === 'dashed-sm' || series.type === 'dashed-md') && config.visualizationType === 'Combo') || config.visualizationType === 'Line') && (
|
|
74
|
+
<>
|
|
75
|
+
<Select value={config.lineDatapointStyle} fieldName='lineDatapointStyle' label='Line Datapoint Style' updateField={updateField} options={['hidden', 'hover', 'always show']} />
|
|
76
|
+
<Select value={config.lineDatapointColor} fieldName='lineDatapointColor' label='Line Datapoint Color' updateField={updateField} options={['Same as Line', 'Lighter than Line']} />
|
|
77
|
+
</>
|
|
78
|
+
)}
|
|
79
|
+
|
|
80
|
+
{/* eslint-disable */}
|
|
81
|
+
<label className='header'>
|
|
82
|
+
<span className='edit-label'>Header Theme</span>
|
|
83
|
+
<ul className='color-palette'>
|
|
84
|
+
{headerColors.map(palette => (
|
|
85
|
+
<button
|
|
86
|
+
title={palette}
|
|
87
|
+
key={palette}
|
|
88
|
+
onClick={e => {
|
|
89
|
+
e.preventDefault()
|
|
90
|
+
updateConfig({ ...config, theme: palette })
|
|
91
|
+
}}
|
|
92
|
+
className={config.theme === palette ? 'selected ' + palette : palette}
|
|
93
|
+
></button>
|
|
94
|
+
))}
|
|
95
|
+
</ul>
|
|
96
|
+
</label>
|
|
97
|
+
{/* eslint-enable */}
|
|
98
|
+
{(visSupportsNonSequentialPallete() || visSupportsNonSequentialPallete()) && (
|
|
99
|
+
<>
|
|
100
|
+
<label>
|
|
101
|
+
<span className='edit-label'>Chart Color Palette</span>
|
|
102
|
+
</label>
|
|
103
|
+
{visSupportsReverseColorPalette() && <InputToggle fieldName='isPaletteReversed' size='small' label='Use selected palette in reverse order' updateField={updateField} value={config.isPaletteReversed} />}
|
|
104
|
+
{visSupportsSequentialPallete() && (
|
|
105
|
+
<>
|
|
106
|
+
<span>Sequential</span>
|
|
107
|
+
<ul className='color-palette'>
|
|
108
|
+
{sequential.map(palette => {
|
|
109
|
+
const colorOne = {
|
|
110
|
+
backgroundColor: colorPalettes[palette][2]
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const colorTwo = {
|
|
114
|
+
backgroundColor: colorPalettes[palette][3]
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const colorThree = {
|
|
118
|
+
backgroundColor: colorPalettes[palette][5]
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<button
|
|
123
|
+
title={palette}
|
|
124
|
+
key={palette}
|
|
125
|
+
onClick={e => {
|
|
126
|
+
e.preventDefault()
|
|
127
|
+
updateConfig({ ...config, palette })
|
|
128
|
+
}}
|
|
129
|
+
className={config.palette === palette ? 'selected' : ''}
|
|
130
|
+
>
|
|
131
|
+
<span style={colorOne}></span>
|
|
132
|
+
<span style={colorTwo}></span>
|
|
133
|
+
<span style={colorThree}></span>
|
|
134
|
+
</button>
|
|
135
|
+
)
|
|
136
|
+
})}
|
|
137
|
+
</ul>
|
|
138
|
+
</>
|
|
139
|
+
)}
|
|
140
|
+
{visSupportsNonSequentialPallete() && (
|
|
141
|
+
<>
|
|
142
|
+
<span>Non-Sequential</span>
|
|
143
|
+
<ul className='color-palette'>
|
|
144
|
+
{nonSequential.map(palette => {
|
|
145
|
+
const colorOne = {
|
|
146
|
+
backgroundColor: colorPalettes[palette][2]
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const colorTwo = {
|
|
150
|
+
backgroundColor: colorPalettes[palette][4]
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const colorThree = {
|
|
154
|
+
backgroundColor: colorPalettes[palette][6]
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<button
|
|
159
|
+
title={palette}
|
|
160
|
+
key={palette}
|
|
161
|
+
onClick={e => {
|
|
162
|
+
e.preventDefault()
|
|
163
|
+
updateConfig({ ...config, palette })
|
|
164
|
+
}}
|
|
165
|
+
className={config.palette === palette ? 'selected' : ''}
|
|
166
|
+
>
|
|
167
|
+
<span style={colorOne}></span>
|
|
168
|
+
<span style={colorTwo}></span>
|
|
169
|
+
<span style={colorThree}></span>
|
|
170
|
+
</button>
|
|
171
|
+
)
|
|
172
|
+
})}
|
|
173
|
+
</ul>
|
|
174
|
+
</>
|
|
175
|
+
)}
|
|
176
|
+
</>
|
|
177
|
+
)}
|
|
178
|
+
{(config.visualizationType === 'Paired Bar' || config.visualizationType === 'Deviation Bar') && (
|
|
179
|
+
<>
|
|
180
|
+
<InputToggle section='twoColor' fieldName='isPaletteReversed' size='small' label='Use selected palette in reverse order' updateField={updateField} value={config.twoColor.isPaletteReversed} />
|
|
181
|
+
<ul className='color-palette'>
|
|
182
|
+
{twoColorPalettes.map(palette => {
|
|
183
|
+
const colorOne = {
|
|
184
|
+
backgroundColor: twoColorPalette[palette][0]
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const colorTwo = {
|
|
188
|
+
backgroundColor: twoColorPalette[palette][1]
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return (
|
|
192
|
+
<button
|
|
193
|
+
title={palette}
|
|
194
|
+
key={palette}
|
|
195
|
+
onClick={e => {
|
|
196
|
+
e.preventDefault()
|
|
197
|
+
updateConfig({ ...config, twoColor: { ...config.twoColor, palette } })
|
|
198
|
+
}}
|
|
199
|
+
className={config.twoColor.palette === palette ? 'selected' : ''}
|
|
200
|
+
>
|
|
201
|
+
<span className='two-color' style={colorOne}></span>
|
|
202
|
+
<span className='two-color' style={colorTwo}></span>
|
|
203
|
+
</button>
|
|
204
|
+
)
|
|
205
|
+
})}
|
|
206
|
+
</ul>
|
|
207
|
+
</>
|
|
208
|
+
)}
|
|
209
|
+
|
|
210
|
+
{visHasDataCutoff() && (
|
|
211
|
+
<>
|
|
212
|
+
<TextField
|
|
213
|
+
value={config.dataCutoff}
|
|
214
|
+
type='number'
|
|
215
|
+
fieldName='dataCutoff'
|
|
216
|
+
className='number-narrow'
|
|
217
|
+
label='Data Cutoff'
|
|
218
|
+
updateField={updateField}
|
|
219
|
+
tooltip={
|
|
220
|
+
<Tooltip style={{ textTransform: 'none' }}>
|
|
221
|
+
<Tooltip.Target>
|
|
222
|
+
<Icon display='question' style={{ marginLeft: '0.5rem' }} />
|
|
223
|
+
</Tooltip.Target>
|
|
224
|
+
<Tooltip.Content>
|
|
225
|
+
<p>Any value below the cut-off value is included in a special "less than" category. This option supports special conditions like suppressed data.</p>
|
|
226
|
+
</Tooltip.Content>
|
|
227
|
+
</Tooltip>
|
|
228
|
+
}
|
|
229
|
+
/>
|
|
230
|
+
</>
|
|
231
|
+
)}
|
|
232
|
+
{visSupportsBarThickness() && config.orientation === 'horizontal' && !config.isLollipopChart && config.yAxis.labelPlacement !== 'On Bar' && <TextField type='number' value={config.barHeight || '25'} fieldName='barHeight' label=' Bar Thickness' updateField={updateField} min={15} />}
|
|
233
|
+
{((config.visualizationType === 'Bar' && config.orientation !== 'horizontal') || config.visualizationType === 'Combo') && <TextField value={config.barThickness} type='number' fieldName='barThickness' label='Bar Thickness' updateField={updateField} />}
|
|
234
|
+
{visSupportsBarSpace() && <TextField type='number' value={config.barSpace || '15'} fieldName='barSpace' label='Bar Space' updateField={updateField} min={0} />}
|
|
235
|
+
{(config.visualizationType === 'Bar' || config.visualizationType === 'Line' || config.visualizationType === 'Combo') && <CheckBox value={config.topAxis.hasLine} section='topAxis' fieldName='hasLine' label='Add Top Axis Line' updateField={updateField} />}
|
|
236
|
+
|
|
237
|
+
{config.visualizationType === 'Spark Line' && (
|
|
238
|
+
<div className='cove-accordion__panel-section checkbox-group'>
|
|
239
|
+
<CheckBox value={visual?.border} section='visual' fieldName='border' label='Show Border' updateField={updateField} />
|
|
240
|
+
<CheckBox value={visual?.borderColorTheme} section='visual' fieldName='borderColorTheme' label='Use Border Color Theme' updateField={updateField} />
|
|
241
|
+
<CheckBox value={visual?.accent} section='visual' fieldName='accent' label='Use Accent Style' updateField={updateField} />
|
|
242
|
+
<CheckBox value={visual?.background} section='visual' fieldName='background' label='Use Theme Background Color' updateField={updateField} />
|
|
243
|
+
<CheckBox value={visual?.hideBackgroundColor} section='visual' fieldName='hideBackgroundColor' label='Hide Background Color' updateField={updateField} />
|
|
244
|
+
</div>
|
|
245
|
+
)}
|
|
246
|
+
|
|
247
|
+
{(config.visualizationType === 'Line' || config.visualizationType === 'Combo') && <CheckBox value={config.showLineSeriesLabels} fieldName='showLineSeriesLabels' label='Append Series Name to End of Line Charts' updateField={updateField} />}
|
|
248
|
+
{(config.visualizationType === 'Line' || config.visualizationType === 'Combo') && config.showLineSeriesLabels && <CheckBox value={config.colorMatchLineSeriesLabels} fieldName='colorMatchLineSeriesLabels' label='Match Series Color to Name at End of Line Charts' updateField={updateField} />}
|
|
249
|
+
|
|
250
|
+
{visSupportsTooltipLines() && (
|
|
251
|
+
<>
|
|
252
|
+
<CheckBox value={visual.verticalHoverLine} fieldName='verticalHoverLine' section='visual' label='Vertical Hover Line' updateField={updateField} />
|
|
253
|
+
<CheckBox value={visual.horizontalHoverLine} fieldName='horizontalHoverLine' section='visual' label='Horizontal Hover Line' updateField={updateField} />
|
|
254
|
+
</>
|
|
255
|
+
)}
|
|
256
|
+
{visSupportsTooltipOpacity() && (
|
|
257
|
+
<label>
|
|
258
|
+
<span className='edit-label column-heading'>Tooltip Opacity</span>
|
|
259
|
+
<input
|
|
260
|
+
type='number'
|
|
261
|
+
value={config.tooltips.opacity ? config.tooltips.opacity : 100}
|
|
262
|
+
onChange={e =>
|
|
263
|
+
updateConfig({
|
|
264
|
+
...config,
|
|
265
|
+
tooltips: {
|
|
266
|
+
...config.tooltips,
|
|
267
|
+
opacity: e.target.value
|
|
268
|
+
}
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
/>
|
|
272
|
+
</label>
|
|
273
|
+
)}
|
|
274
|
+
{config.visualizationType === 'Bar' && <CheckBox value={config.tooltips.singleSeries} fieldName='singleSeries' section='tooltips' label='SHOW HOVER FOR SINGLE DATA SERIES' updateField={updateField} />}
|
|
275
|
+
|
|
276
|
+
<label>
|
|
277
|
+
<span className='edit-label column-heading'>No Data Message</span>
|
|
278
|
+
<input
|
|
279
|
+
type='text'
|
|
280
|
+
value={config.chartMessage.noData ? config.chartMessage.noData : ''}
|
|
281
|
+
onChange={e =>
|
|
282
|
+
updateConfig({
|
|
283
|
+
...config,
|
|
284
|
+
chartMessage: {
|
|
285
|
+
...config.chartMessage,
|
|
286
|
+
noData: e.target.value
|
|
287
|
+
}
|
|
288
|
+
})
|
|
289
|
+
}
|
|
290
|
+
/>
|
|
291
|
+
</label>
|
|
292
|
+
</AccordionItemPanel>
|
|
293
|
+
</AccordionItem>
|
|
294
|
+
)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export default PanelVisual
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import ForestPlotSettings from './Panel.ForestPlotSettings.js'
|
|
2
|
+
import Series from './Panel.Series.js'
|
|
3
|
+
import Regions from './Panel.Regions.js'
|
|
4
|
+
import General from './Panel.General.js'
|
|
5
|
+
import BoxPlot from './Panel.BoxPlot.js'
|
|
6
|
+
import Visual from './Panel.Visual.js'
|
|
7
|
+
|
|
8
|
+
const Panels = {
|
|
9
|
+
ForestPlot: ForestPlotSettings,
|
|
10
|
+
Series: Series,
|
|
11
|
+
Regions,
|
|
12
|
+
General,
|
|
13
|
+
BoxPlot,
|
|
14
|
+
Visual
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default Panels
|
|
@@ -8,17 +8,6 @@
|
|
|
8
8
|
|
|
9
9
|
.cdc-open-viz-module.type-chart {
|
|
10
10
|
.editor-panel {
|
|
11
|
-
@import './components/panels.scss';
|
|
12
|
-
display: flex;
|
|
13
|
-
flex-direction: column;
|
|
14
|
-
position: fixed;
|
|
15
|
-
top: 0;
|
|
16
|
-
bottom: 0;
|
|
17
|
-
left: 0;
|
|
18
|
-
width: 350px;
|
|
19
|
-
background: #fff;
|
|
20
|
-
z-index: 8;
|
|
21
|
-
|
|
22
11
|
&.hidden {
|
|
23
12
|
display: none;
|
|
24
13
|
}
|
|
@@ -711,14 +700,13 @@
|
|
|
711
700
|
color: #000;
|
|
712
701
|
font-size: 1em;
|
|
713
702
|
border: 0;
|
|
714
|
-
position:
|
|
703
|
+
position: absolute;
|
|
715
704
|
z-index: 100;
|
|
716
705
|
transition: 0.1s background;
|
|
717
706
|
cursor: pointer;
|
|
718
707
|
width: 25px;
|
|
719
708
|
height: 25px;
|
|
720
709
|
left: 307px;
|
|
721
|
-
top: 10px;
|
|
722
710
|
box-shadow: rgba(0, 0, 0, 0.5) 0 1px 2px;
|
|
723
711
|
|
|
724
712
|
&:before {
|
|
@@ -275,6 +275,10 @@ export const useEditorPermissions = () => {
|
|
|
275
275
|
return true
|
|
276
276
|
}
|
|
277
277
|
|
|
278
|
+
const visSupportsDateCategoryAxisPadding = () => {
|
|
279
|
+
return config.xAxis.type === 'date' && config.xAxis.sortDates
|
|
280
|
+
}
|
|
281
|
+
|
|
278
282
|
const visSupportsReactTooltip = () => {
|
|
279
283
|
if (['Deviation Bar', 'Box Plot', 'Scatter Plot', 'Paired Bar'].includes(visualizationType) || (visualizationType === 'Bar' && config.tooltips.singleSeries)) {
|
|
280
284
|
return true
|
|
@@ -303,6 +307,7 @@ export const useEditorPermissions = () => {
|
|
|
303
307
|
visSupportsDateCategoryHeight,
|
|
304
308
|
visSupportsDateCategoryNumTicks,
|
|
305
309
|
visSupportsDateCategoryTickRotation,
|
|
310
|
+
visSupportsDateCategoryAxisPadding,
|
|
306
311
|
visSupportsFilters,
|
|
307
312
|
visSupportsFootnotes,
|
|
308
313
|
visSupportsLeftValueAxis,
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import parse from 'html-react-parser'
|
|
2
|
+
import { LegendOrdinal, LegendItem, LegendLabel } from '@visx/legend'
|
|
3
|
+
import LegendCircle from '@cdc/core/components/LegendCircle'
|
|
4
|
+
|
|
5
|
+
import useLegendClasses from '../../hooks/useLegendClasses'
|
|
6
|
+
import { useHighlightedBars } from '../../hooks/useHighlightedBars'
|
|
7
|
+
import { handleLineType } from '../../helpers/handleLineType'
|
|
8
|
+
import { Line } from '@visx/shape'
|
|
9
|
+
import { scaleOrdinal } from '@visx/scale'
|
|
10
|
+
import { Label } from '../../types/Label'
|
|
11
|
+
import { ChartConfig } from '../../types/ChartConfig'
|
|
12
|
+
import { ColorScale } from '../../types/ChartContext'
|
|
13
|
+
import { Group } from '@visx/group'
|
|
14
|
+
|
|
15
|
+
interface LegendProps {
|
|
16
|
+
config: ChartConfig
|
|
17
|
+
colorScale: ColorScale
|
|
18
|
+
seriesHighlight: string[]
|
|
19
|
+
highlight: Function
|
|
20
|
+
highlightReset: Function
|
|
21
|
+
currentViewport: string
|
|
22
|
+
formatLabels: (labels: Label[]) => Label[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* eslint-disable jsx-a11y/no-noninteractive-tabindex, jsx-a11y/no-static-element-interactions */
|
|
26
|
+
const Legend: React.FC<LegendProps> = ({ config, colorScale, seriesHighlight, highlight, highlightReset, currentViewport, formatLabels }) => {
|
|
27
|
+
const { innerClasses, containerClasses } = useLegendClasses(config)
|
|
28
|
+
const { runtime, orientation, legend } = config
|
|
29
|
+
if (!legend) return null
|
|
30
|
+
// create fn to reverse labels while legend is Bottom. Legend-right , legend-left works by default.
|
|
31
|
+
const displayScale = scaleOrdinal({
|
|
32
|
+
domain: config.suppressedData?.map(d => d.label),
|
|
33
|
+
range: ['none'],
|
|
34
|
+
unknown: 'block'
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const renderDashes = style => {
|
|
38
|
+
const dashCount = style === 'Dashed Small' ? 3 : 2
|
|
39
|
+
const dashClass = `dashes ${style.toLowerCase().replace(' ', '-')}`
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className={dashClass}>
|
|
43
|
+
{Array.from({ length: dashCount }, (_, i) => (
|
|
44
|
+
<span key={i}>-</span>
|
|
45
|
+
))}
|
|
46
|
+
</div>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
const renderDashesOrCircle = style => {
|
|
50
|
+
if (['Dashed Small', 'Dashed Medium', 'Dashed Large'].includes(style)) {
|
|
51
|
+
return renderDashes(style)
|
|
52
|
+
} else if (style === 'Open Circles') {
|
|
53
|
+
return <div className='dashes open-circles'></div>
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const isBottomOrSmallViewport = legend.position === 'bottom' || ['sm', 'xs', 'xxs'].includes(currentViewport)
|
|
58
|
+
|
|
59
|
+
const legendClasses = {
|
|
60
|
+
marginBottom: isBottomOrSmallViewport ? '15px' : '0px',
|
|
61
|
+
marginTop: isBottomOrSmallViewport && orientation === 'horizontal' ? `${config.yAxis.label && config.isResponsiveTicks ? config.dynamicMarginTop : config.runtime.xAxis.size}px` : `${isBottomOrSmallViewport ? config.dynamicMarginTop + 15 : 0}px`
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const { HighLightedBarUtils } = useHighlightedBars(config)
|
|
65
|
+
|
|
66
|
+
let highLightedLegendItems = HighLightedBarUtils.findDuplicates(config.highlightedBarValues)
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<aside style={legendClasses} id='legend' className={containerClasses.join(' ')} role='region' aria-label='legend' tabIndex={0}>
|
|
70
|
+
{legend.label && <h2>{parse(legend.label)}</h2>}
|
|
71
|
+
{legend.description && <p>{parse(legend.description)}</p>}
|
|
72
|
+
<LegendOrdinal scale={colorScale} itemDirection='row' labelMargin='0 20px 0 0' shapeMargin='0 10px 0'>
|
|
73
|
+
{labels => {
|
|
74
|
+
return (
|
|
75
|
+
<>
|
|
76
|
+
<div className={innerClasses.join(' ')}>
|
|
77
|
+
{formatLabels(labels as Label[]).map((label, i) => {
|
|
78
|
+
let className = ['legend-item', `legend-text--${label.text.replace(' ', '').toLowerCase()}`]
|
|
79
|
+
let itemName = label.datum
|
|
80
|
+
|
|
81
|
+
// Filter excluded data keys from legend
|
|
82
|
+
if (config.exclusions.active && config.exclusions.keys?.includes(itemName)) {
|
|
83
|
+
return null
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (runtime.seriesLabels) {
|
|
87
|
+
let index = config.runtime.seriesLabelsAll.indexOf(itemName)
|
|
88
|
+
itemName = config.runtime.seriesKeys[index]
|
|
89
|
+
|
|
90
|
+
if (runtime?.forecastingSeriesKeys?.length > 0) {
|
|
91
|
+
itemName = label.text
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (seriesHighlight.length > 0 && false === seriesHighlight.includes(itemName)) {
|
|
96
|
+
className.push('inactive')
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<LegendItem
|
|
101
|
+
className={className.join(' ')}
|
|
102
|
+
tabIndex={0}
|
|
103
|
+
key={`legend-quantile-${i}`}
|
|
104
|
+
onKeyPress={e => {
|
|
105
|
+
if (e.key === 'Enter') {
|
|
106
|
+
highlight(label)
|
|
107
|
+
}
|
|
108
|
+
}}
|
|
109
|
+
onClick={() => {
|
|
110
|
+
highlight(label)
|
|
111
|
+
}}
|
|
112
|
+
>
|
|
113
|
+
{config.visualizationType === 'Line' && config.legend.lineMode ? (
|
|
114
|
+
<svg width={40} height={20}>
|
|
115
|
+
<Line from={{ x: 10, y: 10 }} to={{ x: 40, y: 10 }} stroke={label.value} strokeWidth={2} strokeDasharray={handleLineType(config.series[i]?.type ? config.series[i]?.type : '')} />
|
|
116
|
+
</svg>
|
|
117
|
+
) : (
|
|
118
|
+
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
|
119
|
+
<LegendCircle margin='0' fill={label.value} display={displayScale(label.datum)} />
|
|
120
|
+
<div style={{ marginTop: '2px', marginRight: '6px' }}>{label.icon}</div>
|
|
121
|
+
</div>
|
|
122
|
+
)}
|
|
123
|
+
|
|
124
|
+
<LegendLabel align='left' margin='0 0 0 4px'>
|
|
125
|
+
{label.text}
|
|
126
|
+
</LegendLabel>
|
|
127
|
+
</LegendItem>
|
|
128
|
+
)
|
|
129
|
+
})}
|
|
130
|
+
|
|
131
|
+
{highLightedLegendItems.map((bar, i) => {
|
|
132
|
+
// if duplicates only return first item
|
|
133
|
+
let className = 'legend-item'
|
|
134
|
+
let itemName = bar.legendLabel
|
|
135
|
+
|
|
136
|
+
if (!itemName) return false
|
|
137
|
+
if (seriesHighlight.length > 0 && false === seriesHighlight.includes(itemName)) {
|
|
138
|
+
className += ' inactive'
|
|
139
|
+
}
|
|
140
|
+
return (
|
|
141
|
+
<LegendItem
|
|
142
|
+
className={className}
|
|
143
|
+
tabIndex={0}
|
|
144
|
+
key={`legend-quantile-${i}`}
|
|
145
|
+
onKeyPress={e => {
|
|
146
|
+
if (e.key === 'Enter') {
|
|
147
|
+
highlight(bar.legendLabel)
|
|
148
|
+
}
|
|
149
|
+
}}
|
|
150
|
+
onClick={() => {
|
|
151
|
+
highlight(bar.legendLabel)
|
|
152
|
+
}}
|
|
153
|
+
>
|
|
154
|
+
<LegendCircle fill='transparent' borderColor={bar.color ? bar.color : `rgba(255, 102, 1)`} />{' '}
|
|
155
|
+
<LegendLabel align='left' margin='0 0 0 4px'>
|
|
156
|
+
{bar.legendLabel ? bar.legendLabel : bar.value}
|
|
157
|
+
</LegendLabel>
|
|
158
|
+
</LegendItem>
|
|
159
|
+
)
|
|
160
|
+
})}
|
|
161
|
+
{seriesHighlight.length > 0 && (
|
|
162
|
+
<button className={`legend-reset ${config.theme}`} onClick={labels => highlightReset(labels)} tabIndex={0}>
|
|
163
|
+
Reset
|
|
164
|
+
</button>
|
|
165
|
+
)}
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<>
|
|
169
|
+
{config?.preliminaryData?.some(pd => pd.label) && config.visualizationType === 'Line' && (
|
|
170
|
+
<>
|
|
171
|
+
<hr></hr>
|
|
172
|
+
<div className={config.legend.singleRow && isBottomOrSmallViewport ? 'legend-container__inner bottom single-row' : ''}>
|
|
173
|
+
{config?.preliminaryData?.map((pd, index) => {
|
|
174
|
+
return (
|
|
175
|
+
<>
|
|
176
|
+
{pd.label && (
|
|
177
|
+
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
|
178
|
+
<svg style={{ width: '50px' }} key={index} height={'23px'}>
|
|
179
|
+
{pd.style.includes('Dashed') ? <Line from={{ x: 10, y: 10 }} to={{ x: 40, y: 10 }} stroke={'#000'} strokeWidth={2} strokeDasharray={handleLineType(pd.style)} /> : <circle r={6} strokeWidth={2} stroke={'#000'} cx={22} cy={10} fill='transparent' />}
|
|
180
|
+
</svg>
|
|
181
|
+
<span style={{}}> {pd.label}</span>
|
|
182
|
+
</div>
|
|
183
|
+
)}
|
|
184
|
+
</>
|
|
185
|
+
)
|
|
186
|
+
})}
|
|
187
|
+
</div>
|
|
188
|
+
</>
|
|
189
|
+
)}
|
|
190
|
+
</>
|
|
191
|
+
</>
|
|
192
|
+
)
|
|
193
|
+
}}
|
|
194
|
+
</LegendOrdinal>
|
|
195
|
+
</aside>
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export default Legend
|