@genspectrum/dashboard-components 0.19.2 → 0.19.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 (52) hide show
  1. package/custom-elements.json +160 -10
  2. package/dist/{LineageFilterChangedEvent-ixHQkq8y.js → LineageFilterChangedEvent-b0iuroUL.js} +15 -5
  3. package/dist/LineageFilterChangedEvent-b0iuroUL.js.map +1 -0
  4. package/dist/assets/mutationOverTimeWorker-ChQTFL68.js.map +1 -1
  5. package/dist/components.d.ts +71 -25
  6. package/dist/components.js +9047 -8699
  7. package/dist/components.js.map +1 -1
  8. package/dist/util.d.ts +51 -25
  9. package/dist/util.js +2 -1
  10. package/package.json +1 -1
  11. package/src/componentsEntrypoint.ts +3 -1
  12. package/src/preact/components/error-display.stories.tsx +2 -1
  13. package/src/preact/components/error-display.tsx +2 -3
  14. package/src/preact/components/resize-container.tsx +7 -10
  15. package/src/preact/components/tooltip.tsx +7 -4
  16. package/src/preact/dateRangeFilter/date-range-filter.stories.tsx +5 -4
  17. package/src/preact/dateRangeFilter/date-range-filter.tsx +2 -1
  18. package/src/preact/dateRangeFilter/dateRangeOption.ts +2 -1
  19. package/src/preact/genomeViewer/CDSPlot.tsx +219 -0
  20. package/src/preact/genomeViewer/genome-data-viewer.stories.tsx +113 -0
  21. package/src/preact/genomeViewer/genome-data-viewer.tsx +69 -0
  22. package/src/preact/genomeViewer/loadGff3.spec.ts +61 -0
  23. package/src/preact/genomeViewer/loadGff3.ts +174 -0
  24. package/src/preact/lineageFilter/LineageFilterChangedEvent.ts +3 -1
  25. package/src/preact/lineageFilter/lineage-filter.stories.tsx +3 -2
  26. package/src/preact/locationFilter/LocationChangedEvent.ts +2 -1
  27. package/src/preact/locationFilter/location-filter.stories.tsx +3 -2
  28. package/src/preact/mutationFilter/mutation-filter.stories.tsx +3 -2
  29. package/src/preact/mutationFilter/mutation-filter.tsx +2 -1
  30. package/src/preact/shared/charts/colors.ts +1 -1
  31. package/src/preact/textFilter/TextFilterChangedEvent.ts +3 -1
  32. package/src/preact/textFilter/text-filter.stories.tsx +4 -3
  33. package/src/utilEntrypoint.ts +2 -0
  34. package/src/utils/gsEventNames.ts +9 -0
  35. package/src/web-components/input/gs-date-range-filter.stories.ts +4 -3
  36. package/src/web-components/input/gs-date-range-filter.tsx +3 -2
  37. package/src/web-components/input/gs-lineage-filter.stories.ts +3 -2
  38. package/src/web-components/input/gs-lineage-filter.tsx +2 -1
  39. package/src/web-components/input/gs-location-filter.stories.ts +3 -2
  40. package/src/web-components/input/gs-location-filter.tsx +2 -1
  41. package/src/web-components/input/gs-mutation-filter.stories.ts +3 -2
  42. package/src/web-components/input/gs-mutation-filter.tsx +2 -1
  43. package/src/web-components/input/gs-text-filter.stories.ts +3 -2
  44. package/src/web-components/input/gs-text-filter.tsx +2 -1
  45. package/src/web-components/visualization/gs-genome-data-viewer.spec-d.ts +18 -0
  46. package/src/web-components/visualization/gs-genome-data-viewer.stories.ts +108 -0
  47. package/src/web-components/visualization/gs-genome-data-viewer.tsx +59 -0
  48. package/src/web-components/visualization/index.ts +1 -0
  49. package/standalone-bundle/assets/mutationOverTimeWorker-jChgWnwp.js.map +1 -1
  50. package/standalone-bundle/dashboard-components.js +8275 -8002
  51. package/standalone-bundle/dashboard-components.js.map +1 -1
  52. package/dist/LineageFilterChangedEvent-ixHQkq8y.js.map +0 -1
