@cdc/core 4.26.1 → 4.26.3

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 (249) hide show
  1. package/.claude/agents/qa-test-developer.md +126 -0
  2. package/CLAUDE.local.md +67 -0
  3. package/LICENSE +201 -0
  4. package/_stories/Gallery.Charts.stories.tsx +35 -42
  5. package/_stories/Gallery.DataBite.stories.tsx +15 -8
  6. package/_stories/Gallery.Maps.stories.tsx +37 -28
  7. package/_stories/Gallery.WaffleChart.stories.tsx +1 -1
  8. package/_stories/PageART.stories.tsx +5 -4
  9. package/_stories/PageBRFSS.stories.tsx +21 -16
  10. package/_stories/PageCancerRegistries.stories.tsx +15 -15
  11. package/_stories/PageEasternEquineEncephalitis.stories.tsx +33 -19
  12. package/_stories/PageExcessiveAlcoholUse.stories.tsx +148 -143
  13. package/_stories/PageMaternalMortality.stories.tsx +5 -4
  14. package/_stories/PageOralHealth.stories.tsx +15 -10
  15. package/_stories/PageRespiratory.stories.tsx +4 -4
  16. package/_stories/PageSmokingTobacco.stories.tsx +15 -10
  17. package/_stories/PageStateDiabetesProfiles.stories.tsx +15 -10
  18. package/_stories/PageWastewater.stories.tsx +44 -30
  19. package/_stories/VegaImport.stories.tsx +401 -0
  20. package/_stories/vega-fixtures/bars-with-line.json +444 -0
  21. package/_stories/vega-fixtures/bars.json +58 -0
  22. package/_stories/vega-fixtures/combo-bar-rolling-mean.json +88 -0
  23. package/_stories/vega-fixtures/combo.json +68 -0
  24. package/_stories/vega-fixtures/grouped-horizontal-bars.json +83 -0
  25. package/_stories/vega-fixtures/grouped-horizontal-bars2.json +231 -0
  26. package/_stories/vega-fixtures/horizontal-bar.json +427 -0
  27. package/_stories/vega-fixtures/horizontal-bars-with-bad-colors.json +197 -0
  28. package/_stories/vega-fixtures/horizontal-bars2.json +58 -0
  29. package/_stories/vega-fixtures/lines.json +227 -0
  30. package/_stories/vega-fixtures/measles-bars.json +348 -0
  31. package/_stories/vega-fixtures/measles-map.json +11101 -0
  32. package/_stories/vega-fixtures/measles-stacked-bars.json +2147 -0
  33. package/_stories/vega-fixtures/multi-dataset.json +255 -0
  34. package/_stories/vega-fixtures/no-data.json +14 -0
  35. package/_stories/vega-fixtures/pie-chart.json +94 -0
  36. package/_stories/vega-fixtures/repeat-spec.json +47 -0
  37. package/_stories/vega-fixtures/stacked-area.json +222 -0
  38. package/_stories/vega-fixtures/stacked-bar-with-rect.json +3412 -0
  39. package/_stories/vega-fixtures/stacked-bars-with-line.json +364 -0
  40. package/_stories/vega-fixtures/stacked-bars.json +212 -0
  41. package/_stories/vega-fixtures/stacked-horizontal-bars.json +140 -0
  42. package/_stories/vega-fixtures/warning-combo.json +59 -0
  43. package/_stories/vega-fixtures/warning-scatter-and-line.json +1182 -0
  44. package/assets/callout-flag.svg +7 -0
  45. package/assets/icon-chart-area.svg +1 -0
  46. package/assets/icon-chart-radar.svg +23 -0
  47. package/assets/logo2.svg +31 -0
  48. package/components/AdvancedEditor/EmbedEditor.tsx +270 -38
  49. package/components/Alert/components/Alert.styles.css +2 -2
  50. package/components/ComboBox/combobox.styles.css +48 -48
  51. package/components/CustomColorsEditor/CustomColorsEditor.css +53 -53
  52. package/components/CustomColorsEditor/CustomColorsEditor.tsx +3 -10
  53. package/components/DataTable/DataTable.tsx +46 -18
  54. package/components/DataTable/DataTableStandAlone.tsx +1 -0
  55. package/components/DataTable/components/ChartHeader.tsx +21 -12
  56. package/components/DataTable/components/MapHeader.tsx +34 -28
  57. package/components/DataTable/components/SortIcon/sort-icon.css +5 -5
  58. package/components/DataTable/data-table.css +50 -52
  59. package/components/DataTable/helpers/applyCustomOrder.ts +17 -0
  60. package/components/DataTable/helpers/getChartCellValue.ts +10 -7
  61. package/components/DataTable/helpers/getMapDataTableColumnKeys.ts +22 -0
  62. package/components/DataTable/helpers/getSeriesName.ts +6 -0
  63. package/components/DataTable/helpers/mapCellMatrix.tsx +33 -23
  64. package/components/DataTable/helpers/tests/mapCellMatrix.test.ts +33 -0
  65. package/components/DownloadButton.tsx +14 -6
  66. package/components/EditorPanel/ColumnsEditor.tsx +38 -31
  67. package/components/EditorPanel/CustomSortOrder.tsx +94 -0
  68. package/components/EditorPanel/DataTableEditor.tsx +139 -23
  69. package/components/EditorPanel/EditorPanel.styles.css +71 -71
  70. package/components/EditorPanel/EditorPanel.tsx +3 -8
  71. package/components/EditorPanel/EditorPanelDispatch.tsx +4 -4
  72. package/components/EditorPanel/FootnotesEditor.tsx +2 -2
  73. package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +21 -12
  74. package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +16 -10
  75. package/components/EditorPanel/VizFilterEditor/components/FilterOrder.tsx +33 -29
  76. package/components/EditorPanel/components/MarkupVariablesEditor.tsx +160 -106
  77. package/components/EditorPanel/components/PanelMarkup.tsx +5 -1
  78. package/{styles/v2/components → components/EditorPanel}/editor.scss +76 -22
  79. package/components/EditorPanel/sections/StyleTreatmentSection.tsx +99 -0
  80. package/components/EditorPanel/sections/VisualSection.tsx +11 -0
  81. package/components/EditorWrapper/editor-wrapper.style.css +1 -1
  82. package/components/Filters/Filters.tsx +3 -5
  83. package/components/Filters/components/Tabs.tsx +19 -7
  84. package/{styles → components/Filters}/filters.scss +3 -3
  85. package/components/Footnotes/FootnotesStandAlone.tsx +4 -2
  86. package/components/HeaderThemeSelector/HeaderThemeSelector.css +61 -5
  87. package/components/Layout/components/Responsive.tsx +14 -6
  88. package/components/Layout/components/Sidebar/components/Sidebar.tsx +1 -1
  89. package/components/Layout/components/Sidebar/components/sidebar.styles.scss +14 -20
  90. package/components/Layout/components/Visualization/index.tsx +50 -38
  91. package/components/Layout/components/Visualization/visualizations.scss +232 -15
  92. package/components/Layout/components/VisualizationContainer.test.tsx +67 -0
  93. package/components/Layout/components/VisualizationContainer.tsx +37 -0
  94. package/components/Layout/components/VisualizationContent.test.tsx +182 -0
  95. package/components/Layout/components/VisualizationContent.tsx +75 -0
  96. package/components/Layout/index.tsx +5 -5
  97. package/components/Layout/styles/editor-utils.scss +3 -3
  98. package/components/Layout/styles/editor.scss +4 -4
  99. package/components/Legend/Legend.Gradient.tsx +7 -1
  100. package/components/Loader/loader.styles.css +2 -2
  101. package/components/Loading.jsx +1 -1
  102. package/components/MediaControls.tsx +10 -3
  103. package/components/MultiSelect/multiselect.styles.css +19 -19
  104. package/components/NestedDropdown/nesteddropdown.styles.css +15 -15
  105. package/components/PaletteSelector/PaletteSelector.css +15 -15
  106. package/components/RichTooltip/richTooltip.css +6 -6
  107. package/components/Table/table.styles.css +2 -2
  108. package/components/Waiting.tsx +1 -1
  109. package/components/_stories/CustomColorsEditor.stories.tsx +37 -0
  110. package/components/_stories/DataTable.stories.tsx +1 -0
  111. package/components/_stories/Filters.stories.tsx +1 -1
  112. package/components/_stories/styles.scss +0 -1
  113. package/components/elements/Button.jsx +1 -1
  114. package/components/elements/Card.jsx +1 -1
  115. package/{styles/v2/components → components/elements}/button.scss +9 -8
  116. package/components/inputs/InputCheckbox.jsx +1 -1
  117. package/components/inputs/InputSelect.tsx +1 -1
  118. package/components/inputs/InputText.jsx +1 -1
  119. package/components/inputs/InputToggle.tsx +1 -1
  120. package/{styles/v2/components/input → components/inputs}/_input-check-radio.scss +2 -2
  121. package/{styles/v2/components/input → components/inputs}/_input-group.scss +3 -3
  122. package/{styles/v2/components/input → components/inputs}/_input-slider.scss +2 -2
  123. package/{styles/v2/components/input → components/inputs}/_input.scss +5 -5
  124. package/{styles/v2/components/input → components/inputs}/index.scss +2 -2
  125. package/{styles → components}/loading.scss +1 -1
  126. package/components/managers/DataDesigner.tsx +1 -1
  127. package/{styles/v2/components → components/managers}/data-designer.scss +6 -7
  128. package/components/ui/Accordion.jsx +1 -1
  129. package/components/ui/Icon.tsx +1 -1
  130. package/components/ui/LoadSpin.jsx +1 -1
  131. package/components/ui/Modal.jsx +1 -1
  132. package/components/ui/Overlay.jsx +1 -1
  133. package/components/ui/Title/index.test.tsx +34 -0
  134. package/components/ui/Title/index.tsx +24 -7
  135. package/components/ui/Title/title.styles.css +119 -25
  136. package/components/ui/Tooltip.tsx +1 -1
  137. package/components/ui/_stories/Title.stories.tsx +1 -1
  138. package/{styles/v2/components → components/ui}/accordion.scss +3 -3
  139. package/components/ui/accordion.styles.css +11 -11
  140. package/{styles/v2/components → components/ui}/modal.scss +2 -2
  141. package/{styles/v2/components → components/ui}/overlay.scss +6 -6
  142. package/{styles/v2/components → components}/ui/tooltip.scss +1 -1
  143. package/{styles → components}/waiting.scss +9 -3
  144. package/data/colorPalettes.ts +18 -5
  145. package/data/mapColorPalettes.ts +10 -0
  146. package/devTemplate/dev.js +285 -0
  147. package/devTemplate/index.html +30 -0
  148. package/devTemplate/preview.html +1503 -0
  149. package/devTemplate/sidebar.css +151 -0
  150. package/dist/cove-main.css +2530 -3901
  151. package/dist/cove-main.css.map +1 -1
  152. package/generateViteConfig.js +111 -2
  153. package/helpers/DataTransform.ts +1 -5
  154. package/helpers/backfillDefaults.ts +35 -0
  155. package/helpers/constants.ts +12 -0
  156. package/helpers/cove/date.ts +64 -3
  157. package/helpers/cove/number.ts +29 -15
  158. package/helpers/cove/string.ts +29 -0
  159. package/helpers/coveUpdateWorker.ts +14 -8
  160. package/helpers/displayDataAsText.ts +1 -1
  161. package/helpers/embed/embedCodeGenerator.ts +80 -0
  162. package/helpers/embed/embedHelper.js +169 -0
  163. package/helpers/embed/filterUtils.ts +121 -0
  164. package/helpers/embed/index.ts +17 -0
  165. package/helpers/embed/urlValidation.ts +119 -0
  166. package/helpers/extractDataAndMetadata.ts +20 -0
  167. package/helpers/fetchRemoteData.ts +14 -8
  168. package/helpers/filterVizData.ts +6 -1
  169. package/helpers/getFileExtension.ts +0 -6
  170. package/helpers/labelHash.ts +9 -0
  171. package/helpers/markupProcessor.ts +56 -38
  172. package/helpers/metrics/types.ts +3 -0
  173. package/helpers/palettes/colorDistributions.ts +1 -1
  174. package/helpers/palettes/utils.ts +12 -12
  175. package/helpers/parseCsvWithQuotes.ts +15 -14
  176. package/helpers/prepareScreenshot.ts +33 -10
  177. package/helpers/testing.ts +44 -0
  178. package/helpers/tests/DataTransform.test.ts +125 -0
  179. package/helpers/tests/abbreviateNumber.test.ts +59 -0
  180. package/helpers/tests/backfillDefaults.test.ts +253 -0
  181. package/helpers/tests/date.test.ts +110 -0
  182. package/helpers/tests/extractDataAndMetadata.test.ts +93 -0
  183. package/helpers/tests/markupProcessor.test.ts +315 -124
  184. package/helpers/tests/number.test.ts +42 -0
  185. package/helpers/tests/prepareScreenshot.test.ts +28 -28
  186. package/helpers/tests/testStandaloneBuild.ts +36 -26
  187. package/helpers/tests/useDataVizClasses.test.ts +66 -0
  188. package/helpers/tests/visualizationWrapperUsage.test.ts +57 -0
  189. package/helpers/useDataVizClasses.ts +13 -7
  190. package/helpers/vegaConfig.ts +1 -1
  191. package/helpers/vegaConfigImport.ts +160 -0
  192. package/helpers/ver/4.24.4.ts +24 -0
  193. package/helpers/ver/4.26.1.ts +1 -1
  194. package/helpers/ver/4.26.2.ts +84 -0
  195. package/helpers/ver/4.26.3.ts +44 -0
  196. package/helpers/ver/4.26.4.ts +31 -0
  197. package/helpers/ver/tests/4.26.1.test.ts +105 -0
  198. package/helpers/ver/tests/4.26.2.test.ts +298 -0
  199. package/helpers/ver/tests/4.26.3.test.ts +168 -0
  200. package/helpers/ver/tests/4.26.4.test.ts +88 -0
  201. package/helpers/ver/tests/coveUpdateWorker.test.ts +57 -0
  202. package/helpers/viewports.ts +2 -0
  203. package/package.json +27 -32
  204. package/styles/_global.scss +7 -7
  205. package/styles/_reset.scss +2 -2
  206. package/styles/{v2/base → base}/_file-selector.scss +4 -4
  207. package/styles/{v2/base → base}/_general.scss +2 -4
  208. package/styles/{v2/base → base}/index.scss +1 -1
  209. package/styles/base.scss +107 -165
  210. package/styles/cove-main.scss +3 -6
  211. package/styles/layout/_component.scss +110 -0
  212. package/styles/{v2/layout → layout}/_data-table.scss +7 -7
  213. package/styles/layout/_wrapper-padding.scss +27 -0
  214. package/styles/{v2/main.scss → main.scss} +3 -1
  215. package/styles/{v2/themes → themes}/_color-definitions.scss +46 -41
  216. package/styles/{_accessibility.scss → utils/_accessibility.scss} +1 -1
  217. package/styles/{v2/utils → utils}/_grid.scss +8 -3
  218. package/styles/{_global-variables.scss → utils/_properties.scss} +133 -112
  219. package/styles/{v2/utils → utils}/index.scss +2 -1
  220. package/types/Annotation.ts +10 -11
  221. package/types/Axis.ts +2 -0
  222. package/types/ComponentStyles.ts +1 -0
  223. package/types/ConfigureData.ts +1 -0
  224. package/types/General.ts +2 -0
  225. package/types/MarkupInclude.ts +1 -0
  226. package/types/MarkupVariable.ts +2 -1
  227. package/types/Palette.ts +22 -0
  228. package/types/Table.ts +9 -0
  229. package/types/Visualization.ts +7 -0
  230. package/_stories/StoryRenderingTests.stories.tsx +0 -164
  231. package/helpers/embedCodeGenerator.ts +0 -109
  232. package/styles/_common-components.css +0 -73
  233. package/styles/_variables.scss +0 -63
  234. package/styles/v2/layout/_component.scss +0 -21
  235. package/styles/v2/utils/_variables.scss +0 -9
  236. package/{styles/v2/components/card.scss → components/elements/card.css} +2 -2
  237. /package/{styles/v2/components → components/ui}/icon.scss +0 -0
  238. /package/{styles/v2/components → components/ui}/loadspin.scss +0 -0
  239. /package/styles/{v2/base → base}/_heading.scss +0 -0
  240. /package/styles/{v2/base → base}/_reset.scss +0 -0
  241. /package/styles/{v2/layout → layout}/_alert.scss +0 -0
  242. /package/styles/{v2/layout → layout}/_progression.scss +0 -0
  243. /package/styles/{v2/layout → layout}/_tooltip.scss +0 -0
  244. /package/styles/{v2/layout → layout}/index.scss +0 -0
  245. /package/styles/{v2/themes → themes}/index.scss +0 -0
  246. /package/styles/{v2/utils → utils}/_align.scss +0 -0
  247. /package/styles/{v2/utils → utils}/_animations.scss +0 -0
  248. /package/styles/{v2/utils → utils}/_breakpoints.scss +0 -0
  249. /package/styles/{v2/utils → utils}/_mixins.scss +0 -0
