@cdc/dashboard 4.25.10 → 4.26.1

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 (86) hide show
  1. package/Dynamic_Data.md +66 -0
  2. package/dist/{cdcdashboard-fce76882.es.js → cdcdashboard-BnB1QM5d.es.js} +6 -13
  3. package/dist/{cdcdashboard-c55ac1ea.es.js → cdcdashboard-D6CG2-Hb.es.js} +5 -12
  4. package/dist/{cdcdashboard-31a33da1.es.js → cdcdashboard-MXgURbdZ.es.js} +6 -13
  5. package/dist/{cdcdashboard-1a1724a1.es.js → cdcdashboard-dgT_1dIT.es.js} +136 -151
  6. package/dist/cdcdashboard.js +84214 -79641
  7. package/examples/api-dashboard-data.json +272 -0
  8. package/examples/api-dashboard-years.json +11 -0
  9. package/examples/api-geographies-data.json +11 -0
  10. package/examples/api-test/categories.json +18 -0
  11. package/examples/api-test/chart-data.json +602 -0
  12. package/examples/api-test/topics.json +47 -0
  13. package/examples/api-test/years.json +22 -0
  14. package/examples/markup-axis-label.json +4167 -0
  15. package/examples/private/big-dashboard.json +39095 -39077
  16. package/examples/private/cat-y.json +1235 -0
  17. package/examples/private/chronic-dash.json +1584 -0
  18. package/examples/private/clade-2.json +430 -0
  19. package/examples/private/diabetes.json +546 -196
  20. package/examples/private/map-issue.json +2260 -0
  21. package/examples/private/markup-footer/mortality-deaths-footnotes-age.csv +3 -0
  22. package/examples/private/mpinc-state-reports.json +2260 -0
  23. package/examples/private/mpox.json +38128 -0
  24. package/examples/private/nwss/rsv.json +1240 -0
  25. package/examples/private/reset.json +32920 -0
  26. package/examples/private/simple-dash.json +490 -0
  27. package/examples/private/test-dash.json +0 -0
  28. package/examples/private/test123.json +491 -0
  29. package/examples/test-api-filter-reset.json +132 -0
  30. package/examples/test-dashboard-simple.json +503 -0
  31. package/index.html +25 -26
  32. package/package.json +11 -11
  33. package/src/CdcDashboardComponent.tsx +35 -10
  34. package/src/DashboardContext.tsx +3 -1
  35. package/src/_stories/Dashboard.DataSetup.stories.tsx +203 -0
  36. package/src/_stories/Dashboard.stories.tsx +402 -1
  37. package/src/_stories/_mock/custom-order-new-values.json +116 -0
  38. package/src/_stories/_mock/filter-cascade.json +3350 -0
  39. package/src/_stories/_mock/gallery-data-bite-dashboard.json +3500 -0
  40. package/src/_stories/_mock/nested-parent-child-filters.json +392 -0
  41. package/src/_stories/_mock/parent-child-filters.json +233 -0
  42. package/src/components/DashboardFilters/DashboardFilters.tsx +54 -31
  43. package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +118 -50
  44. package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +96 -108
  45. package/src/components/DashboardFilters/DashboardFiltersEditor/components/NestedDropDownDashboard.tsx +196 -59
  46. package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +129 -29
  47. package/src/components/DashboardFilters/_stories/DashboardFilters.stories.tsx +62 -3
  48. package/src/components/DataDesignerModal.tsx +18 -6
  49. package/src/components/Header/Header.tsx +53 -21
  50. package/src/components/Toggle/Toggle.tsx +48 -48
  51. package/src/components/VisualizationRow.tsx +73 -6
  52. package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +2 -3
  53. package/src/components/Widget/Widget.tsx +1 -1
  54. package/src/data/initial-state.js +1 -0
  55. package/src/helpers/addValuesToDashboardFilters.ts +24 -6
  56. package/src/helpers/apiFilterHelpers.ts +26 -2
  57. package/src/helpers/changeFilterActive.ts +67 -65
  58. package/src/helpers/filterData.ts +52 -7
  59. package/src/helpers/filterResetHelpers.ts +102 -0
  60. package/src/helpers/formatConfigBeforeSave.ts +6 -5
  61. package/src/helpers/getUpdateConfig.ts +91 -91
  62. package/src/helpers/getVizConfig.ts +2 -2
  63. package/src/helpers/loadAPIFilters.ts +109 -99
  64. package/src/helpers/tests/filterResetHelpers.test.ts +532 -0
  65. package/src/helpers/tests/updatesChildFilters.test.ts +53 -22
  66. package/src/helpers/updateChildFilters.ts +50 -27
  67. package/src/index.tsx +1 -0
  68. package/src/scss/editor-panel.scss +3 -431
  69. package/src/scss/main.scss +142 -25
  70. package/src/store/errorMessage/errorMessage.reducer.ts +1 -1
  71. package/src/test/CdcDashboard.test.jsx +9 -4
  72. package/src/types/Dashboard.ts +1 -0
  73. package/src/types/DashboardFilters.ts +9 -8
  74. package/src/types/FilterStyles.ts +8 -7
  75. package/src/types/SharedFilter.ts +13 -0
  76. package/LICENSE +0 -201
  77. package/examples/private/DEV-11072.json +0 -7591
  78. package/examples/private/burden_toolkit_mortality_diabetes_attributable_deaths_data.csv +0 -14041
  79. package/examples/private/burden_toolkit_mortality_diabetes_attributable_deaths_per_100000_data.csv +0 -14041
  80. package/examples/private/burden_toolkit_mortality_qaly_data.csv +0 -18721
  81. package/examples/private/burden_toolkit_mortality_yll_data.csv +0 -18721
  82. package/examples/private/pedro.json +0 -1
  83. package/src/helpers/getAutoLoadVisualization.ts +0 -11
  84. package/src/scss/mixins.scss +0 -47
  85. package/src/scss/variables.scss +0 -5
  86. /package/dist/{cdcdashboard-548642e6.es.js → cdcdashboard-Ct2SB0vL.es.js} +0 -0
