Skip to content

Commit 6286ae9

Browse files
authored
Merge pull request #2364 from CDCgov/dev
Dev
2 parents 8e1ef4e + da02158 commit 6286ae9

File tree

63 files changed

+4481
-1540
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

63 files changed

+4481
-1540
lines changed

docs/TESTING_BEST_PRACTICES.md

Lines changed: 243 additions & 289 deletions
Large diffs are not rendered by default.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191
"scripts": {
9292
"build": "npx lerna run build",
9393
"clean": "find . -name 'node_modules' -type d -prune -exec rm -rf '{}' +",
94+
"reset": "lerna clean -y && rm -rf node_modules && yarn install && lerna run build",
9495
"publish": "npx lerna publish",
9596
"storybook": "storybook dev -p 6006",
9697
"test-storybook": "vitest run --project=storybook -t 'Tests'",
@@ -116,4 +117,4 @@
116117
"resolutions": {
117118
"jackspeak": "2.1.1"
118119
}
119-
}
120+
}

packages/chart/examples/feature/__data__/planet-example-data.json

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -17,34 +17,4 @@
1717
"Diameter": "18",
1818
"distance": "40"
1919
},
20-
{
21-
"name": "Neptune",
22-
"Radius": "700",
23-
"Diameter": "18",
24-
"distance": "0"
25-
},
26-
{
27-
"name": "Venus",
28-
"Radius": "500",
29-
"Diameter": "18",
30-
"distance": "0"
31-
},
32-
{
33-
"name": "Mars",
34-
"Radius": "400",
35-
"Diameter": "18",
36-
"distance": "0"
37-
},
38-
{
39-
"name": "Mercury",
40-
"Radius": "300",
41-
"Diameter": "22",
42-
"distance": "0"
43-
},
44-
{
45-
"name": "Pluto",
46-
"Radius": "200",
47-
"Diameter": "22",
48-
"distance": "0"
49-
}
5020
]

packages/chart/index.html

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
<!-- <div class="react-container" data-config="/examples/test.json"></div> -->
5353
<!-- <div class="react-container" data-config="/examples/feature/line/line-chart.json"></div> -->
5454
<!-- <div class="react-container" data-config="/examples/dev-8332.json"></div> -->
55-
<!-- <div class="react-container" data-config="/examples/private/f4.json"></div> -->
55+
<!-- <div class="react-container" data-config="/examples/private/pie-chart-legend.json"></div> -->
5656
<!-- <div class="react-container" data-config="/examples/feature/filters/url-filter.json"></div> -->
5757
<!-- <div class="react-container" data-config="/examples/feature/pie/planet-pie-example-config.json"></div> -->
5858
<!-- <div class="react-container" data-config=https://www.cdc.gov/wcms/4.0/cdc-wp/data-presentation/examples/Line_Chart_Viz.json></div> -->
@@ -74,10 +74,8 @@
7474
<!-- BAR -->
7575
<!-- <div class="react-container" data-config="/examples/feature/bar/planet-example-config.json"></div> -->
7676
<!-- <div class="react-container" data-config="/examples/feature/bar/example-bar-chart.json"></div> -->
77-
<!-- <div class="react-container" data-config="/examples/feature/bar/horizontal-chart-max-increase.json"></div> -->
78-
<div class="react-container" data-config="/examples/feature/bar/horizontal-chart.json"></div>
79-
<!-- <div class="react-container" data-config="/examples/feature/bar/horizontal-stacked-bar-chart.json"></div> -->
80-
<!-- <div class="react-container" data-config="/examples/feature/bar/planet-chart-horizontal-example-config.json"></div> -->
77+
<div class="react-container" data-config="/examples/feature/bar/horizontal-chart-max-increase.json"></div>
78+
<!-- <div class="react-container" data-config="/examples/feature/bar/horizontal-chart.json"></div> -->
8179

