@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.
- package/Dynamic_Data.md +66 -0
- package/dist/{cdcdashboard-fce76882.es.js → cdcdashboard-BnB1QM5d.es.js} +6 -13
- package/dist/{cdcdashboard-c55ac1ea.es.js → cdcdashboard-D6CG2-Hb.es.js} +5 -12
- package/dist/{cdcdashboard-31a33da1.es.js → cdcdashboard-MXgURbdZ.es.js} +6 -13
- package/dist/{cdcdashboard-1a1724a1.es.js → cdcdashboard-dgT_1dIT.es.js} +136 -151
- package/dist/cdcdashboard.js +84214 -79641
- package/examples/api-dashboard-data.json +272 -0
- package/examples/api-dashboard-years.json +11 -0
- package/examples/api-geographies-data.json +11 -0
- package/examples/api-test/categories.json +18 -0
- package/examples/api-test/chart-data.json +602 -0
- package/examples/api-test/topics.json +47 -0
- package/examples/api-test/years.json +22 -0
- package/examples/markup-axis-label.json +4167 -0
- package/examples/private/big-dashboard.json +39095 -39077
- package/examples/private/cat-y.json +1235 -0
- package/examples/private/chronic-dash.json +1584 -0
- package/examples/private/clade-2.json +430 -0
- package/examples/private/diabetes.json +546 -196
- package/examples/private/map-issue.json +2260 -0
- package/examples/private/markup-footer/mortality-deaths-footnotes-age.csv +3 -0
- package/examples/private/mpinc-state-reports.json +2260 -0
- package/examples/private/mpox.json +38128 -0
- package/examples/private/nwss/rsv.json +1240 -0
- package/examples/private/reset.json +32920 -0
- package/examples/private/simple-dash.json +490 -0
- package/examples/private/test-dash.json +0 -0
- package/examples/private/test123.json +491 -0
- package/examples/test-api-filter-reset.json +132 -0
- package/examples/test-dashboard-simple.json +503 -0
- package/index.html +25 -26
- package/package.json +11 -11
- package/src/CdcDashboardComponent.tsx +35 -10
- package/src/DashboardContext.tsx +3 -1
- package/src/_stories/Dashboard.DataSetup.stories.tsx +203 -0
- package/src/_stories/Dashboard.stories.tsx +402 -1
- package/src/_stories/_mock/custom-order-new-values.json +116 -0
- package/src/_stories/_mock/filter-cascade.json +3350 -0
- package/src/_stories/_mock/gallery-data-bite-dashboard.json +3500 -0
- package/src/_stories/_mock/nested-parent-child-filters.json +392 -0
- package/src/_stories/_mock/parent-child-filters.json +233 -0
- package/src/components/DashboardFilters/DashboardFilters.tsx +54 -31
- package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +118 -50
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +96 -108
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/NestedDropDownDashboard.tsx +196 -59
- package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +129 -29
- package/src/components/DashboardFilters/_stories/DashboardFilters.stories.tsx +62 -3
- package/src/components/DataDesignerModal.tsx +18 -6
- package/src/components/Header/Header.tsx +53 -21
- package/src/components/Toggle/Toggle.tsx +48 -48
- package/src/components/VisualizationRow.tsx +73 -6
- package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +2 -3
- package/src/components/Widget/Widget.tsx +1 -1
- package/src/data/initial-state.js +1 -0
- package/src/helpers/addValuesToDashboardFilters.ts +24 -6
- package/src/helpers/apiFilterHelpers.ts +26 -2
- package/src/helpers/changeFilterActive.ts +67 -65
- package/src/helpers/filterData.ts +52 -7
- package/src/helpers/filterResetHelpers.ts +102 -0
- package/src/helpers/formatConfigBeforeSave.ts +6 -5
- package/src/helpers/getUpdateConfig.ts +91 -91
- package/src/helpers/getVizConfig.ts +2 -2
- package/src/helpers/loadAPIFilters.ts +109 -99
- package/src/helpers/tests/filterResetHelpers.test.ts +532 -0
- package/src/helpers/tests/updatesChildFilters.test.ts +53 -22
- package/src/helpers/updateChildFilters.ts +50 -27
- package/src/index.tsx +1 -0
- package/src/scss/editor-panel.scss +3 -431
- package/src/scss/main.scss +142 -25
- package/src/store/errorMessage/errorMessage.reducer.ts +1 -1
- package/src/test/CdcDashboard.test.jsx +9 -4
- package/src/types/Dashboard.ts +1 -0
- package/src/types/DashboardFilters.ts +9 -8
- package/src/types/FilterStyles.ts +8 -7
- package/src/types/SharedFilter.ts +13 -0
- package/LICENSE +0 -201
- package/examples/private/DEV-11072.json +0 -7591
- package/examples/private/burden_toolkit_mortality_diabetes_attributable_deaths_data.csv +0 -14041
- package/examples/private/burden_toolkit_mortality_diabetes_attributable_deaths_per_100000_data.csv +0 -14041
- package/examples/private/burden_toolkit_mortality_qaly_data.csv +0 -18721
- package/examples/private/burden_toolkit_mortality_yll_data.csv +0 -18721
- package/examples/private/pedro.json +0 -1
- package/src/helpers/getAutoLoadVisualization.ts +0 -11
- package/src/scss/mixins.scss +0 -47
- package/src/scss/variables.scss +0 -5
- /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
|
+
}
|