@@ -1,5 +1,6 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-vite'
2
2
  import { faker } from '@faker-js/faker'
3
+ import { waitForOptionsToPopulate, performAndAssert } from '@cdc/core/helpers/testing'
3
4
  import APIFiltersMapData from './_mock/api-filter-map.json'
4
5
  import APIFiltersChartData from './_mock/api-filter-chart.json'
5
6
  import APIFilterErrorConfig from './_mock/api-filter-error.json'
@@ -15,7 +16,7 @@ import StandaloneTable from './_mock/standalone-table.json'
15
16
  import GroupPivotConfig from './_mock/group-pivot-filter.json'
16
17
  import PivotFitlerConfig from './_mock/pivot-filter.json'
17
18
  import { type DashboardConfig as Config } from '../types/DashboardConfig'
18
- import { userEvent, within } from 'storybook/test'
19
+ import { userEvent, within, expect } from 'storybook/test'
19
20
  import ToggleExampleConfig from './_mock/toggle-example.json'
20
21
  import _ from 'lodash'
21
22
  import { footnotesSymbols } from '@cdc/core/helpers/footnoteSymbols'
@@ -28,6 +29,12 @@ import TopSpacing_1 from './_mock/data-bite-dash-test.json'
28
29
  import TopSpacing_2 from './_mock/data-bite-dash-test_1.json'
29
30
  import TopSpacing_3 from './_mock/data-bite-dash-test_1_1.json'
30
31
  import TopSpacing_4 from './_mock/data-bite-dash-test_1_1_1.json'
32
+ import CustomOrderNewValues from './_mock/custom-order-new-values.json'
33
+ import APIFilterResetConfig from '../../examples/test-api-filter-reset.json'
34
+ import CascadingDataFilters from './_mock/filter-cascade.json'
35
+ import ParentChildFilters from './_mock/parent-child-filters.json'
36
+ import NestedParentChildFilters from './_mock/nested-parent-child-filters.json'
37
+ import GalleryDataBiteDashboard from './_mock/gallery-data-bite-dashboard.json'
31
38
 
32
39
  // Dashboard Filter Updates for Ascending, Descending, and Custom Order
33
40
  import DashboardFilterAsc from './_mock/dashboard-filter-asc.json'
@@ -38,6 +45,7 @@ DashboardFilterCust.dashboard.sharedFilters[0].order = 'cust'
38
45
 
39
46
  // On DashboardFilterCust change the sharedFilters[0].values and orderedValues to be in a custom order
40
47
  const customOrder = ['American Samoa', 'Alaska', 'Alabama', 'Arizona', 'Arkansas']