8280
<!-- TESTS DATA TABLE SORTING -->
8381
<!-- Bar Chart with Confidence Intervals (bottom of page) -->
@@ -108,10 +106,8 @@
108106
<!-- TESTS NONNUMERICS -->
109107
<!-- <div class="react-container"
110108
data-config="/examples/feature/tests-non-numerics/planet-pie-example-config-nonnumeric.json"></div> -->
111-
<!-- <div class="react-container" data-config="/examples/feature/tests-non-numerics/example-combo-bar-nonnumeric.json">
112-
</div> -->
113-
<!-- <div class="react-container" data-config="/examples/feature/tests-non-numerics/example-bar-chart-nonnumeric.json">
114-
</div> -->
109+
<!-- <div class="react-container" data-config="/examples/feature/tests-non-numerics/example-combo-bar-nonnumeric.json"></div> -->
110+
<!-- <div class="react-container" data-config="/examples/feature/tests-non-numerics/example-bar-chart-nonnumeric.json"></div> -->
115111
<!-- <div class="react-container" data-config="/examples/sparkline.json"></div> -->
116112
<!-- <div class="react-container" data-config="/examples/feature/tests-non-numerics/sparkline-chart-nonnumeric.json"></div> -->
117113
<!-- <div class="react-container" data-config="/examples/region-issue.json"></div> -->

packages/chart/src/CdcChartComponent.tsx

Lines changed: 128 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState, useEffect, useCallback, useRef, useId, useContext, useReducer } from 'react'
1+
import React, { useState, useEffect, useCallback, useRef, useId, useContext, useReducer, useMemo } from 'react'
22

