@genspectrum/dashboard-components 0.6.10 → 0.6.12

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 (39) hide show
  1. package/dist/dashboard-components.js +1006 -618
  2. package/dist/dashboard-components.js.map +1 -1
  3. package/dist/genspectrum-components.d.ts +7 -7
  4. package/dist/style.css +147 -1
  5. package/package.json +2 -2
  6. package/src/preact/aggregatedData/aggregate.tsx +12 -4
  7. package/src/preact/components/checkbox-selector.stories.tsx +93 -11
  8. package/src/preact/components/checkbox-selector.tsx +19 -0
  9. package/src/preact/components/color-scale-selector-dropdown.tsx +5 -3
  10. package/src/preact/components/dropdown.tsx +3 -3
  11. package/src/preact/components/info.tsx +88 -1
  12. package/src/preact/components/mutation-type-selector.stories.tsx +115 -0
  13. package/src/preact/components/mutation-type-selector.tsx +33 -8
  14. package/src/preact/components/percent-input.stories.tsx +93 -0
  15. package/src/preact/components/percent-intput.tsx +4 -0
  16. package/src/preact/components/proportion-selector-dropdown.stories.tsx +2 -2
  17. package/src/preact/components/proportion-selector-dropdown.tsx +9 -7
  18. package/src/preact/components/proportion-selector.stories.tsx +4 -4
  19. package/src/preact/components/proportion-selector.tsx +46 -12
  20. package/src/preact/components/segment-selector.stories.tsx +151 -0
  21. package/src/preact/components/{SegmentSelector.tsx → segment-selector.tsx} +29 -20
  22. package/src/preact/mutationComparison/mutation-comparison.stories.tsx +1 -1
  23. package/src/preact/mutationComparison/mutation-comparison.tsx +1 -1
  24. package/src/preact/mutationComparison/queryMutationData.ts +1 -1
  25. package/src/preact/mutations/mutations-grid.tsx +5 -1
  26. package/src/preact/mutations/mutations.tsx +33 -3
  27. package/src/preact/mutations/queryMutations.ts +1 -1
  28. package/src/preact/mutationsOverTime/getFilteredMutationsOverTime.spec.ts +4 -4
  29. package/src/preact/mutationsOverTime/getFilteredMutationsOverTimeData.ts +3 -2
  30. package/src/preact/mutationsOverTime/mutations-over-time.tsx +1 -1
  31. package/src/preact/numberSequencesOverTime/number-sequences-over-time.tsx +43 -8
  32. package/src/preact/prevalenceOverTime/prevalence-over-time.stories.tsx +2 -1
  33. package/src/preact/prevalenceOverTime/prevalence-over-time.tsx +44 -53
  34. package/src/preact/useQuery.ts +1 -1
  35. package/src/query/queryMutationsOverTime.ts +3 -3
  36. package/src/utils/map2d.spec.ts +83 -22
  37. package/src/utils/map2d.ts +158 -0
  38. package/src/web-components/visualization/gs-prevalence-over-time.tsx +2 -4
  39. package/src/utils/Map2d.ts +0 -75