package/dist/util.d.ts CHANGED
@@ -149,6 +149,16 @@ declare const dateRangeValueSchema: default_2.ZodNullable<default_2.ZodUnion<[de
149
149
  dateTo?: string | undefined;
150
150
  }>]>>;
151
151
 
152
+ export declare const gsEventNames: {
153
+ readonly error: "gs-error";
154
+ readonly dateRangeFilterChanged: "gs-date-range-filter-changed";
155
+ readonly dateRangeOptionChanged: "gs-date-range-option-changed";
156
+ readonly mutationFilterChanged: "gs-mutation-filter-changed";
157
+ readonly lineageFilterChanged: "gs-lineage-filter-changed";
158
+ readonly locationChanged: "gs-location-changed";
159
+ readonly textFilterChanged: "gs-text-filter-changed";
160
+ };
161
+
152
162
  export declare type LapisFilter = default_2.infer<typeof lapisFilterSchema>;
153
163
 
154
164
  declare const lapisFilterSchema: default_2.ZodIntersection<default_2.ZodRecord<default_2.ZodString, default_2.ZodUnion<[default_2.ZodString, default_2.ZodArray<default_2.ZodString, "many">, default_2.ZodNumber, default_2.ZodNull, default_2.ZodBoolean, default_2.ZodUndefined]>>, default_2.ZodObject<{
@@ -879,7 +889,7 @@ export { }
879
889
 
880
890
  declare global {
881
891
  interface HTMLElementEventMap {
882
- 'gs-error': ErrorEvent;
892
+ [gsEventNames.error]: ErrorEvent;
883
893
  }
884
894
  }
885
895
 
@@ -900,6 +910,22 @@ declare global {
900
910
  }
901
911
 
902
912
 
913
+ declare global {
914
+ interface HTMLElementTagNameMap {
915
+ 'gs-genome-data-viewer': GenomeDataViewerComponent;
916
+ }
917
+ }
918
+
919
+
920
+ declare global {
921
+ namespace JSX {
922
+ interface IntrinsicElements {
923
+ 'gs-genome-data-viewer': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
924
+ }
925
+ }
926
+ }
927
+
928
+
903
929
  declare global {
904
930
  interface HTMLElementTagNameMap {
905
931
  'gs-mutation-comparison-component': MutationComparisonComponent;
@@ -934,7 +960,7 @@ declare global {
934
960
 
935
961
  declare global {
936
962
  interface HTMLElementTagNameMap {
937
- 'gs-prevalence-over-time': PrevalenceOverTimeComponent;
963
+ 'gs-wastewater-mutations-over-time': WastewaterMutationsOverTimeComponent;
938
964
  }
939
965
  }
940
966
 
@@ -942,7 +968,7 @@ declare global {
942
968
  declare global {
943
969
  namespace JSX {
944
970
  interface IntrinsicElements {
945
- 'gs-prevalence-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
971
+ 'gs-wastewater-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
946
972
  }
947
973
  }
948
974
  }
@@ -950,7 +976,7 @@ declare global {
950
976
 
951
977
  declare global {
952
978
  interface HTMLElementTagNameMap {
953
- 'gs-relative-growth-advantage': RelativeGrowthAdvantageComponent;
979
+ 'gs-prevalence-over-time': PrevalenceOverTimeComponent;
954
980
  }
955
981
  }
956
982
 
@@ -958,7 +984,7 @@ declare global {
958
984
  declare global {
959
985
  namespace JSX {
960
986
  interface IntrinsicElements {
961
- 'gs-relative-growth-advantage': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
987
+ 'gs-prevalence-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
962
988
  }
963
989
  }
964
990
  }
@@ -966,7 +992,7 @@ declare global {
966
992
 
967
993
  declare global {
968
994
  interface HTMLElementTagNameMap {
969
- 'gs-aggregate': AggregateComponent;
995
+ 'gs-relative-growth-advantage': RelativeGrowthAdvantageComponent;
970
996
  }
971
997
  }
972
998
 
@@ -974,7 +1000,7 @@ declare global {
974
1000
  declare global {
975
1001
  namespace JSX {
976
1002
  interface IntrinsicElements {
977
- 'gs-aggregate': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1003
+ 'gs-relative-growth-advantage': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
978
1004
  }
979
1005
  }
980
1006
  }
@@ -982,7 +1008,7 @@ declare global {
982
1008
 
983
1009
  declare global {
984
1010
  interface HTMLElementTagNameMap {
985
- 'gs-number-sequences-over-time': NumberSequencesOverTimeComponent;
1011
+ 'gs-aggregate': AggregateComponent;
986
1012
  }
987
1013
  }
988
1014
 
@@ -990,7 +1016,7 @@ declare global {
990
1016
  declare global {
991
1017
  namespace JSX {
992
1018
  interface IntrinsicElements {
993
- 'gs-number-sequences-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1019
+ 'gs-aggregate': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
994
1020
  }
995
1021
  }
996
1022
  }
@@ -1014,7 +1040,7 @@ declare global {
1014
1040
 
1015
1041
  declare global {
1016
1042
  interface HTMLElementTagNameMap {
1017
- 'gs-sequences-by-location': SequencesByLocationComponent;
1043
+ 'gs-number-sequences-over-time': NumberSequencesOverTimeComponent;
1018
1044
  }
1019
1045
  }
1020
1046
 
@@ -1022,7 +1048,7 @@ declare global {
1022
1048
  declare global {
1023
1049
  namespace JSX {
1024
1050
  interface IntrinsicElements {
1025
- 'gs-sequences-by-location': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1051
+ 'gs-number-sequences-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1026
1052
  }
1027
1053
  }
1028
1054
  }
@@ -1030,7 +1056,7 @@ declare global {
1030
1056
 
1031
1057
  declare global {
1032
1058
  interface HTMLElementTagNameMap {
1033
- 'gs-statistics': StatisticsComponent;
1059
+ 'gs-sequences-by-location': SequencesByLocationComponent;
1034
1060
  }
1035
1061
  }
1036
1062
 
@@ -1038,7 +1064,7 @@ declare global {
1038
1064
  declare global {
1039
1065
  namespace JSX {
1040
1066
  interface IntrinsicElements {
1041
- 'gs-statistics': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1067
+ 'gs-sequences-by-location': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1042
1068
  }
1043
1069
  }
1044
1070
  }
@@ -1046,7 +1072,7 @@ declare global {
1046
1072
 
1047
1073
  declare global {
1048
1074
  interface HTMLElementTagNameMap {
1049
- 'gs-wastewater-mutations-over-time': WastewaterMutationsOverTimeComponent;
1075
+ 'gs-statistics': StatisticsComponent;
1050
1076
  }
1051
1077
  }
1052
1078
 
@@ -1054,7 +1080,7 @@ declare global {
1054
1080
  declare global {
1055
1081
  namespace JSX {
1056
1082
  interface IntrinsicElements {
1057
- 'gs-wastewater-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1083
+ 'gs-statistics': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1058
1084
  }
1059
1085
  }
1060
1086
  }
@@ -1062,10 +1088,11 @@ declare global {
1062
1088
 
1063
1089
  declare global {
1064
1090
  interface HTMLElementTagNameMap {
1065
- 'gs-location-filter': LocationFilterComponent;
1091
+ 'gs-date-range-filter': DateRangeFilterComponent;
1066
1092
  }
1067
1093
  interface HTMLElementEventMap {
1068
- 'gs-location-changed': LocationChangedEvent;
1094
+ [gsEventNames.dateRangeFilterChanged]: CustomEvent<Record<string, string>>;
1095
+ [gsEventNames.dateRangeOptionChanged]: DateRangeOptionChangedEvent;
1069
1096
  }
1070
1097
  }
1071
1098
 
@@ -1073,7 +1100,7 @@ declare global {
1073
1100
  declare global {
1074
1101
  namespace JSX {
1075
1102
  interface IntrinsicElements {
1076
- 'gs-location-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1103
+ 'gs-date-range-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1077
1104
  }
1078
1105
  }
1079
1106
  }
@@ -1081,11 +1108,10 @@ declare global {
1081
1108
 
1082
1109
  declare global {
1083
1110
  interface HTMLElementTagNameMap {
1084
- 'gs-date-range-filter': DateRangeFilterComponent;
1111
+ 'gs-location-filter': LocationFilterComponent;
1085
1112
  }
1086
1113
  interface HTMLElementEventMap {
1087
- 'gs-date-range-filter-changed': CustomEvent<Record<string, string>>;
1088
- 'gs-date-range-option-changed': DateRangeOptionChangedEvent;
1114
+ [gsEventNames.locationChanged]: LocationChangedEvent;
1089
1115
  }
1090
1116
  }
1091
1117
 
@@ -1093,7 +1119,7 @@ declare global {
1093
1119
  declare global {
1094
1120
  namespace JSX {
1095
1121
  interface IntrinsicElements {
1096
- 'gs-date-range-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1122
+ 'gs-location-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1097
1123
  }
1098
1124
  }
1099
1125
  }
@@ -1104,7 +1130,7 @@ declare global {
1104
1130
  'gs-text-filter': TextFilterComponent;
1105
1131
  }
1106
1132
  interface HTMLElementEventMap {
1107
- 'gs-text-filter-changed': TextFilterChangedEvent;
1133
+ [gsEventNames.textFilterChanged]: TextFilterChangedEvent;
1108
1134
  }
1109
1135
  }
1110
1136
 
@@ -1123,7 +1149,7 @@ declare global {
1123
1149
  'gs-mutation-filter': MutationFilterComponent;
1124
1150
  }
1125
1151
  interface HTMLElementEventMap {
1126
- 'gs-mutation-filter-changed': CustomEvent<MutationsFilter>;
1152
+ [gsEventNames.mutationFilterChanged]: CustomEvent<MutationsFilter>;
1127
1153
  }
1128
1154
  }
1129
1155
 
@@ -1142,7 +1168,7 @@ declare global {
1142
1168
  'gs-lineage-filter': LineageFilterComponent;
1143
1169
  }
1144
1170
  interface HTMLElementEventMap {
1145
- 'gs-lineage-filter-changed': LineageFilterChangedEvent;
1171
+ [gsEventNames.lineageFilterChanged]: LineageFilterChangedEvent;
1146
1172
  }
1147
1173
  }
1148
1174
 
package/dist/util.js CHANGED
@@ -1,10 +1,11 @@
1
- import { D, a, L, T, d, v } from "./LineageFilterChangedEvent-ixHQkq8y.js";
1
+ import { D, a, L, T, d, g, v } from "./LineageFilterChangedEvent-b0iuroUL.js";
2
2
  export {
3
3
  D as DateRangeOptionChangedEvent,
4
4
  a as LineageFilterChangedEvent,
5
5
  L as LocationChangedEvent,
6
6
  T as TextFilterChangedEvent,
7
7
  d as dateRangeOptionPresets,
8
+ g as gsEventNames,
8
9
  v as views
9
10
  };
10
11
  //# sourceMappingURL=util.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genspectrum/dashboard-components",
3
- "version": "0.19.2",
3
+ "version": "0.19.3",
4
4
  "description": "GenSpectrum web components for building dashboards",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-only",
@@ -1,9 +1,11 @@
1
+ import { type gsEventNames } from './utils/gsEventNames';
2
+
1
3
  export * from './web-components';
2
4
 
3
5
  export { type ErrorEvent, UserFacingError } from './preact/components/error-display';
4
6
 
5
7
  declare global {
6
8
  interface HTMLElementEventMap {
7
- 'gs-error': ErrorEvent;
9
+ [gsEventNames.error]: ErrorEvent;
8
10
  }
9
11
  }
@@ -3,6 +3,7 @@ import { expect, fn, userEvent, waitFor, within } from '@storybook/test';
3
3
 
4
4
  import { ErrorDisplay, UserFacingError } from './error-display';
5
5
  import { ResizeContainer } from './resize-container';
6
+ import { gsEventNames } from '../../utils/gsEventNames';
6
7
 
7
8
  const meta: Meta = {
8
9
  title: 'Component/Error',
@@ -58,7 +59,7 @@ export const FiresEvent: StoryObj = {
58
59
 
59
60
  play: async ({ canvasElement }) => {
60
61
  const listenerMock = fn();
61
- canvasElement.addEventListener('gs-error', listenerMock);
62
+ canvasElement.addEventListener(gsEventNames.error, listenerMock);
62
63
 
63
64
  await waitFor(async () => {
64
65
  await expect(listenerMock.mock.calls.at(-1)![0].error.name).toStrictEqual('UserFacingError');
@@ -5,12 +5,11 @@ import { type ZodError } from 'zod';
5
5
  import { InfoHeadline1, InfoParagraph } from './info';
6
6
  import { Modal } from './modal';
7
7
  import { LapisError, UnknownLapisError } from '../../lapisApi/lapisApi';
8
-
9
- export const GS_ERROR_EVENT_TYPE = 'gs-error';
8
+ import { gsEventNames } from '../../utils/gsEventNames';
10
9
 
11
10
  export class ErrorEvent extends Event {
12
11
  constructor(public readonly error: Error) {
13
- super(GS_ERROR_EVENT_TYPE, {
12
+ super(gsEventNames.error, {
14
13
  bubbles: true,
15
14
  composed: true,
16
15
  });
@@ -1,18 +1,15 @@
1
- import { type FunctionComponent } from 'preact';
1
+ import { type ComponentChildren } from 'preact';
2
+ import { forwardRef } from 'preact/compat';
2
3
 
3
4
  export type Size = {
4
5
  width: string;
5
6
  height?: string;
6
7
  };
7
8
 
8
- export interface ResizeContainerProps {
9
- size: Size;
10
- }
11
-
12
- export const ResizeContainer: FunctionComponent<ResizeContainerProps> = ({ children, size }) => {
13
- return (
14
- <div style={size} className='bg-white'>
9
+ export const ResizeContainer = forwardRef<HTMLDivElement, { size: Size; children: ComponentChildren }>(
10
+ ({ size, children }, ref) => (
11
+ <div ref={ref} style={{ width: size.width, height: size.height, position: 'relative' }}>
15
12
  {children}
16
13
  </div>
17
- );
18
- };
14
+ ),
15
+ );
@@ -1,4 +1,5 @@
1
1
  import { type FunctionComponent } from 'preact';
2
+ import { type CSSProperties } from 'preact/compat';
2
3
  import { type JSXInternal } from 'preact/src/jsx';
3
4
 
4
5
  export type TooltipPosition =
@@ -14,6 +15,7 @@ export type TooltipPosition =
14
15
  export type TooltipProps = {
15
16
  content: string | JSXInternal.Element;
16
17
  position?: TooltipPosition;
18
+ tooltipStyle?: CSSProperties;
17
19
  };
18
20
 
19
21
  function getPositionCss(position?: TooltipPosition) {
@@ -39,12 +41,13 @@ function getPositionCss(position?: TooltipPosition) {
39
41
  }
40
42
  }
41
43
 
42
- const Tooltip: FunctionComponent<TooltipProps> = ({ children, content, position = 'bottom' }) => {
44
+ const Tooltip: FunctionComponent<TooltipProps> = ({ children, content, position = 'bottom', tooltipStyle }) => {
43
45
  return (
44
- <div className='relative w-full h-full'>
45
- <div className='peer w-full h-full'>{children}</div>
46
+ <div className={`relative group`}>
47
+ <div>{children}</div>
46
48
  <div
47
- className={`absolute z-10 w-max bg-white p-4 border border-gray-200 rounded-md invisible peer-hover:visible ${getPositionCss(position)}`}
49
+ className={`absolute z-10 w-max bg-white p-4 border border-gray-200 rounded-md invisible group-hover:visible ${getPositionCss(position)}`}
50
+ style={{ ...tooltipStyle }}
48
51
  >
49
52
  {content}
50
53
  </div>
@@ -7,6 +7,7 @@ import { useEffect, useRef, useState } from 'preact/hooks';
7
7
  import { DateRangeFilter, type DateRangeFilterProps } from './date-range-filter';
8
8
  import { previewHandles } from '../../../.storybook/preview';
9
9
  import { LAPIS_URL } from '../../constants';
10
+ import { gsEventNames } from '../../utils/gsEventNames';
10
11
  import { LapisUrlContextProvider } from '../LapisUrlContext';
11
12
  import { dateRangeOptionPresets, type DateRangeValue } from './dateRangeOption';
12
13
  import { expectInvalidAttributesErrorMessage } from '../shared/stories/expectErrorMessage';
@@ -27,7 +28,7 @@ const meta: Meta<DateRangeFilterProps> = {
27
28
  component: DateRangeFilter,
28
29
  parameters: {
29
30
  actions: {
30
- handles: ['gs-date-range-filter-changed', 'gs-date-range-option-changed', ...previewHandles],
31
+ handles: [gsEventNames.dateRangeFilterChanged, gsEventNames.dateRangeOptionChanged, ...previewHandles],
31
32
  },
32
33
  fetchMock: {},
33
34
  },
@@ -198,7 +199,7 @@ export const ChangingTheValueProgrammatically: StoryObj<DateRangeFilterProps> =
198
199
  const ref = useRef<HTMLDivElement>(null);
199
200
 
200
201
  useEffect(() => {
201
- ref.current?.addEventListener('gs-date-range-option-changed', (event) => {
202
+ ref.current?.addEventListener(gsEventNames.dateRangeOptionChanged, (event) => {
202
203
  setValue(event.detail);
203
204
  });
204
205
  }, []);
@@ -333,8 +334,8 @@ async function prepare(canvasElement: HTMLElement, step: StepFunction<PreactRend
333
334
  const filterChangedListenerMock = fn();
334
335
  const optionChangedListenerMock = fn();
335
336
  await step('Setup event listener mock', () => {
336
- canvasElement.addEventListener('gs-date-range-filter-changed', filterChangedListenerMock);
337
- canvasElement.addEventListener('gs-date-range-option-changed', optionChangedListenerMock);
337
+ canvasElement.addEventListener(gsEventNames.dateRangeFilterChanged, filterChangedListenerMock);
338
+ canvasElement.addEventListener(gsEventNames.dateRangeOptionChanged, optionChangedListenerMock);
338
339
  });
339
340
 
340
341
  return { canvas, filterChangedListenerMock, optionChangedListenerMock };
@@ -10,6 +10,7 @@ import {
10
10
  dateRangeOptionSchema,
11
11
  dateRangeValueSchema,
12
12
  } from './dateRangeOption';
13
+ import { gsEventNames } from '../../utils/gsEventNames';
13
14
  import { ClearableSelect } from '../components/clearable-select';
14
15
  import { ErrorBoundary } from '../components/error-boundary';
15
16
 
@@ -170,7 +171,7 @@ export const DateRangeFilterInner = ({
170
171
  };
171
172
 
172
173
  divRef.current?.dispatchEvent(
173
- new CustomEvent('gs-date-range-filter-changed', {
174
+ new CustomEvent(gsEventNames.dateRangeFilterChanged, {
174
175
  detail,
175
176
  bubbles: true,
176
177
  composed: true,
@@ -1,6 +1,7 @@
1
1
  import z from 'zod';
2
2
 
3
3
  import { toYYYYMMDD } from './dateConversion';
4
+ import { gsEventNames } from '../../utils/gsEventNames';
4
5
 
5
6
  /**
6
7
  * A date range option that can be used in the `gs-date-range-filter` component.
@@ -36,7 +37,7 @@ export type DateRangeValue = z.infer<typeof dateRangeValueSchema>;
36
37
 
37
38
  export class DateRangeOptionChangedEvent extends CustomEvent<DateRangeValue> {
38
39
  constructor(detail: DateRangeValue) {
39
- super('gs-date-range-option-changed', {
40
+ super(gsEventNames.dateRangeOptionChanged, {
40
41
  detail,
41
42
  bubbles: true,
42
43
  composed: true,
@@ -0,0 +1,219 @@
1
+ import { Fragment, type FunctionComponent } from 'preact';
2
+ import { useMemo, useState } from 'preact/hooks';
3
+
4
+ import { type CDSFeature } from './loadGff3';
5
+ import { MinMaxRangeSlider } from '../components/min-max-range-slider';
6
+ import Tooltip from '../components/tooltip';
7
+ import { singleGraphColorRGBByName } from '../shared/charts/colors';
8
+
9
+ const MAX_TICK_NUMBER = 20;
10
+ const MIN_TICK_NUMBER = 2;
11
+
12
+ function getMaxTickNumber(fullWidth: number): number {
13
+ const baseValue = 1000;
14
+
15
+ const normalizedRatio = fullWidth / baseValue;
16
+
17
+ const ticks = Math.round(MAX_TICK_NUMBER * normalizedRatio);
18
+ return Math.max(MIN_TICK_NUMBER, Math.min(MAX_TICK_NUMBER, ticks));
19
+ }
20
+
21
+ function getTicks(zoomStart: number, zoomEnd: number, fullWidth: number) {
22
+ const maxTickNumber = getMaxTickNumber(fullWidth);
23
+ const length = zoomEnd - zoomStart;
24
+ const minTickSize = length / maxTickNumber;
25
+ let maxTickSize = 10 ** Math.round(Math.log(minTickSize) / Math.log(10));
26
+ const numTicks = Math.round(length / maxTickSize);
27
+ if (numTicks > maxTickNumber) {
28
+ if (numTicks > 2 * maxTickNumber) {
29
+ maxTickSize = maxTickSize * 5;
30
+ } else {
31
+ maxTickSize = maxTickSize * 2;
32
+ }
33
+ }
34
+ const ticks = [];
35
+
36
+ const offset = Math.ceil(zoomStart / maxTickSize);
37
+ ticks.push({ start: zoomStart, end: zoomStart + maxTickSize - (zoomStart % maxTickSize) });
38
+ for (let i = 0; i <= numTicks; i++) {
39
+ const start = i * maxTickSize + offset * maxTickSize;
40
+ if (start >= zoomEnd) {
41
+ break;
42
+ }
43
+ const end = (i + 1) * maxTickSize + offset * maxTickSize;
44
+ if (end > zoomEnd) {
45
+ ticks.push({ start, end: zoomEnd });
46
+ break;
47
+ }
48
+ ticks.push({ start, end });
49
+ }
50
+ return ticks;
51
+ }
52
+
53
+ interface XAxisProps {
54
+ zoomStart: number;
55
+ zoomEnd: number;
56
+ fullWidth: number;
57
+ }
58
+
59
+ const XAxis: FunctionComponent<XAxisProps> = (componentProps) => {
60
+ const { zoomStart, zoomEnd, fullWidth } = componentProps;
61
+
62
+ const ticks = useMemo(() => getTicks(zoomStart, zoomEnd, fullWidth), [zoomStart, zoomEnd, fullWidth]);
63
+ const visibleRegionLength = zoomEnd - zoomStart;
64
+ const averageWidth = visibleRegionLength / ticks.length;
65
+ return (
66
+ <div className={'h-6 relative'}>
67
+ {ticks.map((tick, idx) => {
68
+ const width = tick.end - tick.start;
69
+ const widthPercent = (width / visibleRegionLength) * 100;
70
+ const leftPercent = ((tick.start - zoomStart) / visibleRegionLength) * 100;
71
+ return (
72
+ <div
73
+ key={idx}
74
+ class='absolute text-xs text-black px-1 hover:opacity-80 border-l border-r border-gray-400 border-t'
75
+ style={{
76
+ left: `calc(${leftPercent}% - 1px)`,
77
+ width: `calc(${widthPercent}% - 1px)`,
78
+ }}
79
+ >
80
+ {width >= averageWidth ? tick.start : ''}
81
+ </div>
82
+ );
83
+ })}
84
+ </div>
85
+ );
86
+ };
87
+
88
+ function getTooltipPosition(cds_start: number, cds_end: number, genomeLength: number) {
89
+ const tooltipY = cds_start + (cds_end - cds_start) / 2 < genomeLength / 2 ? 'start' : 'end';
90
+ return `bottom-${tooltipY}` as const;
91
+ }
92
+
93
+ interface TooltipContentProps {
94
+ cds: CDSFeature;
95
+ }
96
+
97
+ const TooltipContent: FunctionComponent<TooltipContentProps> = (componentProps) => {
98
+ const { cds } = componentProps;
99
+ const cdsLength = cds.positions.reduce((acc, pos) => acc + pos.end - pos.start, 0);
100
+ const cdsCoordinates = cds.positions.map((pos) => `${pos.start}-${pos.end}`).join(', ');
101
+ return (
102
+ <>
103
+ <p>
104
+ <span className='font-bold'>{cds.label}</span>
105
+ </p>
106
+ <table>
107
+ <tbody>
108
+ <tr>
109
+ <th className='font-medium px-2 text-right'>CDS Length</th>
110
+ <td>{cdsLength}</td>
111
+ </tr>
112
+ <tr>
113
+ <th className='font-medium px-2 text-right'>CDS Coordinates</th>
114
+ <td>{cdsCoordinates}</td>
115
+ </tr>
116
+ </tbody>
117
+ </table>
118
+ </>
119
+ );
120
+ };
121
+
122
+ interface CDSBarsProps {
123
+ gffData: CDSFeature[][];
124
+ zoomStart: number;
125
+ zoomEnd: number;
126
+ }
127
+
128
+ const CDSBars: FunctionComponent<CDSBarsProps> = (componentProps) => {
129
+ const { gffData, zoomStart, zoomEnd } = componentProps;
130
+ const visibleRegionLength = zoomEnd - zoomStart;
131
+ return (
132
+ <>
133
+ {gffData.map((data, listId) => (
134
+ <div className={'w-full h-6 relative'} key={listId}>
135
+ {data.map((cds, idx) => (
136
+ <Fragment key={idx}>
137
+ {cds.positions.map((position, posIdx) => {
138
+ const start = Math.max(position.start, zoomStart);
139
+ const end = Math.min(position.end, zoomEnd);
140
+
141
+ if (start >= end) {
142
+ return null;
143
+ }
144
+
145
+ const widthPercent = ((end - start) / visibleRegionLength) * 100;
146
+ const leftPercent = ((start - zoomStart) / visibleRegionLength) * 100;
147
+ const tooltipPosition = getTooltipPosition(start, end, visibleRegionLength);
148
+ return (
149
+ <Tooltip
150
+ content={<TooltipContent cds={cds} />}
151
+ position={tooltipPosition}
152
+ key={`${idx}-${posIdx}`}
153
+ tooltipStyle={{ left: `${leftPercent}%` }}
154
+ >
155
+ <div
156
+ className='absolute text-xs text-white rounded px-1 py-0.5 cursor-pointer hover:opacity-80 shadow-md'
157
+ style={{
158
+ left: `${leftPercent}%`,
159
+ width: `${widthPercent}%`,
160
+ backgroundColor: singleGraphColorRGBByName(cds.color),
161
+ whiteSpace: 'nowrap',
162
+ overflow: 'hidden',
163
+ textOverflow: 'ellipsis',
164
+ }}
165
+ >
166
+ {cds.label}
167
+ </div>
168
+ </Tooltip>
169
+ );
170
+ })}
171
+ </Fragment>
172
+ ))}
173
+ </div>
174
+ ))}
175
+ </>
176
+ );
177
+ };
178
+
179
+ interface CDSProps {
180
+ gffData: CDSFeature[][];
181
+ genomeLength: number;
182
+ width: number;
183
+ }
184
+
185
+ const CDSPlot: FunctionComponent<CDSProps> = (componentProps) => {
186
+ const { gffData, genomeLength, width } = componentProps;
187
+
188
+ const [zoomStart, setZoomStart] = useState(0);
189
+ const [zoomEnd, setZoomEnd] = useState(genomeLength);
190
+
191
+ const updateZoomStart = (newStart: number) => {
192
+ setZoomStart(newStart);
193
+ };
194
+
195
+ const updateZoomEnd = (newEnd: number) => {
196
+ setZoomEnd(newEnd);
197
+ };
198
+
199
+ return (
200
+ <div class='p-4'>
201
+ <CDSBars gffData={gffData} zoomStart={zoomStart} zoomEnd={zoomEnd} />
202
+ <XAxis zoomStart={zoomStart} zoomEnd={zoomEnd} fullWidth={width} />
203
+ <div class='relative w-full h-5'>
204
+ <MinMaxRangeSlider
205
+ min={zoomStart}
206
+ max={zoomEnd}
207
+ setMin={updateZoomStart}
208
+ setMax={updateZoomEnd}
209
+ rangeMin={0}
210
+ rangeMax={genomeLength}
211
+ step={1}
212
+ />
213
+ </div>
214
+ <XAxis zoomStart={0} zoomEnd={genomeLength} fullWidth={width} />
215
+ </div>
216
+ );
217
+ };
218
+
219
+ export default CDSPlot;