@@ -10,21 +10,23 @@ import FieldSetWrapper from './FieldSetWrapper'
10
10
  import { useDataColumns } from '../../hooks/useDataColumns'
11
11
  import Alert from '../Alert/components/Alert'
12
12
 
13
- interface ColumnsEditorProps {
14
- config: Partial<Visualization>
15
- updateField: UpdateFieldFunc<string | boolean | string[] | number | Column | Record<string, Partial<Column>>>
16
- deleteColumn: (colName: string) => void
17
- }
13
+ interface ColumnsEditorProps {
14
+ config: Partial<Visualization>
15
+ updateField: UpdateFieldFunc<string | boolean | string[] | number | Column | Record<string, Partial<Column>>>
16
+ deleteColumn: (colName: string) => void
17
+ hiddenColumnNames?: string[]
18
+ }
18
19
 
19
20
  type OpenControls = [Record<string, boolean>, Function] // useState type
20
21
 
21
- const FieldSet: React.FC<ColumnsEditorProps & { colKey: string; controls: OpenControls }> = ({
22
- config,
23
- deleteColumn,
24
- updateField,
25
- colKey,
26
- controls
27
- }) => {
22
+ const FieldSet: React.FC<ColumnsEditorProps & { colKey: string; controls: OpenControls }> = ({
23
+ config,
24
+ deleteColumn,
25
+ updateField,
26
+ hiddenColumnNames = [],
27
+ colKey,
28
+ controls
29
+ }) => {
28
30
  const [openControls, setOpenControls] = controls
29
31
 
30
32
  const editColumn = (key, value) => {
@@ -58,17 +60,18 @@ const FieldSet: React.FC<ColumnsEditorProps & { colKey: string; controls: OpenCo
58
60
  const allColumns = useDataColumns(config.data)
59
61
 
60
62
  // Filter out groupBy and already configured columns
61
- const availableColumns = useMemo(() => {
62
- const configuredColumns = Object.values(config.columns).map(col => col.name)
63
- const cols = allColumns.filter(key => {
64
- if (config.table.groupBy === key) return false
65
- if (configuredColumns.includes(key)) return false
66
- return true
67
- })
68
- // Add current column name if it exists
69
- if (config.columns[colKey]?.name) cols.push(config.columns[colKey].name)
70
- return cols
71
- }, [allColumns, config.table.groupBy, config.columns, colKey])
63
+ const availableColumns = useMemo(() => {
64
+ const configuredColumns = Object.values(config.columns).map(col => col.name)
65
+ const cols = allColumns.filter(key => {
66
+ if (config.table.groupBy === key) return false
67
+ if (configuredColumns.includes(key)) return false
68
+ if (hiddenColumnNames.includes(key)) return false
69
+ return true
70
+ })
71
+ // Add current column name if it exists
72
+ if (config.columns[colKey]?.name) cols.push(config.columns[colKey].name)
73
+ return cols
74
+ }, [allColumns, config.table.groupBy, config.columns, colKey, hiddenColumnNames])
72
75
 
73
76
  const colName = config.columns[colKey]?.name
74
77
 
@@ -264,9 +267,12 @@ const FieldSet: React.FC<ColumnsEditorProps & { colKey: string; controls: OpenCo
264
267
  )
265
268
  }
266
269
 
267
- const ColumnsEditor: React.FC<ColumnsEditorProps> = ({ config, updateField, deleteColumn }) => {
268
- const openControls = useState({})
269
- const additionalColumns = Object.keys(config.columns)
270
+ const ColumnsEditor: React.FC<ColumnsEditorProps> = ({ config, updateField, deleteColumn, hiddenColumnNames = [] }) => {
271
+ const openControls = useState({})
272
+ const additionalColumns = Object.keys(config.columns).filter(colKey => {
273
+ const columnName = config.columns[colKey]?.name || colKey
274
+ return !hiddenColumnNames.includes(columnName)
275
+ })
270
276
 
271
277
  // just adds a new column but not set to any data yet
272
278
  const addColumnConfig = number => {
@@ -312,11 +318,12 @@ const ColumnsEditor: React.FC<ColumnsEditorProps> = ({ config, updateField, dele
312
318
  key={val + i}
313
319
  controls={openControls}
314
320
  config={config}
315
- deleteColumn={deleteColumn}
316
- updateField={updateField}
317
- colKey={val}
318
- />
319
- ))}
321
+ deleteColumn={deleteColumn}
322
+ updateField={updateField}
323
+ hiddenColumnNames={hiddenColumnNames}
324
+ colKey={val}
325
+ />
326
+ ))}
320
327
  <button
321
328
  className={'btn btn-primary'}
322
329
  onClick={event => {
@@ -0,0 +1,94 @@
1
+ import { useCallback, useMemo } from 'react'
2
+ import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd'
3
+ import _ from 'lodash'
4
+
5
+ type CustomSortOrderProps = {
6
+ column: string
7
+ data: Record<string, any>[]
8
+ customOrder?: string[]
9
+ updateField: Function
10
+ /** Optional transform for display labels (e.g. displayGeoName for maps) */
11
+ displayTransform?: (value: string) => string
12
+ }
13
+
14
+ /**
15
+ * Editor component for drag-and-drop custom sort ordering.
16
+ * Shows unique values from the selected column and allows reordering via DnD.
17
+ */
18
+ const CustomSortOrder: React.FC<CustomSortOrderProps> = ({
19
+ column,
20
+ data,
21
+ customOrder,
22
+ updateField,
23
+ displayTransform
24
+ }) => {
25
+ // Compute unique values from the selected column
26
+ const uniqueValues = useMemo(() => {
27
+ if (!column || !data?.length) return []
28
+ const values = data.map(row => String(row[column] ?? '')).filter(v => v !== '')
29
+ return _.uniq(values)
30
+ }, [column, data])
31
+
32
+ // Use customOrder if set, otherwise fall back to natural unique order
33
+ const orderedValues = useMemo(() => {
34
+ if (customOrder?.length) {
35
+ // Include any new values from data that aren't in customOrder
36
+ const extra = uniqueValues.filter(v => !customOrder.includes(v))
37
+ return [...customOrder.filter(v => uniqueValues.includes(v)), ...extra]
38
+ }
39
+ return uniqueValues
40
+ }, [customOrder, uniqueValues])
41
+
42
+ const handleDragEnd = useCallback(
43
+ ({ source, destination }) => {
44
+ if (!destination || source.index === destination.index) return
45
+ const reordered = [...orderedValues]
46
+ const [moved] = reordered.splice(source.index, 1)
47
+ reordered.splice(destination.index, 0, moved)
48
+ updateField('table', 'defaultSort', 'customOrder', reordered)
49
+ },
50
+ [orderedValues, updateField]
51
+ )
52
+
53
+ if (!orderedValues.length) return null
54
+
55
+ return (
56
+ <div>
57
+ <DragDropContext onDragEnd={handleDragEnd}>
58
+ <Droppable droppableId='custom_sort_order'>
59
+ {provided => (
60
+ <ul
61
+ {...provided.droppableProps}
62
+ className='sort-list'
63
+ ref={provided.innerRef}
64
+ style={{ marginTop: '0.5em', paddingLeft: 0, listStyle: 'none' }}
65
+ >
66
+ {orderedValues.map((value, index) => (
67
+ <Draggable key={value} draggableId={`customSort-${value}`} index={index}>
68
+ {(provided, snapshot) => (
69
+ <li style={{ marginBottom: '2px' }}>
70
+ <div
71
+ className={snapshot.isDragging ? 'currently-dragging' : ''}
72
+ style={{
73
+ ...provided.draggableProps.style
74
+ }}
75
+ ref={provided.innerRef}
76
+ {...provided.draggableProps}
77
+ {...provided.dragHandleProps}
78
+ >
79
+ {displayTransform ? displayTransform(value) : value}
80
+ </div>
81
+ </li>
82
+ )}
83
+ </Draggable>
84
+ ))}
85
+ {provided.placeholder}
86
+ </ul>
87
+ )}
88
+ </Droppable>
89
+ </DragDropContext>
90
+ </div>
91
+ )
92
+ }
93
+
94
+ export default CustomSortOrder
@@ -7,6 +7,7 @@ import { UpdateFieldFunc } from '../../types/UpdateFieldFunc'
7
7
  import { Visualization } from '../../types/Visualization'
8
8
  import _ from 'lodash'
9
9
  import { Column } from '../../types/Column'
10
+ import CustomSortOrder from './CustomSortOrder'
10
11
 
11
12
  interface DataTableProps {
12
13
  config: Partial<Visualization>
@@ -27,7 +28,7 @@ const DataTableEditor: React.FC<DataTableProps> = ({ config, updateField, isDash
27
28
  }, [config.columns])
28
29
 
29
30
  const groupPivotColumns = useMemo(() => {
30
- const columns: string[] = config.data.flatMap(Object.keys)
31
+ const columns: string[] = (config.data ?? []).flatMap(Object.keys)
31
32
  const cols = _.uniq(columns).filter(key => {
32
33
  return true
33
34
  })
@@ -39,6 +40,56 @@ const DataTableEditor: React.FC<DataTableProps> = ({ config, updateField, isDash
39
40
  updateField('table', null, 'groupBy', value)
40
41
  }
41
42
 
43
+ const changeSortColumn = (value: string) => {
44
+ if (value === '' || value === PLACEHOLDER) {
45
+ // Clear entire defaultSort when column is deselected
46
+ updateField('table', null, 'defaultSort', {})
47
+ return
48
+ }
49
+ // Set column and reset to ascending, clear custom order
50
+ updateField('table', null, 'defaultSort', { column: value, sortDirection: 'asc' })
51
+ }
52
+
53
+ const changeSortDirection = (value: string) => {
54
+ const newDefaultSort = { ...config.table.defaultSort, sortDirection: value }
55
+ // Clear customOrder when switching away from custom
56
+ if (value !== 'custom') {
57
+ delete newDefaultSort.customOrder
58
+ } else if (!newDefaultSort.customOrder?.length) {
59
+ // Auto-populate customOrder with unique values so the table updates immediately
60
+ const col = newDefaultSort.column
61
+ const dataCol = config.type === 'map' && config.columns?.[col]?.name ? config.columns[col].name : col
62
+ if (dataCol && config.data?.length) {
63
+ newDefaultSort.customOrder = _.uniq(config.data.map(row => String(row[dataCol] ?? '')).filter(v => v !== ''))
64
+ }
65
+ }
66
+ updateField('table', null, 'defaultSort', newDefaultSort)
67
+ }
68
+
69
+ // Build sort column options based on visualization type
70
+ // Maps use config.columns keys (geo, primary, etc.); other types use data column names
71
+ const sortColumnOptions = useMemo(() => {
72
+ if (config.type === 'map' && config.columns) {
73
+ return Object.keys(config.columns)
74
+ .filter(key => config.columns[key].dataTable !== false && config.columns[key].name)
75
+ .map(key => ({
76
+ label: config.columns[key].label || config.columns[key].name || key,
77
+ value: key
78
+ }))
79
+ }
80
+ return dataColumns.map(col => ({ label: col, value: col }))
81
+ }, [config.type, config.columns, dataColumns])
82
+
83
+ // For custom sort, resolve the actual data column name to get unique values
84
+ const customSortDataColumn = useMemo(() => {
85
+ const col = config.table?.defaultSort?.column
86
+ if (!col) return ''
87
+ if (config.type === 'map' && config.columns?.[col]?.name) {
88
+ return config.columns[col].name
89
+ }
90
+ return col
91
+ }, [config.type, config.columns, config.table?.defaultSort?.column])
92
+
42
93
  const excludeColumns = (section, subSection, fieldName, excludedColNames: string[]) => {
43
94
  const newColumns = _.cloneDeep(config.columns)
44
95
 
@@ -234,6 +285,49 @@ const DataTableEditor: React.FC<DataTableProps> = ({ config, updateField, isDash
234
285
  updateField={updateField}
235
286
  />
236
287
  )}
288
+ <Select
289
+ value={config.table.defaultSort?.column || ''}
290
+ fieldName='column'
291
+ section='table'
292
+ subsection='defaultSort'
293
+ label='Default Sort Column'
294
+ initial={PLACEHOLDER}
295
+ options={sortColumnOptions}
296
+ updateField={(_section, _subSection, _fieldName, value) => changeSortColumn(value)}
297
+ tooltip={
298
+ <Tooltip style={{ textTransform: 'none' }}>
299
+ <Tooltip.Target>
300
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
301
+ </Tooltip.Target>
302
+ <Tooltip.Content>
303
+ <p>Choose a column to sort the data table by when it first loads.</p>
304
+ </Tooltip.Content>
305
+ </Tooltip>
306
+ }
307
+ />
308
+ {config.table.defaultSort?.column && (
309
+ <Select
310
+ value={config.table.defaultSort?.sortDirection || 'asc'}
311
+ fieldName='sortDirection'
312
+ section='table'
313
+ subsection='defaultSort'
314
+ label='Sort Direction'
315
+ options={[
316
+ { label: 'Ascending', value: 'asc' },
317
+ { label: 'Descending', value: 'desc' },
318
+ { label: 'Custom', value: 'custom' }
319
+ ]}
320
+ updateField={(_section, _subSection, _fieldName, value) => changeSortDirection(value)}
321
+ />
322
+ )}
323
+ {config.table.defaultSort?.column && config.table.defaultSort?.sortDirection === 'custom' && (
324
+ <CustomSortOrder
325
+ column={customSortDataColumn}
326
+ data={config.data}
327
+ customOrder={config.table.defaultSort.customOrder}
328
+ updateField={updateField}
329
+ />
330
+ )}
237
331
  <CheckBox
238
332
  value={config.table.download}
239
333
  fieldName='download'
@@ -259,6 +353,16 @@ const DataTableEditor: React.FC<DataTableProps> = ({ config, updateField, isDash
259
353
  section='table'
260
354
  updateField={updateField}
261
355
  />
356
+ <div className='ms-4 mt-2' style={{ maxWidth: 'calc(100% - 1.5rem)' }}>
357
+ <TextField
358
+ value={config.table.downloadDataLabel}
359
+ section='table'
360
+ fieldName='downloadDataLabel'
361
+ label='Download Data Link Text'
362
+ placeholder='Download Data (CSV)'
363
+ updateField={updateField}
364
+ />
365
+ </div>
262
366
  </>
263
367
  )}
264
368
  {isDashboard && config.type !== 'table' && (
@@ -289,28 +393,40 @@ const DataTableEditor: React.FC<DataTableProps> = ({ config, updateField, isDash
289
393
  />
290
394
  )}
291
395
  {config.type !== 'table' && config.table.showDownloadImgButton && (
292
- <CheckBox
293
- value={config.table.includeContextInDownload}
294
- fieldName='includeContextInDownload'
295
- className='ms-4'
296
- label='Include Heading & Context'
297
- section='table'
298
- updateField={updateField}
299
- tooltip={
300
- <Tooltip style={{ textTransform: 'none' }}>
301
- <Tooltip.Target>
302
- <Icon display='question' style={{ marginLeft: '0.5rem' }} />
303
- </Tooltip.Target>
304
- <Tooltip.Content>
305
- <p>
306
- When enabled, the image download will include the section heading (H2 or H3) and any explanatory
307
- paragraphs that appear immediately before the visualization. Be sure to test the image download on the
308
- published page to ensure the correct context is included.
309
- </p>
310
- </Tooltip.Content>
311
- </Tooltip>
312
- }
313
- />
396
+ <>
397
+ <CheckBox
398
+ value={config.table.includeContextInDownload}
399
+ fieldName='includeContextInDownload'
400
+ className='ms-4'
401
+ label='Include Heading & Context'
402
+ section='table'
403
+ updateField={updateField}
404
+ tooltip={
405
+ <Tooltip style={{ textTransform: 'none' }}>
406
+ <Tooltip.Target>
407
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
408
+ </Tooltip.Target>
409
+ <Tooltip.Content>
410
+ <p>
411
+ When enabled, the image download will include the section heading (H2 or H3) and any explanatory
412
+ paragraphs that appear immediately before the visualization. Be sure to test the image download on
413
+ the published page to ensure the correct context is included.
414
+ </p>
415
+ </Tooltip.Content>
416
+ </Tooltip>
417
+ }
418
+ />
419
+ <div className='ms-4 mt-2' style={{ maxWidth: 'calc(100% - 1.5rem)' }}>
420
+ <TextField
421
+ value={config.table.downloadImageLabel}
422
+ section='table'
423
+ fieldName='downloadImageLabel'
424
+ label='Download Image Link Text'
425
+ placeholder={`Download ${config.type === 'map' ? 'Map' : 'Chart'} (PNG)`}
426
+ updateField={updateField}
427
+ />
428
+ </div>
429
+ </>
314
430
  )}
315
431
  <label>
316
432
  <span className='edit-label column-heading'>Table Cell Min Width</span>