@@ -990,14 +990,14 @@ declare global {
990
990
 
991
991
  declare global {
992
992
  interface HTMLElementTagNameMap {
993
- 'gs-aggregate-component': AggregateComponent;
993
+ 'gs-number-sequences-over-time': NumberSequencesOverTimeComponent;
994
994
  }
995
995
  }
996
996
 
997
997
 
998
998
  declare global {
999
999
  interface HTMLElementTagNameMap {
1000
- 'gs-number-sequences-over-time': NumberSequencesOverTimeComponent;
1000
+ 'gs-aggregate-component': AggregateComponent;
1001
1001
  }
1002
1002
  }
1003
1003
 
@@ -1041,21 +1041,21 @@ declare global {
1041
1041
 
1042
1042
  declare global {
1043
1043
  interface HTMLElementTagNameMap {
1044
- 'gs-mutation-filter': MutationFilterComponent;
1044
+ 'gs-lineage-filter': LineageFilterComponent;
1045
1045
  }
1046
1046
  interface HTMLElementEventMap {
1047
- 'gs-mutation-filter-changed': CustomEvent<SelectedMutationFilterStrings>;
1048
- 'gs-mutation-filter-on-blur': CustomEvent<SelectedMutationFilterStrings>;
1047
+ 'gs-lineage-filter-changed': CustomEvent<Record<string, string>>;
1049
1048
  }
1050
1049
  }
1051
1050
 
1052
1051
 
1053
1052
  declare global {
1054
1053
  interface HTMLElementTagNameMap {
1055
- 'gs-lineage-filter': LineageFilterComponent;
1054
+ 'gs-mutation-filter': MutationFilterComponent;
1056
1055
  }
1057
1056
  interface HTMLElementEventMap {
1058
- 'gs-lineage-filter-changed': CustomEvent<Record<string, string>>;
1057
+ 'gs-mutation-filter-changed': CustomEvent<SelectedMutationFilterStrings>;
1058
+ 'gs-mutation-filter-on-blur': CustomEvent<SelectedMutationFilterStrings>;
1059
1059
  }
1060
1060
  }
1061
1061
 
package/dist/style.css CHANGED
@@ -376,7 +376,7 @@ input[type="range"] {
376
376
  background-color: #C6C6C6;
377
377
  pointer-events: none;
378
378
  }/*
379
- ! tailwindcss v3.4.6 | MIT License | https://tailwindcss.com
379
+ ! tailwindcss v3.4.7 | MIT License | https://tailwindcss.com
380
380
  *//*
381
381
  1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
382
382
  2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
@@ -983,6 +983,39 @@ html {
983
983
  --tw-contain-paint: ;
984
984
  --tw-contain-style: ;
985
985
  }
986
+ .container {
987
+ width: 100%;
988
+ }
989
+ @media (min-width: 640px) {
990
+
991
+ .container {
992
+ max-width: 640px;
993
+ }
994
+ }
995
+ @media (min-width: 768px) {
996
+
997
+ .container {
998
+ max-width: 768px;
999
+ }
1000
+ }
1001
+ @media (min-width: 1024px) {
1002
+
1003
+ .container {
1004
+ max-width: 1024px;
1005
+ }
1006
+ }
1007
+ @media (min-width: 1280px) {
1008
+
1009
+ .container {
1010
+ max-width: 1280px;
1011
+ }
1012
+ }
1013
+ @media (min-width: 1536px) {
1014
+
1015
+ .container {
1016
+ max-width: 1536px;
1017
+ }
1018
+ }
986
1019
  .alert {
987
1020
  display: grid;
988
1021
  width: 100%;
@@ -1024,6 +1057,15 @@ html {
1024
1057
  color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));
1025
1058
  }
1026
1059
 
1060
+ .menu li > *:not(ul, .menu-title, details, .btn):active,
1061
+ .menu li > *:not(ul, .menu-title, details, .btn).active,
1062
+ .menu li > details > summary:active {
1063
+ --tw-bg-opacity: 1;
1064
+ background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));
1065
+ --tw-text-opacity: 1;
1066
+ color: var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity)));
1067
+ }
1068
+
1027
1069
  .tab:hover {
1028
1070
  --tw-text-opacity: 1;
1029
1071
  }
@@ -1117,6 +1159,25 @@ html {
1117
1159
  container-type: inline-size;
1118
1160
  grid-template-columns: auto 1fr;
1119
1161
  }
1162
+ .divider {
1163
+ display: flex;
1164
+ flex-direction: row;
1165
+ align-items: center;
1166
+ align-self: stretch;
1167
+ margin-top: 1rem;
1168
+ margin-bottom: 1rem;
1169
+ height: 1rem;
1170
+ white-space: nowrap;
1171
+ }
1172
+ .divider:before,
1173
+ .divider:after {
1174
+ height: 0.125rem;
1175
+ width: 100%;
1176
+ flex-grow: 1;
1177
+ --tw-content: '';
1178
+ content: var(--tw-content);
1179
+ background-color: var(--fallback-bc,oklch(var(--bc)/0.1));
1180
+ }
1120
1181
  .dropdown {
1121
1182
  position: relative;
1122
1183
  display: inline-block;
@@ -1362,6 +1423,10 @@ html {
1362
1423
  border-radius: inherit;
1363
1424
  }
1364
1425
  }
1426
+ .link {
1427
+ cursor: pointer;
1428
+ text-decoration-line: underline;
1429
+ }
1365
1430
  .menu li.disabled {
1366
1431
  cursor: not-allowed;
1367
1432
  -webkit-user-select: none;
@@ -1629,6 +1694,11 @@ input.tab:checked + .tab-content,
1629
1694
  --alert-bg: var(--fallback-er,oklch(var(--er)/1));
1630
1695
  --alert-bg-mix: var(--fallback-b1,oklch(var(--b1)/1));
1631
1696
  }
1697
+ .btm-nav > *:where(.active) {
1698
+ border-top-width: 2px;
1699
+ --tw-bg-opacity: 1;
1700
+ background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));
1701
+ }
1632
1702
  .btm-nav > *.disabled,
1633
1703
  .btm-nav > *[disabled] {
1634
1704
  pointer-events: none;
@@ -1781,6 +1851,9 @@ input.tab:checked + .tab-content,
1781
1851
  background-position-y: 0;
1782
1852
  }
1783
1853
  }
1854
+ .divider:not(:empty) {
1855
+ gap: 1rem;
1856
+ }
1784
1857
  .dropdown.dropdown-open .dropdown-content,
1785
1858
  .dropdown:focus .dropdown-content,
1786
1859
  .dropdown:focus-within .dropdown-content {
@@ -1858,6 +1931,14 @@ input.tab:checked + .tab-content,
1858
1931
  .join > :where(*:not(:first-child)):is(.btn) {
1859
1932
  margin-inline-start: calc(var(--border-btn) * -1);
1860
1933
  }
1934
+ .link:focus {
1935
+ outline: 2px solid transparent;
1936
+ outline-offset: 2px;
1937
+ }
1938
+ .link:focus-visible {
1939
+ outline: 2px solid currentColor;
1940
+ outline-offset: 2px;
1941
+ }
1861
1942
  .loading {
1862
1943
  pointer-events: none;
1863
1944
  display: inline-block;
@@ -1888,6 +1969,14 @@ input.tab:checked + .tab-content,
1888
1969
  outline: 2px solid transparent;
1889
1970
  outline-offset: 2px;
1890
1971
  }
1972
+ .menu li > *:not(ul, .menu-title, details, .btn):active,
1973
+ .menu li > *:not(ul, .menu-title, details, .btn).active,
1974
+ .menu li > details > summary:active {
1975
+ --tw-bg-opacity: 1;
1976
+ background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));
1977
+ --tw-text-opacity: 1;
1978
+ color: var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity)));
1979
+ }
1891
1980
  .mockup-phone .display {
1892
1981
  overflow: hidden;
1893
1982
  border-radius: 40px;
@@ -2384,6 +2473,12 @@ input.tab:checked + .tab-content,
2384
2473
  --tw-bg-opacity: 1;
2385
2474
  background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));
2386
2475
  }
2476
+ .table-zebra tr.active,
2477
+ .table-zebra tr.active:nth-child(even),
2478
+ .table-zebra-zebra tbody tr:nth-child(even) {
2479
+ --tw-bg-opacity: 1;
2480
+ background-color: var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity)));
2481
+ }
2387
2482
  .table :where(thead tr, tbody tr:not(:last-child), tbody tr:first-child:last-child) {
2388
2483
  border-bottom-width: 1px;
2389
2484
  --tw-border-opacity: 1;
@@ -2456,6 +2551,18 @@ input.tab:checked + .tab-content,
2456
2551
  --togglehandleborder: 0 0 0 3px var(--fallback-bc,oklch(var(--bc)/1)) inset,
2457
2552
  var(--handleoffsetcalculator) 0 0 3px var(--fallback-bc,oklch(var(--bc)/1)) inset;
2458
2553
  }
2554
+ .btm-nav-xs > *:where(.active) {
2555
+ border-top-width: 1px;
2556
+ }
2557
+ .btm-nav-sm > *:where(.active) {
2558
+ border-top-width: 2px;
2559
+ }
2560
+ .btm-nav-md > *:where(.active) {
2561
+ border-top-width: 2px;
2562
+ }
2563
+ .btm-nav-lg > *:where(.active) {
2564
+ border-top-width: 4px;
2565
+ }
2459
2566
  .btn-xs {
2460
2567
  height: 1.5rem;
2461
2568
  min-height: 1.5rem;
@@ -2875,6 +2982,9 @@ input.tab:checked + .tab-content,
2875
2982
  margin-top: 1rem;
2876
2983
  margin-bottom: 1rem;
2877
2984
  }
2985
+ .mb-0 {
2986
+ margin-bottom: 0px;
2987
+ }
2878
2988
  .mb-1 {
2879
2989
  margin-bottom: 0.25rem;
2880
2990
  }
@@ -2884,6 +2994,9 @@ input.tab:checked + .tab-content,
2884
2994
  .ml-1 {
2885
2995
  margin-left: 0.25rem;
2886
2996
  }
2997
+ .ml-2 {
2998
+ margin-left: 0.5rem;
2999
+ }
2887
3000
  .ml-2\.5 {
2888
3001
  margin-left: 0.625rem;
2889
3002
  }
@@ -2896,6 +3009,9 @@ input.tab:checked + .tab-content,
2896
3009
  .mr-2 {
2897
3010
  margin-right: 0.5rem;
2898
3011
  }
3012
+ .mt-0 {
3013
+ margin-top: 0px;
3014
+ }
2899
3015
  .mt-0\.5 {
2900
3016
  margin-top: 0.125rem;
2901
3017
  }
@@ -2935,12 +3051,24 @@ input.tab:checked + .tab-content,
2935
3051
  .w-16 {
2936
3052
  width: 4rem;
2937
3053
  }
3054
+ .w-20 {
3055
+ width: 5rem;
3056
+ }
3057
+ .w-24 {
3058
+ width: 6rem;
3059
+ }
2938
3060
  .w-32 {
2939
3061
  width: 8rem;
2940
3062
  }
3063
+ .w-44 {
3064
+ width: 11rem;
3065
+ }
2941
3066
  .w-64 {
2942
3067
  width: 16rem;
2943
3068
  }
3069
+ .w-\[6rem\] {
3070
+ width: 6rem;
3071
+ }
2944
3072
  .w-\[7\.5rem\] {
2945
3073
  width: 7.5rem;
2946
3074
  }
@@ -2977,6 +3105,12 @@ input.tab:checked + .tab-content,
2977
3105
  .resize {
2978
3106
  resize: both;
2979
3107
  }
3108
+ .list-inside {
3109
+ list-style-position: inside;
3110
+ }
3111
+ .list-disc {
3112
+ list-style-type: disc;
3113
+ }
2980
3114
  .flex-row {
2981
3115
  flex-direction: row;
2982
3116
  }
@@ -3007,6 +3141,9 @@ input.tab:checked + .tab-content,
3007
3141
  .overflow-auto {
3008
3142
  overflow: auto;
3009
3143
  }
3144
+ .overflow-x-auto {
3145
+ overflow-x: auto;
3146
+ }
3010
3147
  .whitespace-nowrap {
3011
3148
  white-space: nowrap;
3012
3149
  }
@@ -3016,6 +3153,9 @@ input.tab:checked + .tab-content,
3016
3153
  .rounded-full {
3017
3154
  border-radius: 9999px;
3018
3155
  }
3156
+ .rounded-lg {
3157
+ border-radius: 0.5rem;
3158
+ }
3019
3159
  .rounded-md {
3020
3160
  border-radius: 0.375rem;
3021
3161
  }
@@ -3271,6 +3411,12 @@ input.tab:checked + .tab-content,
3271
3411
  }
3272
3412
  .peer:hover ~ .peer-hover\:visible {
3273
3413
  visibility: visible;
3414
+ }
3415
+ @media (min-width: 640px) {
3416
+
3417
+ .sm\:max-w-5xl {
3418
+ max-width: 64rem;
3419
+ }
3274
3420
  }.flatpickr-calendar{background:transparent;opacity:0;display:none;text-align:center;visibility:hidden;padding:0;-webkit-animation:none;animation:none;direction:ltr;border:0;font-size:14px;line-height:24px;border-radius:5px;position:absolute;width:307.875px;-webkit-box-sizing:border-box;box-sizing:border-box;-ms-touch-action:manipulation;touch-action:manipulation;background:#fff;-webkit-box-shadow:1px 0 0 #e6e6e6,-1px 0 0 #e6e6e6,0 1px 0 #e6e6e6,0 -1px 0 #e6e6e6,0 3px 13px rgba(0,0,0,0.08);box-shadow:1px 0 0 #e6e6e6,-1px 0 0 #e6e6e6,0 1px 0 #e6e6e6,0 -1px 0 #e6e6e6,0 3px 13px rgba(0,0,0,0.08)}.flatpickr-calendar.open,.flatpickr-calendar.inline{opacity:1;max-height:640px;visibility:visible}.flatpickr-calendar.open{display:inline-block;z-index:99999}.flatpickr-calendar.animate.open{-webkit-animation:fpFadeInDown 300ms cubic-bezier(.23,1,.32,1);animation:fpFadeInDown 300ms cubic-bezier(.23,1,.32,1)}.flatpickr-calendar.inline{display:block;position:relative;top:2px}.flatpickr-calendar.static{position:absolute;top:calc(100% + 2px)}.flatpickr-calendar.static.open{z-index:999;display:block}.flatpickr-calendar.multiMonth .flatpickr-days .dayContainer:nth-child(n+1) .flatpickr-day.inRange:nth-child(7n+7){-webkit-box-shadow:none !important;box-shadow:none !important}.flatpickr-calendar.multiMonth .flatpickr-days .dayContainer:nth-child(n+2) .flatpickr-day.inRange:nth-child(7n+1){-webkit-box-shadow:-2px 0 0 #e6e6e6,5px 0 0 #e6e6e6;box-shadow:-2px 0 0 #e6e6e6,5px 0 0 #e6e6e6}.flatpickr-calendar .hasWeeks .dayContainer,.flatpickr-calendar .hasTime .dayContainer{border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.flatpickr-calendar .hasWeeks .dayContainer{border-left:0}.flatpickr-calendar.hasTime .flatpickr-time{height:40px;border-top:1px solid #e6e6e6}.flatpickr-calendar.noCalendar.hasTime .flatpickr-time{height:auto}.flatpickr-calendar:before,.flatpickr-calendar:after{position:absolute;display:block;pointer-events:none;border:solid transparent;content:'';height:0;width:0;left:22px}.flatpickr-calendar.rightMost:before,.flatpickr-calendar.arrowRight:before,.flatpickr-calendar.rightMost:after,.flatpickr-calendar.arrowRight:after{left:auto;right:22px}.flatpickr-calendar.arrowCenter:before,.flatpickr-calendar.arrowCenter:after{left:50%;right:50%}.flatpickr-calendar:before{border-width:5px;margin:0 -5px}.flatpickr-calendar:after{border-width:4px;margin:0 -4px}.flatpickr-calendar.arrowTop:before,.flatpickr-calendar.arrowTop:after{bottom:100%}.flatpickr-calendar.arrowTop:before{border-bottom-color:#e6e6e6}.flatpickr-calendar.arrowTop:after{border-bottom-color:#fff}.flatpickr-calendar.arrowBottom:before,.flatpickr-calendar.arrowBottom:after{top:100%}.flatpickr-calendar.arrowBottom:before{border-top-color:#e6e6e6}.flatpickr-calendar.arrowBottom:after{border-top-color:#fff}.flatpickr-calendar:focus{outline:0}.flatpickr-wrapper{position:relative;display:inline-block}.flatpickr-months{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.flatpickr-months .flatpickr-month{background:transparent;color:rgba(0,0,0,0.9);fill:rgba(0,0,0,0.9);height:34px;line-height:1;text-align:center;position:relative;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;overflow:hidden;-webkit-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1}.flatpickr-months .flatpickr-prev-month,.flatpickr-months .flatpickr-next-month{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;text-decoration:none;cursor:pointer;position:absolute;top:0;height:34px;padding:10px;z-index:3;color:rgba(0,0,0,0.9);fill:rgba(0,0,0,0.9)}.flatpickr-months .flatpickr-prev-month.flatpickr-disabled,.flatpickr-months .flatpickr-next-month.flatpickr-disabled{display:none}.flatpickr-months .flatpickr-prev-month i,.flatpickr-months .flatpickr-next-month i{position:relative}.flatpickr-months .flatpickr-prev-month.flatpickr-prev-month,.flatpickr-months .flatpickr-next-month.flatpickr-prev-month{/*
3275
3421
  /*rtl:begin:ignore*/left:0/*
3276
3422
  /*rtl:end:ignore*/}/*
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genspectrum/dashboard-components",
3
- "version": "0.6.10",
3
+ "version": "0.6.12",
4
4
  "description": "GenSpectrum web components for building dashboards",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-only",
@@ -90,7 +90,7 @@
90
90
  "@storybook/types": "^8.0.9",
91
91
  "@storybook/web-components": "^8.0.9",
92
92
  "@storybook/web-components-vite": "^8.0.9",
93
- "@types/node": "^20.12.7",
93
+ "@types/node": "^22.0.0",
94
94
  "@types/object-hash": "^3.0.6",
95
95
  "@typescript-eslint/eslint-plugin": "^7.14.1",
96
96
  "@typescript-eslint/parser": "^7.14.1",
@@ -9,7 +9,7 @@ import { CsvDownloadButton } from '../components/csv-download-button';
9
9
  import { ErrorBoundary } from '../components/error-boundary';
10
10
  import { ErrorDisplay } from '../components/error-display';
11
11
  import { Fullscreen } from '../components/fullscreen';
12
- import Info from '../components/info';
12
+ import Info, { InfoHeadline1, InfoParagraph } from '../components/info';
13
13
  import { LoadingDisplay } from '../components/loading-display';
14
14
  import { NoDataDisplay } from '../components/no-data-display';
15
15
  import { ResizeContainer } from '../components/resize-container';
@@ -94,18 +94,26 @@ const AggregatedDataTabs: FunctionComponent<AggregatedDataTabsProps> = ({ data,
94
94
 
95
95
  const tabs = views.map((view) => getTab(view));
96
96
 
97
- return <Tabs tabs={tabs} toolbar={<Toolbar data={data} />} />;
97
+ return <Tabs tabs={tabs} toolbar={<Toolbar data={data} fields={fields} />} />;
98
98
  };
99
99
 
100
100
  type ToolbarProps = {
101
101
  data: AggregateData;
102
+ fields: string[];
102
103
  };
103
104
 
104
- const Toolbar: FunctionComponent<ToolbarProps> = ({ data }) => {
105
+ const Toolbar: FunctionComponent<ToolbarProps> = ({ data, fields }) => {
105
106
  return (
106
107
  <div class='flex flex-row'>
107
108
  <CsvDownloadButton className='mx-1 btn btn-xs' getData={() => data} filename='aggregate.csv' />
108
- <Info>Info for aggregate</Info>
109
+ <Info>
110
+ <InfoHeadline1>Aggregated data</InfoHeadline1>
111
+ <InfoParagraph>
112
+ This table shows the number and proportion of sequences stratified by the following fields:{' '}
113
+ {fields.join(', ')}. The proportion is calculated with respect to the total count within the
114
+ filtered dataset.
115
+ </InfoParagraph>
116
+ </Info>
109
117
  <Fullscreen />
110
118
  </div>
111
119
  );
@@ -1,5 +1,7 @@
1
1
  import { type Meta, type StoryObj } from '@storybook/preact';
2
2
  import { expect, fn, waitFor, within } from '@storybook/test';
3
+ import { type FunctionComponent } from 'preact';
4
+ import { useState } from 'preact/hooks';
3
5
 
4
6
  import { type CheckboxItem, CheckboxSelector, type CheckboxSelectorProps } from './checkbox-selector';
5
7
 
@@ -14,12 +16,33 @@ const meta: Meta<CheckboxSelectorProps> = {
14
16
 
15
17
  export default meta;
16
18
 
19
+ const WrapperWithState: FunctionComponent<CheckboxSelectorProps> = ({
20
+ items: initialItems,
21
+ label,
22
+ setItems: setItemsMock,
23
+ }) => {
24
+ const [items, setItems] = useState<CheckboxItem[]>(initialItems);
25
+
26
+ return (
27
+ <div className='w-32'>
28
+ <CheckboxSelector
29
+ items={items}
30
+ label={label}
31
+ setItems={(items: CheckboxItem[]) => {
32
+ setItemsMock(items);
33
+ setItems(items);
34
+ }}
35
+ />
36
+ </div>
37
+ );
38
+ };
39
+
17
40
  export const CheckboxSelectorStory: StoryObj<CheckboxSelectorProps> = {
18
41
  render: (args) => {
19
42
  let wrapperStateItems = args.items;
20
43
 
21
44
  return (
22
- <CheckboxSelector
45
+ <WrapperWithState
23
46
  items={wrapperStateItems}
24
47
  label={args.label}
25
48
  setItems={(items: CheckboxItem[]) => {
@@ -37,20 +60,79 @@ export const CheckboxSelectorStory: StoryObj<CheckboxSelectorProps> = {
37
60
  label: 'Some label',
38
61
  setItems: fn(),
39
62
  },
40
- play: async ({ canvasElement, args }) => {
63
+ play: async ({ canvasElement, args, step }) => {
41
64
  const canvas = within(canvasElement);
42
65
 
43
- const open = () => canvas.getByText('Some label', { exact: false });
66
+ const open = () => canvas.getByText('Some label');
67
+ const selectAll = () => canvas.getByText('Select all');
68
+ const selectNone = () => canvas.getByText('Select none');
69
+ const firstItem = () => canvas.getByLabelText('item1');
44
70
  open().click();
45
71
 
46
- const item1 = canvas.getByLabelText('item1', { exact: false });
47
- item1.click();
72
+ await step('Select one item', async () => {
73
+ firstItem().click();
48
74
 
49
- await waitFor(() =>
50
- expect(args.setItems).toHaveBeenCalledWith([
51
- { checked: true, label: 'item1' },
52
- { checked: false, label: 'item2' },
53
- ]),
54
- );
75
+ await waitFor(() =>
76
+ expect(args.setItems).toHaveBeenCalledWith([
77
+ { checked: true, label: 'item1' },
78
+ { checked: false, label: 'item2' },
79
+ ]),
80
+ );
81
+ });
82
+
83
+ await step('Select all items with one item already selected', async () => {
84
+ selectAll().click();
85
+
86
+ await waitFor(() =>
87
+ expect(args.setItems).toHaveBeenCalledWith([
88
+ { checked: true, label: 'item1' },
89
+ { checked: true, label: 'item2' },
90
+ ]),
91
+ );
92
+ });
93
+
94
+ await step('Deselect one item', async () => {
95
+ firstItem().click();
96
+
97
+ await waitFor(() =>
98
+ expect(args.setItems).toHaveBeenCalledWith([
99
+ { checked: false, label: 'item1' },
100
+ { checked: true, label: 'item2' },
101
+ ]),
102
+ );
103
+ });
104
+
105
+ await step('Select none with one item already selected', async () => {
106
+ selectNone().click();
107
+
108
+ await waitFor(() =>
109
+ expect(args.setItems).toHaveBeenCalledWith([
110
+ { checked: false, label: 'item1' },
111
+ { checked: false, label: 'item2' },
112
+ ]),
113
+ );
114
+ });
115
+
116
+ await step('Select all items with none selected', async () => {
117
+ selectAll().click();
118
+
119
+ await waitFor(() =>
120
+ expect(args.setItems).toHaveBeenCalledWith([
121
+ { checked: true, label: 'item1' },
122
+ { checked: true, label: 'item2' },
123
+ ]),
124
+ );
125
+ });
126
+
127
+ await step('Select none with all items selected', async () => {
128
+ selectNone().click();
129
+
130
+ await waitFor(() =>
131
+ expect(args.setItems).toHaveBeenCalledWith([
132
+ { checked: false, label: 'item1' },
133
+ { checked: false, label: 'item2' },
134
+ ]),
135
+ );
136
+ });
55
137
  },
56
138
  };
@@ -18,6 +18,25 @@ export const CheckboxSelector = <Item extends CheckboxItem>({
18
18
  }: CheckboxSelectorProps<Item>) => {
19
19
  return (
20
20
  <Dropdown buttonTitle={label} placement={'bottom-start'}>
21
+ <button
22
+ className='btn btn-xs btn-ghost'
23
+ onClick={() => {
24
+ const newItems = items.map((item) => ({ ...item, checked: true }));
25
+ setItems(newItems);
26
+ }}
27
+ >
28
+ Select all
29
+ </button>
30
+ <button
31
+ className='btn btn-xs btn-ghost'
32
+ onClick={() => {
33
+ const newItems = items.map((item) => ({ ...item, checked: false }));
34
+ setItems(newItems);
35
+ }}
36
+ >
37
+ Select none
38
+ </button>
39
+ <div className='divider mt-0 mb-0' />
21
40
  <ul>
22
41
  {items.map((item, index) => (
23
42
  <li className='flex flex-row items-center' key={item.label}>
@@ -10,8 +10,10 @@ export const ColorScaleSelectorDropdown: FunctionComponent<ColorScaleSelectorDro
10
10
  setColorScale,
11
11
  }) => {
12
12
  return (
13
- <Dropdown buttonTitle={`Color scale`} placement={'bottom-start'}>
14
- <ColorScaleSelector colorScale={colorScale} setColorScale={setColorScale} />
15
- </Dropdown>
13
+ <div className='w-20'>
14
+ <Dropdown buttonTitle={`Color scale`} placement={'bottom-start'}>
15
+ <ColorScaleSelector colorScale={colorScale} setColorScale={setColorScale} />
16
+ </Dropdown>
17
+ </div>
16
18
  );
17
19
  };
@@ -28,13 +28,13 @@ export const Dropdown: FunctionComponent<DropdownProps> = ({ children, buttonTit
28
28
  };
29
29
 
30
30
  return (
31
- <div>
32
- <button type='button' className='btn btn-xs whitespace-nowrap' onClick={toggle} ref={referenceRef}>
31
+ <>
32
+ <button type='button' className='btn btn-xs whitespace-nowrap w-full' onClick={toggle} ref={referenceRef}>
33
33
  {buttonTitle}
34
34
  </button>
35
35
  <div ref={floatingRef} className={`${dropdownClass} ${showContent ? '' : 'hidden'}`}>
36
36
  {children}
37
37
  </div>
38
- </div>
38
+ </>
39
39
  );
40
40
  };
@@ -16,7 +16,7 @@ const Info: FunctionComponent<InfoProps> = ({ children }) => {
16
16
  ?
17
17
  </button>
18
18
  <dialog ref={dialogRef} className={'modal modal-bottom sm:modal-middle'}>
19
- <div className='modal-box'>
19
+ <div className='modal-box sm:max-w-5xl'>
20
20
  <form method='dialog'>
21
21
  <button className='btn btn-sm btn-circle btn-ghost absolute right-2 top-2'>✕</button>
22
22
  </form>
@@ -55,4 +55,91 @@ export const InfoLink: FunctionComponent<{ href: string }> = ({ children, href }
55
55
  );
56
56
  };
57
57
 
58
+ export type InfoComponentCodeProps = {
59
+ componentName: string;
60
+ params: object;
61
+ lapisUrl: string;
62
+ };
63
+
64
+ export const InfoComponentCode: FunctionComponent<InfoComponentCodeProps> = ({ componentName, params, lapisUrl }) => {
65
+ const componentCode = componentParametersToCode(componentName, params, lapisUrl);
66
+ const codePenData = {
67
+ title: 'GenSpectrum dashboard component',
68
+ html: generateFullExampleCode(componentCode, componentName),
69
+ layout: 'left',
70
+ editors: '100',
71
+ };
72
+ return (
73
+ <>
74
+ <InfoHeadline2>Use this component yourself</InfoHeadline2>
75
+ <InfoParagraph>
76
+ This component was created using the following parameters:
77
+ <div className='p-4 border border-gray-200 rounded-lg overflow-x-auto'>
78
+ <pre>
79
+ <code>{componentCode}</code>
80
+ </pre>
81
+ </div>
82
+ </InfoParagraph>
83
+ <InfoParagraph>
84
+ You can add this component to your own website using the{' '}
85
+ <InfoLink href='https://github.com/GenSpectrum/dashboard-components'>
86
+ GenSpectrum dashboard components library
87
+ </InfoLink>{' '}
88
+ and the code from above.
89
+ </InfoParagraph>
90
+ <InfoParagraph>
91
+ <form action='https://codepen.io/pen/define' method='POST' target='_blank'>
92
+ <input
93
+ type='hidden'
94
+ name='data'
95
+ value={JSON.stringify(codePenData).replace(/"/g, '&quot;').replace(/'/g, '&apos;')}
96
+ />
97
+
98
+ <button className='text-blue-600 hover:text-blue-800' type='submit'>
99
+ Click here to try it out on CodePen.
100
+ </button>
101
+ </form>
102
+ </InfoParagraph>
103
+ </>
104
+ );
105
+ };
106
+
58
107
  export default Info;
108
+
109
+ function componentParametersToCode(componentName: string, params: object, lapisUrl: string) {
110
+ const stringifyIfNeeded = (value: unknown) => {
111
+ return typeof value === 'object' ? JSON.stringify(value) : value;
112
+ };
113
+
114
+ const attributes = indentLines(
115
+ Object.entries(params)
116
+ .map(([key, value]) => `${key}='${stringifyIfNeeded(value)}'`)
117
+ .join('\n'),
118
+ 4,
119
+ );
120
+ return `<gs-app lapis="${lapisUrl}">\n <gs-${componentName}\n${attributes}\n />\n</gs-app>`;
121
+ }
122
+
123
+ function generateFullExampleCode(componentCode: string, componentName: string) {
124
+ const storyBookPath = `/docs/visualization-${componentName}--docs`;
125
+ return `<html>
126
+ <head>
127
+ <script type="module" src="https://unpkg.com/@genspectrum/dashboard-components@latest/dist/dashboard-components.js"></script>
128
+ <link rel="stylesheet" href="https://unpkg.com/@genspectrum/dashboard-components@latest/dist/style.css" />
129
+ </head>
130
+
131
+ <body>
132
+ <!-- Component documentation: https://genspectrum.github.io/dashboard-components/?path=${storyBookPath} -->
133
+ ${indentLines(componentCode, 2)}
134
+ </body>
135
+ </html>
136
+ `;
137
+ }
138
+
139
+ function indentLines(text: string, numberSpaces: number) {
140
+ const spaces = ' '.repeat(numberSpaces);
141
+ return text
142
+ .split('\n')
143
+ .map((line) => spaces + line)
144
+ .join('\n');
145
+ }