48
+ DashboardFilterCust.dashboard.sharedFilters[0].orderedValues = customOrder
41
49
 
42
50
  const meta: Meta<typeof Dashboard> = {
43
51
  title: 'Components/Pages/Dashboard',
@@ -67,6 +75,13 @@ export const Dashboard_Filter_Cust: Story = {
67
75
  }
68
76
  }
69
77
 
78
+ export const CustomOrder_NewValues_AutoAppend: Story = {
79
+ args: {
80
+ config: CustomOrderNewValues,
81
+ isEditor: false
82
+ }
83
+ }
84
+
70
85
  export const Example_1: Story = {
71
86
  args: {
72
87
  config: ExampleConfig_1,
@@ -472,4 +487,390 @@ export const Top_Spacing_4: Story = {
472
487
  }
473
488
  }
474
489
 
490
+ export const Gallery_Data_Bite_Dashboard: Story = {
491
+ args: {
492
+ config: GalleryDataBiteDashboard,
493
+ isEditor: false
494
+ }
495
+ }
496
+
497
+ export const Clear_Filters_Button: Story = {
498
+ args: {
499
+ config: APIFilterResetConfig as unknown as Config,
500
+ isEditor: false
501
+ }
502
+ }
503
+
504
+ export const Cascading_Multi_Parent_Data_Filters: Story = {
505
+ args: {
506
+ config: CascadingDataFilters as unknown as Config,
507
+ isEditor: false
508
+ },
509
+ parameters: {
510
+ docs: {
511
+ description: {
512
+ story:
513
+ 'Demonstrates cascading data filters with multiple parent relationships. The size filter depends on color, and weight depends on both color and size, creating a multi-tier cascading filter system with deterministic test data.'
514
+ }
515
+ }
516
+ },
517
+ play: async ({ canvasElement }) => {
518
+ const canvas = within(canvasElement)
519
+ const user = userEvent.setup()
520
+
521
+ // Get all filter dropdowns (using findBy to wait for them)
522
+ const colorFilter = (await canvas.findByLabelText('color', { selector: 'select' })) as HTMLSelectElement
523
+ const sizeFilter = (await canvas.findByLabelText('size', { selector: 'select' })) as HTMLSelectElement
524
+ const weightFilter = (await canvas.findByLabelText('weight', { selector: 'select' })) as HTMLSelectElement
525
+ const smellFilter = (await canvas.findByLabelText('smell', { selector: 'select' })) as HTMLSelectElement
526
+
527
+ // Helper to get non-empty filter options
528
+ const getFilterOptions = (filter: HTMLSelectElement) =>
529
+ Array.from(filter.querySelectorAll('option'))
530
+ .map(opt => opt.value)
531
+ .filter(v => v)
532
+
533
+ // Helper to get state of filters and chart
534
+ const getState = () => ({
535
+ colorOptions: getFilterOptions(colorFilter),
536
+ colorSelected: colorFilter.value,
537
+ sizeOptions: getFilterOptions(sizeFilter),
538
+ sizeSelected: sizeFilter.value,
539
+ weightOptions: getFilterOptions(weightFilter),
540
+ weightSelected: weightFilter.value,
541
+ smellOptions: getFilterOptions(smellFilter),
542
+ smellSelected: smellFilter.value,
543
+ chartRendered: !!canvasElement.querySelector('svg'),
544
+ noDataVisible: !!canvas.queryByText('No Data Available')
545
+ })
546
+
547
+ // Initial verification - wait for options to populate
548
+ await waitForOptionsToPopulate(colorFilter, 3)
549
+ const initialState = getState()
550
+ expect(initialState.chartRendered).toBe(true)
551
+ expect(initialState.noDataVisible).toBe(false)
552
+
553
+ // ============================================================================
554
+ // TEST: Color → Size cascade (single parent)
555
+ // ============================================================================
556
+
557
+ // Test 1: Select blue → size should show all 3 sizes
558
+ await performAndAssert(
559
+ 'Select color=blue → size shows large, medium, small',
560
+ getState,
561
+ async () => await user.selectOptions(colorFilter, ['blue']),
562
+ (before, after) =>
563
+ after.colorSelected === 'blue' &&
564
+ after.sizeOptions.includes('large') &&
565
+ after.sizeOptions.includes('medium') &&
566
+ after.sizeOptions.includes('small') &&
567
+ after.sizeOptions.length === 3 &&
568
+ after.chartRendered &&
569
+ !after.noDataVisible
570
+ )
571
+
572
+ // Test 2: Select red → size should show only small, medium
573
+ await performAndAssert(
574
+ 'Select color=red → size shows medium, small (no large)',
575
+ getState,
576
+ async () => await user.selectOptions(colorFilter, ['red']),
577
+ (before, after) =>
578
+ after.colorSelected === 'red' &&
579
+ after.sizeOptions.includes('medium') &&
580
+ after.sizeOptions.includes('small') &&
581
+ !after.sizeOptions.includes('large') &&
582
+ after.sizeOptions.length === 2 &&
583
+ after.chartRendered &&
584
+ !after.noDataVisible
585
+ )
586
+
587
+ // Test 3: Select green → size should show only large
588
+ await performAndAssert(
589
+ 'Select color=green → size shows only large',
590
+ getState,
591
+ async () => await user.selectOptions(colorFilter, ['green']),
592
+ (before, after) =>
593
+ after.colorSelected === 'green' &&
594
+ after.sizeOptions.length === 1 &&
595
+ after.sizeOptions[0] === 'large' &&
596
+ after.sizeSelected === 'large' &&
597
+ after.chartRendered &&
598
+ !after.noDataVisible
599
+ )
600
+
601
+ // ============================================================================
602
+ // TEST: Color + Size → Weight cascade (multiple parents)
603
+ // ============================================================================
604
+
605
+ // Test 4: Select blue + small → weight updates based on both parents
606
+ await performAndAssert(
607
+ 'Select color=blue → size options repopulate',
608
+ getState,
609
+ async () => await user.selectOptions(colorFilter, ['blue']),
610
+ (before, after) =>
611
+ after.colorSelected === 'blue' && after.sizeSelected === 'large' && after.sizeOptions.length === 3
612
+ )
613
+
614
+ await performAndAssert(
615
+ 'Select size=small → weight updates based on color+size',
616
+ getState,
617
+ async () => await user.selectOptions(sizeFilter, ['small']),
618
+ (before, after) =>
619
+ after.colorSelected === 'blue' &&
620
+ after.sizeSelected === 'small' &&
621
+ after.weightOptions.length === 3 &&
622
+ after.chartRendered &&
623
+ !after.noDataVisible
624
+ )
625
+
626
+ // ============================================================================
627
+ // TEST: Weight → Smell cascade
628
+ // ============================================================================
629
+
630
+ // Test 5: Select light → smell shows only neutral and sweet (2 options)
631
+ await performAndAssert(
632
+ 'Select weight=light → smell shows neutral, sweet (not bitter)',
633
+ getState,
634
+ async () => await user.selectOptions(weightFilter, ['light']),
635
+ (before, after) =>
636
+ after.colorSelected === 'blue' &&
637
+ after.sizeSelected === 'small' &&
638
+ after.weightSelected === 'light' &&
639
+ after.smellOptions.includes('neutral') &&
640
+ after.smellOptions.includes('sweet') &&
641
+ !after.smellOptions.includes('bitter') &&
642
+ after.smellOptions.length === 2 &&
643
+ after.chartRendered &&
644
+ !after.noDataVisible
645
+ )
646
+
647
+ // ============================================================================
648
+ // TEST: Full cascade verification - change parent and verify all children
649
+ // ============================================================================
650
+
651
+ // Test 6: Final - select red again and verify entire cascade updates
652
+ await performAndAssert(
653
+ 'Select color=red → all filters update; size shows medium, small',
654
+ getState,
655
+ async () => await user.selectOptions(colorFilter, ['red']),
656
+ (before, after) =>
657
+ after.colorSelected === 'red' &&
658
+ after.sizeOptions.length === 2 &&
659
+ after.sizeOptions.includes('medium') &&
660
+ after.sizeOptions.includes('small') &&
661
+ // Size, weight, and smell selected values should be valid in their updated options
662
+ after.sizeOptions.includes(after.sizeSelected) &&
663
+ after.weightOptions.includes(after.weightSelected) &&
664
+ after.smellOptions.includes(after.smellSelected) &&
665
+ after.chartRendered &&
666
+ !after.noDataVisible
667
+ )
668
+ }
669
+ }
670
+
671
+ export const Parent_Child_Filters_With_DefaultValue: Story = {
672
+ args: {
673
+ config: ParentChildFilters as unknown as Config,
674
+ isEditor: false
675
+ },
676
+ parameters: {
677
+ docs: {
678
+ description: {
679
+ story:
680
+ 'Demonstrates parent-child filter relationships (State → County → City) with defaultValue support. Shows how child filter options update based on parent selection, and how defaultValue is applied on initial load.'
681
+ }
682
+ }
683
+ },
684
+ play: async ({ canvasElement }) => {
685
+ const canvas = within(canvasElement)
686
+ const user = userEvent.setup()
687
+
688
+ // Get filter dropdowns
689
+ const stateFilter = (await canvas.findByLabelText('State', { selector: 'select' })) as HTMLSelectElement
690
+ const countyFilter = (await canvas.findByLabelText('County', { selector: 'select' })) as HTMLSelectElement
691
+ const cityFilter = (await canvas.findByLabelText('City', { selector: 'select' })) as HTMLSelectElement
692
+
693
+ const getFilterOptions = (select: HTMLSelectElement) =>
694
+ Array.from(select.options)
695
+ .map(o => o.value)
696
+ .filter(Boolean)
697
+
698
+ const getState = () => ({
699
+ stateOptions: getFilterOptions(stateFilter),
700
+ stateSelected: stateFilter.value,
701
+ countyOptions: getFilterOptions(countyFilter),
702
+ countySelected: countyFilter.value,
703
+ cityOptions: getFilterOptions(cityFilter),
704
+ citySelected: cityFilter.value,
705
+ chartRendered: !!canvasElement.querySelector('svg')
706
+ })
707
+
708
+ // Wait for initial load
709
+ await waitForOptionsToPopulate(stateFilter, 3)
710
+ const initialState = getState()
711
+
712
+ // Verify defaultValue is applied on initial load
713
+ expect(initialState.stateSelected).toBe('California')
714
+ expect(initialState.countySelected).toBe('Los Angeles')
715
+ expect(initialState.citySelected).toBe('Los Angeles')
716
+ expect(initialState.chartRendered).toBe(true)
717
+
718
+ // Test 1: Change state to Texas → county and city should update
719
+ await performAndAssert(
720
+ 'Select state=Texas → county shows Texas counties',
721
+ getState,
722
+ async () => await user.selectOptions(stateFilter, ['Texas']),
723
+ (before, after) =>
724
+ after.stateSelected === 'Texas' &&
725
+ after.countyOptions.includes('Harris') &&
726
+ after.countyOptions.includes('Dallas') &&
727
+ after.countyOptions.includes('Bexar') &&
728
+ after.countyOptions.includes('Travis') &&
729
+ !after.countyOptions.includes('Los Angeles') &&
730
+ after.chartRendered
731
+ )
732
+
733
+ // Test 2: Select county → city options update
734
+ await performAndAssert(
735
+ 'Select county=Harris → city shows Houston, Pasadena',
736
+ getState,
737
+ async () => await user.selectOptions(countyFilter, ['Harris']),
738
+ (before, after) =>
739
+ after.countySelected === 'Harris' &&
740
+ after.cityOptions.includes('Houston') &&
741
+ after.cityOptions.includes('Pasadena') &&
742
+ after.cityOptions.length === 2 &&
743
+ after.chartRendered
744
+ )
745
+
746
+ // Test 3: Change state back to California → verify cascade updates
747
+ await performAndAssert(
748
+ 'Select state=California → filters reset to California data',
749
+ getState,
750
+ async () => await user.selectOptions(stateFilter, ['California']),
751
+ (before, after) =>
752
+ after.stateSelected === 'California' &&
753
+ after.countyOptions.includes('Los Angeles') &&
754
+ after.countyOptions.includes('San Diego') &&
755
+ after.countyOptions.includes('Orange') &&
756
+ after.chartRendered
757
+ )
758
+
759
+ // Test 4: Select Orange county → verify city options
760
+ await performAndAssert(
761
+ 'Select county=Orange → city shows Anaheim, Santa Ana, Irvine',
762
+ getState,
763
+ async () => await user.selectOptions(countyFilter, ['Orange']),
764
+ (before, after) =>
765
+ after.countySelected === 'Orange' &&
766
+ after.cityOptions.includes('Anaheim') &&
767
+ after.cityOptions.includes('Santa Ana') &&
768
+ after.cityOptions.includes('Irvine') &&
769
+ after.cityOptions.length === 3 &&
770
+ !after.cityOptions.includes('Los Angeles') &&
771
+ after.chartRendered
772
+ )
773
+ }
774
+ }
775
+
776
+ export const Nested_Dropdown_With_Parent_Child: Story = {
777
+ args: {
778
+ config: NestedParentChildFilters as unknown as Config,
779
+ isEditor: false
780
+ },
781
+ parameters: {
782
+ docs: {
783
+ description: {
784
+ story:
785
+ 'Demonstrates nested dropdown filters (Year/Quarter subGrouping) with parent-child relationships. The Year/Quarter filter depends on Region selection, and both Year and Quarter defaultValue properties are respected on initial load.'
786
+ }
787
+ }
788
+ },
789
+ play: async ({ canvasElement }) => {
790
+ const canvas = within(canvasElement)
791
+ const user = userEvent.setup()
792
+
793
+ // Get filter dropdowns
794
+ const regionFilter = (await canvas.findByLabelText('Region', { selector: 'select' })) as HTMLSelectElement
795
+ const yearQuarterFilter = (await canvas.findByLabelText('Year and Quarter', {
796
+ selector: 'select'
797
+ })) as HTMLSelectElement
798
+
799
+ const getFilterOptions = (select: HTMLSelectElement) =>
800
+ Array.from(select.options)
801
+ .map(o => o.text)
802
+ .filter(Boolean)
803
+
804
+ const getState = () => ({
805
+ regionOptions: getFilterOptions(regionFilter),
806
+ regionSelected: regionFilter.value,
807
+ yearQuarterOptions: getFilterOptions(yearQuarterFilter),
808
+ yearQuarterSelected: yearQuarterFilter.value,
809
+ chartRendered: !!canvasElement.querySelector('svg')
810
+ })
811
+
812
+ // Wait for initial load
813
+ await waitForOptionsToPopulate(regionFilter, 4)
814
+ const initialState = getState()
815
+
816
+ // Verify defaultValue is applied on initial load (North region, 2023 Q2)
817
+ expect(initialState.regionSelected).toBe('North')
818
+ expect(initialState.yearQuarterSelected).toContain('2023')
819
+ expect(initialState.yearQuarterSelected).toContain('Q2')
820
+ expect(initialState.chartRendered).toBe(true)
821
+
822
+ // Test 1: Change region to South → year options should update based on available data
823
+ await performAndAssert(
824
+ 'Select region=South → year/quarter filter updates',
825
+ getState,
826
+ async () => await user.selectOptions(regionFilter, ['South']),
827
+ (before, after) =>
828
+ after.regionSelected === 'South' &&
829
+ after.yearQuarterOptions.some(opt => opt.includes('2022')) &&
830
+ after.yearQuarterOptions.some(opt => opt.includes('2023')) &&
831
+ after.yearQuarterOptions.some(opt => opt.includes('2024')) &&
832
+ after.chartRendered
833
+ )
834
+
835
+ // Test 2: Change region to East → should only show 2023 and 2024 (no 2022 data for East)
836
+ await performAndAssert(
837
+ 'Select region=East → year shows only 2023, 2024',
838
+ getState,
839
+ async () => await user.selectOptions(regionFilter, ['East']),
840
+ (before, after) =>
841
+ after.regionSelected === 'East' &&
842
+ after.yearQuarterOptions.some(opt => opt.includes('2023')) &&
843
+ after.yearQuarterOptions.some(opt => opt.includes('2024')) &&
844
+ !after.yearQuarterOptions.some(opt => opt.includes('2022')) &&
845
+ after.chartRendered
846
+ )
847
+
848
+ // Test 3: Change region to West → should only show 2023 Q3-Q4 and 2024
849
+ await performAndAssert(
850
+ 'Select region=West → limited year/quarter options',
851
+ getState,
852
+ async () => await user.selectOptions(regionFilter, ['West']),
853
+ (before, after) =>
854
+ after.regionSelected === 'West' &&
855
+ (after.yearQuarterOptions.some(opt => opt.includes('2023')) ||
856
+ after.yearQuarterOptions.some(opt => opt.includes('2024'))) &&
857
+ !after.yearQuarterOptions.some(opt => opt.includes('2022')) &&
858
+ after.chartRendered
859
+ )
860
+
861
+ // Test 4: Change back to North → verify all years available again
862
+ await performAndAssert(
863
+ 'Select region=North → all years (2022-2024) available',
864
+ getState,
865
+ async () => await user.selectOptions(regionFilter, ['North']),
866
+ (before, after) =>
867
+ after.regionSelected === 'North' &&
868
+ after.yearQuarterOptions.some(opt => opt.includes('2022')) &&
869
+ after.yearQuarterOptions.some(opt => opt.includes('2023')) &&
870
+ after.yearQuarterOptions.some(opt => opt.includes('2024')) &&
871
+ after.chartRendered
872
+ )
873
+ }
874
+ }
875
+
475
876
  export default meta
@@ -0,0 +1,116 @@
1
+ {
2
+ "dashboard": {
3
+ "theme": "theme-blue",
4
+ "title": "Custom Order Filter - New Values Auto-Append Demo",
5
+ "sharedFilters": [
6
+ {
7
+ "key": "Location",
8
+ "columnName": "Location",
9
+ "showDropdown": true,
10
+ "type": "datafilter",
11
+ "order": "cust",
12
+ "orderedValues": [
13
+ "California",
14
+ "Texas",
15
+ "Florida"
16
+ ],
17
+ "usedBy": [
18
+ "data-bite-demo",
19
+ "chart-demo"
20
+ ],
21
+ "tier": 1
22
+ }
23
+ ]
24
+ },
25
+ "datasets": {
26
+ "custom-order-demo-data.csv": {
27
+ "dataUrl": "http://localhost:6006/custom-order-demo-data.csv"
28
+ }
29
+ },
30
+ "rows": [
31
+ {
32
+ "columns": [
33
+ {
34
+ "width": 12,
35
+ "widget": "legacySharedFilters"
36
+ }
37
+ ]
38
+ },
39
+ {
40
+ "columns": [
41
+ {
42
+ "width": 6,
43
+ "widget": "data-bite-demo"
44
+ },
45
+ {
46
+ "width": 6,
47
+ "widget": "chart-demo"
48
+ }
49
+ ]
50
+ }
51
+ ],
52
+ "visualizations": {
53
+ "data-bite-demo": {
54
+ "type": "data-bite",
55
+ "dataKey": "custom-order-demo-data.csv",
56
+ "dataFunction": "Sum",
57
+ "dataColumn": "Amount",
58
+ "bitePosition": "Left",
59
+ "biteFontSize": 36,
60
+ "fontSize": "large",
61
+ "biteBody": "<h3>Total Amount for Selected Location</h3><p><strong>Custom Order Demo:</strong> The filter has custom order: <em>California, Texas, Florida</em>. But the CSV data includes <strong>New York</strong> and <strong>Illinois</strong> which should auto-append to the end of the dropdown.</p><p>Select different locations from the dropdown above to see the total amount update.</p>",
62
+ "dataFormat": {
63
+ "roundToPlace": 0,
64
+ "commas": true,
65
+ "prefix": "$",
66
+ "suffix": ""
67
+ },
68
+ "biteStyle": "graphic",
69
+ "theme": "theme-blue",
70
+ "shadow": true,
71
+ "uid": "data-bite-demo",
72
+ "visualizationType": "data-bite"
73
+ },
74
+ "chart-demo": {
75
+ "type": "chart",
76
+ "title": "Amount by Year",
77
+ "theme": "theme-blue",
78
+ "fontSize": "medium",
79
+ "height": "250",
80
+ "yAxis": {
81
+ "label": "Amount",
82
+ "gridLines": true
83
+ },
84
+ "xAxis": {
85
+ "dataKey": "Year",
86
+ "label": "Year"
87
+ },
88
+ "table": {
89
+ "show": true,
90
+ "label": "Data Table"
91
+ },
92
+ "legend": {
93
+ "hide": true
94
+ },
95
+ "visualizationType": "Bar",
96
+ "series": [
97
+ {
98
+ "dataKey": "Amount",
99
+ "type": "Bar"
100
+ }
101
+ ],
102
+ "dataKey": "custom-order-demo-data.csv",
103
+ "uid": "chart-demo"
104
+ },
105
+ "legacySharedFilters": {
106
+ "type": "dashboardFilters",
107
+ "visualizationType": "dashboardFilters",
108
+ "sharedFilterIndexes": [
109
+ 0
110
+ ],
111
+ "filterBehavior": "Filter Change",
112
+ "uid": "legacySharedFilters"
113
+ }
114
+ },
115
+ "type": "dashboard"
116
+ }