33
// IE11
44
import ResizeObserver from 'resize-observer-polyfill'
@@ -47,6 +47,7 @@ import { lineOptions } from './helpers/lineOptions'
4747
import { handleLineType } from './helpers/handleLineType'
4848
import { handleRankByValue } from './helpers/handleRankByValue'
4949
import { generateColorsArray } from '@cdc/core/helpers/generateColorsArray'
50+
import { processMarkupVariables } from '@cdc/core/helpers/markupProcessor'
5051
import Loading from '@cdc/core/components/Loading'
5152
import Filters from '@cdc/core/components/Filters'
5253
import MediaControls from '@cdc/core/components/MediaControls'
@@ -154,6 +155,73 @@ const CdcChart: React.FC<CdcChartProps> = ({
154155
// Destructure items from config for more readable JSX
155156
let { legend, title } = config
156157

158+
// Process markup variables for text fields (memoized to prevent re-processing on every render)
159+
// Note: XSS Safety - The processed content is parsed using html-react-parser which sanitizes
160+
// HTML input by default. The markup processor returns plain text with user data substituted.
161+
const processedTextFields = useMemo(() => {
162+
if (!config.enableMarkupVariables || !config.markupVariables?.length) {
163+
return {
164+
title,
165+
superTitle: config.superTitle,
166+
introText: config.introText,
167+
legacyFootnotes: config.legacyFootnotes,
168+
description: config.description
169+
}
170+
}
171+
172+
return {
173+
title: title
174+
? processMarkupVariables(title, config.data || [], config.markupVariables, {
175+
isEditor,
176+
filters: config.filters || []
177+
}).processedContent
178+
: title,
179+
superTitle: config.superTitle
180+
? processMarkupVariables(config.superTitle, config.data || [], config.markupVariables, {
181+
isEditor,
182+
filters: config.filters || []
183+
}).processedContent
184+
: config.superTitle,
185+
introText: config.introText
186+
? processMarkupVariables(config.introText, config.data || [], config.markupVariables, {
187+
isEditor,
188+
filters: config.filters || []
189+
}).processedContent
190+
: config.introText,
191+
legacyFootnotes: config.legacyFootnotes
192+
? processMarkupVariables(config.legacyFootnotes, config.data || [], config.markupVariables, {
193+
isEditor,
194+
filters: config.filters || []
195+
}).processedContent
196+
: config.legacyFootnotes,
197+
description: config.description
198+
? processMarkupVariables(config.description, config.data || [], config.markupVariables, {
199+
isEditor,
200+
filters: config.filters || []
201+
}).processedContent
202+
: config.description
203+
}
204+
}, [
205+
config.enableMarkupVariables,
206+
config.markupVariables,
207+
config.data,
208+
config.filters,
209+
title,
210+
config.superTitle,
211+
config.introText,
212+
config.legacyFootnotes,
213+
config.description,
214+
isEditor
215+
])
216+
217+
// Destructure processed values
218+
title = processedTextFields.title
219+
const processedSuperTitle = processedTextFields.superTitle
220+
const processedIntroText = processedTextFields.introText
221+
const processedLegacyFootnotes = processedTextFields.legacyFootnotes
222+
const processedDescription = processedTextFields.description
223+
// Note: Axis labels are processed within updateConfig to ensure they use the correct data
224+
157225
// set defaults on titles if blank AND only in editor
158226
if (isEditor) {
159227
if (!title || title === '') title = 'Chart Title'
@@ -213,6 +281,25 @@ const CdcChart: React.FC<CdcChartProps> = ({
213281

214282
data = handleRankByValue(data, newConfig)
215283

284+
// Process axis labels for markup variables if enabled
285+
let processedXAxis = newConfig.xAxis?.label
286+
let processedYAxis = newConfig.yAxis?.label
287+
288+
if (newConfig.enableMarkupVariables && newConfig.markupVariables?.length) {
289+
if (newConfig.xAxis?.label) {
290+
processedXAxis = processMarkupVariables(newConfig.xAxis.label, data || [], newConfig.markupVariables, {
291+
isEditor,
292+
filters: newConfig.filters || []
293+
}).processedContent
294+
}
295+
if (newConfig.yAxis?.label) {
296+
processedYAxis = processMarkupVariables(newConfig.yAxis.label, data || [], newConfig.markupVariables, {
297+
isEditor,
298+
filters: newConfig.filters || []
299+
}).processedContent
300+
}
301+
}
302+
216303
// Deeper copy
217304
Object.keys(defaults).forEach(key => {
218305
if (newConfig[key] && 'object' === typeof newConfig[key] && !Array.isArray(newConfig[key])) {
@@ -311,8 +398,15 @@ const CdcChart: React.FC<CdcChartProps> = ({
311398
newConfig.orientation === 'horizontal') ||
312399
['Deviation Bar', 'Paired Bar', 'Forest Plot'].includes(newConfig.visualizationType)
313400
) {
314-
newConfig.runtime.xAxis = _.cloneDeep(newConfig.yAxis.yAxis || newConfig.yAxis)
315-
newConfig.runtime.yAxis = _.cloneDeep(newConfig.xAxis.xAxis || newConfig.xAxis)
401+
// For horizontal charts, axes are swapped, so processedYAxis goes to runtime.xAxis and vice versa
402+
newConfig.runtime.xAxis = {
403+
..._.cloneDeep(newConfig.yAxis.yAxis || newConfig.yAxis),
404+
label: processedYAxis || (newConfig.yAxis.yAxis || newConfig.yAxis).label
405+
}
406+
newConfig.runtime.yAxis = {
407+
..._.cloneDeep(newConfig.xAxis.xAxis || newConfig.xAxis),
408+
label: processedXAxis || (newConfig.xAxis.xAxis || newConfig.xAxis).label
409+
}
316410
newConfig.runtime.yAxis.labelOffset *= -1
317411

318412
newConfig.runtime.horizontal = false
@@ -323,13 +417,13 @@ const CdcChart: React.FC<CdcChartProps> = ({
323417
['Scatter Plot', 'Area Chart', 'Line', 'Forecasting'].includes(newConfig.visualizationType) &&
324418
!convertLineToBarGraph
325419
) {
326-
newConfig.runtime.xAxis = newConfig.xAxis
327-
newConfig.runtime.yAxis = newConfig.yAxis
420+
newConfig.runtime.xAxis = { ...newConfig.xAxis, label: processedXAxis || newConfig.xAxis.label }
421+
newConfig.runtime.yAxis = { ...newConfig.yAxis, label: processedYAxis || newConfig.yAxis.label }
328422
newConfig.runtime.horizontal = false
329423
newConfig.orientation = 'vertical'
330424
} else {
331-
newConfig.runtime.xAxis = newConfig.xAxis
332-
newConfig.runtime.yAxis = newConfig.yAxis
425+
newConfig.runtime.xAxis = { ...newConfig.xAxis, label: processedXAxis || newConfig.xAxis.label }
426+
newConfig.runtime.yAxis = { ...newConfig.yAxis, label: processedYAxis || newConfig.yAxis.label }
333427
newConfig.runtime.horizontal = false
334428
}
335429

@@ -446,8 +540,16 @@ const CdcChart: React.FC<CdcChartProps> = ({
446540
} else if (newConfig.formattedData) {
447541
newConfig.data = newConfig.formattedData
448542
} else if (newConfig.dataDescription) {
449-
newConfig.data = transform.autoStandardize(newConfig.data)
450-
newConfig.data = transform.developerStandardize(newConfig.data, newConfig.dataDescription)
543+
// For dashboard contexts, get data from datasets if config.data is undefined
544+
let dataToProcess = newConfig.data
545+
if (!dataToProcess && isDashboard && datasets && newConfig.dataKey) {
546+
dataToProcess = datasets[newConfig.dataKey]?.data
547+
}
548+
549+
if (dataToProcess) {
550+
newConfig.data = transform.autoStandardize(dataToProcess)
551+
newConfig.data = transform.developerStandardize(newConfig.data, newConfig.dataDescription)
552+
}
451553
}
452554
} catch (err) {
453555
console.error('Error on prepareData function ', err)
@@ -919,7 +1021,7 @@ const CdcChart: React.FC<CdcChartProps> = ({
9191021
showTitle={config.showTitle}
9201022
isDashboard={isDashboard}
9211023
title={title}
922-
superTitle={config.superTitle}
1024+
superTitle={processedSuperTitle}
9231025
classes={['chart-title', `${config.theme}`, 'cove-component__header', 'mb-3']}
9241026
style={undefined}
9251027
config={config}
@@ -928,8 +1030,8 @@ const CdcChart: React.FC<CdcChartProps> = ({
9281030
{/* Visualization Wrapper */}
9291031
<div className={getChartWrapperClasses().join(' ')}>
9301032
{/* Intro Text/Message */}
931-
{config?.introText && config.visualizationType !== 'Spark Line' && (
932-
<section className={`introText mb-4`}>{parse(config.introText)}</section>
1033+
{processedIntroText && config.visualizationType !== 'Spark Line' && (
1034+
<section className={`introText mb-4`}>{parse(processedIntroText)}</section>
9331035
)}
9341036

9351037
{/* Filters */}
@@ -976,7 +1078,14 @@ const CdcChart: React.FC<CdcChartProps> = ({
9761078

9771079
{config.visualizationType === 'Pie' && (
9781080
<ParentSize className='justify-content-center d-flex' style={{ width: `100%` }}>
979-
{parent => <PieChart ref={svgRef} parentWidth={parent.width} parentHeight={parent.height} />}
1081+
{parent => (
1082+
<PieChart
1083+
ref={svgRef}
1084+
parentWidth={parent.width}
1085+
parentHeight={parent.height}
1086+
interactionLabel={interactionLabel}
1087+
/>
1088+
)}
9801089
</ParentSize>
9811090
)}
9821091
{/* Line Chart */}
@@ -1020,9 +1129,9 @@ const CdcChart: React.FC<CdcChartProps> = ({
10201129
dimensions={dimensions}
10211130
interactionLabel={interactionLabel}
10221131
/>
1023-
{config?.introText && (
1132+
{processedIntroText && (
10241133
<section className='introText mb-4' style={{ padding: '0px 0 35px' }}>
1025-
{parse(config.introText)}
1134+
{parse(processedIntroText)}
10261135
</section>
10271136
)}
10281137
<div style={{ height: `100px`, width: `100%`, ...sparkLineStyles }}>
@@ -1059,8 +1168,8 @@ const CdcChart: React.FC<CdcChartProps> = ({
10591168
: link && link}
10601169
{/* Description */}
10611170

1062-
{config.description && config.visualizationType !== 'Spark Line' && (
1063-
<div className={getChartSubTextClasses().join(' ')}>{parse(config.description)}</div>
1171+
{processedDescription && config.visualizationType !== 'Spark Line' && (
1172+
<div className={getChartSubTextClasses().join(' ')}>{parse(processedDescription)}</div>
10641173
)}
10651174

10661175
{/* buttons */}
@@ -1121,8 +1230,8 @@ const CdcChart: React.FC<CdcChartProps> = ({
11211230
)}
11221231
{config?.annotations?.length > 0 && <Annotation.Dropdown />}
11231232
{/* show pdf or image button */}
1124-
{config?.legacyFootnotes && (
1125-
<section className='footnotes pt-2 mt-4'>{parse(config.legacyFootnotes)}</section>
1233+
{processedLegacyFootnotes && (
1234+
<section className='footnotes pt-2 mt-4'>{parse(processedLegacyFootnotes)}</section>
11261235
)}
11271236
</div>
11281237
<FootnotesStandAlone

packages/chart/src/components/EditorPanel/EditorPanel.tsx

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { Select, TextField, CheckBox } from '@cdc/core/components/EditorPanel/In
2323
import MultiSelect from '@cdc/core/components/MultiSelect'
2424
import { viewports } from '@cdc/core/helpers/getViewport'
2525
import { approvedCurveTypes } from '@cdc/core/helpers/lineChartHelpers'
26+
import PanelMarkup from '@cdc/core/components/EditorPanel/components/PanelMarkup'
2627

2728
// chart components
2829
import Panels from './components/Panels'
@@ -905,8 +906,36 @@ const EditorPanel: React.FC<ChartEditorPanelProps> = ({ datasets }) => {
905906

906907
const getColumns = (filter = true) => {
907908
let columns = {}
908-
unfilteredData.forEach(row => {
909-
Object.keys(row).forEach(columnName => (columns[columnName] = true))
909+
910+
// Try multiple data sources in order of preference
911+
let dataToUse = []
912+
913+
if (unfilteredData && unfilteredData.length > 0) {
914+
// First preference: unfilteredData from context
915+
dataToUse = unfilteredData
916+
} else if (isDashboard && datasets && config.dataKey && datasets[config.dataKey]?.data?.length > 0) {
917+
// Second preference: data from datasets in dashboard mode
918+
dataToUse = datasets[config.dataKey].data
919+
} else if (rawData && rawData.length > 0) {
920+
// Third preference: rawData from context
921+
dataToUse = rawData
922+
} else if (data && data.length > 0) {
923+
// Fourth preference: transformedData from context
924+
dataToUse = data
925+
} else if (config.data && config.data.length > 0) {
926+
// Fifth preference: data directly from config
927+
dataToUse = config.data
928+
}
929+
930+
// If we still don't have data, return empty array
931+
if (!dataToUse || dataToUse.length === 0) {
932+
return []
933+
}
934+
935+
dataToUse.forEach(row => {
936+
if (row && typeof row === 'object') {
937+
Object.keys(row).forEach(columnName => (columns[columnName] = true))
938+
}
910939
})
911940

912941
if (filter) {
@@ -4424,6 +4453,14 @@ const EditorPanel: React.FC<ChartEditorPanelProps> = ({ datasets }) => {
44244453
)}
44254454
<Panels.Annotate name='Text Annotations' />
44264455
{/* {(config.visualizationType === 'Bar' || config.visualizationType === 'Line') && <Panels.DateHighlighting name='Date Highlighting' />} */}
4456+
<PanelMarkup
4457+
name='Markup Variables'
4458+
markupVariables={config.markupVariables || []}
4459+
data={rawData}
4460+
enableMarkupVariables={config.enableMarkupVariables || false}
4461+
onMarkupVariablesChange={variables => updateField(null, null, 'markupVariables', variables)}
4462+
onToggleEnable={enabled => updateField(null, null, 'enableMarkupVariables', enabled)}
4463+
/>
44274464
</Accordion>
44284465
{config.type !== 'Spark Line' && (
44294466
<AdvancedEditor loadConfig={updateConfig} config={config} convertStateToConfig={convertStateToConfig} />

0 commit comments

Comments
 (0)