@genspectrum/dashboard-components 0.6.9 → 0.6.11
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/custom-elements.json +3 -3
- package/dist/dashboard-components.js +366 -75
- package/dist/dashboard-components.js.map +1 -1
- package/dist/genspectrum-components.d.ts +2 -2
- package/dist/style.css +65 -1
- package/package.json +4 -1
- package/src/preact/aggregatedData/aggregate.tsx +14 -4
- package/src/preact/components/chart.tsx +14 -1
- package/src/preact/components/fullscreen.tsx +57 -0
- package/src/preact/components/info.tsx +88 -1
- package/src/preact/components/resize-container.tsx +5 -1
- package/src/preact/mutationComparison/mutation-comparison.tsx +2 -0
- package/src/preact/mutations/mutations.tsx +34 -2
- package/src/preact/mutationsOverTime/__mockData__/aminoAcidMutations_byDayOverall.json +4726 -0
- package/src/preact/mutationsOverTime/__mockData__/nucleotideMutations_byMonthOverall.json +11143 -0
- package/src/preact/mutationsOverTime/__mockData__/nucleotideMutations_byWeekOverall.json +9154 -0
- package/src/preact/mutationsOverTime/getFilteredMutationsOverTime.spec.ts +66 -22
- package/src/preact/mutationsOverTime/getFilteredMutationsOverTimeData.ts +16 -7
- package/src/preact/mutationsOverTime/mutations-over-time.stories.tsx +17 -0
- package/src/preact/mutationsOverTime/mutations-over-time.tsx +28 -4
- package/src/preact/numberSequencesOverTime/number-sequences-over-time.tsx +42 -6
- package/src/preact/prevalenceOverTime/prevalence-over-time.stories.tsx +2 -1
- package/src/preact/prevalenceOverTime/prevalence-over-time.tsx +46 -53
- package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.tsx +2 -0
- package/src/query/queryMutationsOverTime.ts +10 -1
- package/src/web-components/visualization/gs-mutations-over-time.stories.ts +51 -1
- package/src/web-components/visualization/gs-prevalence-over-time.tsx +2 -4
|
@@ -976,14 +976,14 @@ declare global {
|
|
|
976
976
|
|
|
977
977
|
declare global {
|
|
978
978
|
interface HTMLElementTagNameMap {
|
|
979
|
-
'gs-
|
|
979
|
+
'gs-prevalence-over-time': PrevalenceOverTimeComponent;
|
|
980
980
|
}
|
|
981
981
|
}
|
|
982
982
|
|
|
983
983
|
|
|
984
984
|
declare global {
|
|
985
985
|
interface HTMLElementTagNameMap {
|
|
986
|
-
'gs-
|
|
986
|
+
'gs-relative-growth-advantage': RelativeGrowthAdvantageComponent;
|
|
987
987
|
}
|
|
988
988
|
}
|
|
989
989
|
|
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.
|
|
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)
|
|
@@ -1362,6 +1362,10 @@ html {
|
|
|
1362
1362
|
border-radius: inherit;
|
|
1363
1363
|
}
|
|
1364
1364
|
}
|
|
1365
|
+
.link {
|
|
1366
|
+
cursor: pointer;
|
|
1367
|
+
text-decoration-line: underline;
|
|
1368
|
+
}
|
|
1365
1369
|
.menu li.disabled {
|
|
1366
1370
|
cursor: not-allowed;
|
|
1367
1371
|
-webkit-user-select: none;
|
|
@@ -1858,6 +1862,14 @@ input.tab:checked + .tab-content,
|
|
|
1858
1862
|
.join > :where(*:not(:first-child)):is(.btn) {
|
|
1859
1863
|
margin-inline-start: calc(var(--border-btn) * -1);
|
|
1860
1864
|
}
|
|
1865
|
+
.link:focus {
|
|
1866
|
+
outline: 2px solid transparent;
|
|
1867
|
+
outline-offset: 2px;
|
|
1868
|
+
}
|
|
1869
|
+
.link:focus-visible {
|
|
1870
|
+
outline: 2px solid currentColor;
|
|
1871
|
+
outline-offset: 2px;
|
|
1872
|
+
}
|
|
1861
1873
|
.loading {
|
|
1862
1874
|
pointer-events: none;
|
|
1863
1875
|
display: inline-block;
|
|
@@ -2777,6 +2789,18 @@ input.tab:checked + .tab-content,
|
|
|
2777
2789
|
right: auto;
|
|
2778
2790
|
bottom: var(--tooltip-tail-offset);
|
|
2779
2791
|
}
|
|
2792
|
+
.iconify {
|
|
2793
|
+
display: inline-block;
|
|
2794
|
+
width: 1em;
|
|
2795
|
+
height: 1em;
|
|
2796
|
+
background-color: currentColor;
|
|
2797
|
+
-webkit-mask-image: var(--svg);
|
|
2798
|
+
mask-image: var(--svg);
|
|
2799
|
+
-webkit-mask-repeat: no-repeat;
|
|
2800
|
+
mask-repeat: no-repeat;
|
|
2801
|
+
-webkit-mask-size: 100% 100%;
|
|
2802
|
+
mask-size: 100% 100%;
|
|
2803
|
+
}
|
|
2780
2804
|
.visible {
|
|
2781
2805
|
visibility: visible;
|
|
2782
2806
|
}
|
|
@@ -2872,6 +2896,9 @@ input.tab:checked + .tab-content,
|
|
|
2872
2896
|
.ml-1 {
|
|
2873
2897
|
margin-left: 0.25rem;
|
|
2874
2898
|
}
|
|
2899
|
+
.ml-2 {
|
|
2900
|
+
margin-left: 0.5rem;
|
|
2901
|
+
}
|
|
2875
2902
|
.ml-2\.5 {
|
|
2876
2903
|
margin-left: 0.625rem;
|
|
2877
2904
|
}
|
|
@@ -2884,6 +2911,9 @@ input.tab:checked + .tab-content,
|
|
|
2884
2911
|
.mr-2 {
|
|
2885
2912
|
margin-right: 0.5rem;
|
|
2886
2913
|
}
|
|
2914
|
+
.mt-0\.5 {
|
|
2915
|
+
margin-top: 0.125rem;
|
|
2916
|
+
}
|
|
2887
2917
|
.mt-1 {
|
|
2888
2918
|
margin-top: 0.25rem;
|
|
2889
2919
|
}
|
|
@@ -2962,6 +2992,12 @@ input.tab:checked + .tab-content,
|
|
|
2962
2992
|
.resize {
|
|
2963
2993
|
resize: both;
|
|
2964
2994
|
}
|
|
2995
|
+
.list-inside {
|
|
2996
|
+
list-style-position: inside;
|
|
2997
|
+
}
|
|
2998
|
+
.list-disc {
|
|
2999
|
+
list-style-type: disc;
|
|
3000
|
+
}
|
|
2965
3001
|
.flex-row {
|
|
2966
3002
|
flex-direction: row;
|
|
2967
3003
|
}
|
|
@@ -2992,6 +3028,9 @@ input.tab:checked + .tab-content,
|
|
|
2992
3028
|
.overflow-auto {
|
|
2993
3029
|
overflow: auto;
|
|
2994
3030
|
}
|
|
3031
|
+
.overflow-x-auto {
|
|
3032
|
+
overflow-x: auto;
|
|
3033
|
+
}
|
|
2995
3034
|
.whitespace-nowrap {
|
|
2996
3035
|
white-space: nowrap;
|
|
2997
3036
|
}
|
|
@@ -3001,6 +3040,9 @@ input.tab:checked + .tab-content,
|
|
|
3001
3040
|
.rounded-full {
|
|
3002
3041
|
border-radius: 9999px;
|
|
3003
3042
|
}
|
|
3043
|
+
.rounded-lg {
|
|
3044
|
+
border-radius: 0.5rem;
|
|
3045
|
+
}
|
|
3004
3046
|
.rounded-md {
|
|
3005
3047
|
border-radius: 0.375rem;
|
|
3006
3048
|
}
|
|
@@ -3183,6 +3225,12 @@ input.tab:checked + .tab-content,
|
|
|
3183
3225
|
.duration-150 {
|
|
3184
3226
|
transition-duration: 150ms;
|
|
3185
3227
|
}
|
|
3228
|
+
.mdi--fullscreen {
|
|
3229
|
+
--svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M5 5h5v2H7v3H5zm9 0h5v5h-2V7h-3zm3 9h2v5h-5v-2h3zm-7 3v2H5v-5h2v3z'/%3E%3C/svg%3E");
|
|
3230
|
+
}
|
|
3231
|
+
.mdi--fullscreen-exit {
|
|
3232
|
+
--svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M14 14h5v2h-3v3h-2zm-9 0h5v5H8v-3H5zm3-9h2v5H5V8h3zm11 3v2h-5V5h2v3z'/%3E%3C/svg%3E");
|
|
3233
|
+
}
|
|
3186
3234
|
@media (min-width: 640px) {
|
|
3187
3235
|
|
|
3188
3236
|
.sm\:modal-middle {
|
|
@@ -3206,6 +3254,16 @@ input.tab:checked + .tab-content,
|
|
|
3206
3254
|
--tw-border-opacity: 1;
|
|
3207
3255
|
border-color: rgb(156 163 175 / var(--tw-border-opacity));
|
|
3208
3256
|
}
|
|
3257
|
+
.hover\:scale-110:hover {
|
|
3258
|
+
--tw-scale-x: 1.1;
|
|
3259
|
+
--tw-scale-y: 1.1;
|
|
3260
|
+
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
|
3261
|
+
}
|
|
3262
|
+
.hover\:scale-90:hover {
|
|
3263
|
+
--tw-scale-x: .9;
|
|
3264
|
+
--tw-scale-y: .9;
|
|
3265
|
+
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
|
3266
|
+
}
|
|
3209
3267
|
.hover\:bg-gray-100:hover {
|
|
3210
3268
|
--tw-bg-opacity: 1;
|
|
3211
3269
|
background-color: rgb(243 244 246 / var(--tw-bg-opacity));
|
|
@@ -3240,6 +3298,12 @@ input.tab:checked + .tab-content,
|
|
|
3240
3298
|
}
|
|
3241
3299
|
.peer:hover ~ .peer-hover\:visible {
|
|
3242
3300
|
visibility: visible;
|
|
3301
|
+
}
|
|
3302
|
+
@media (min-width: 640px) {
|
|
3303
|
+
|
|
3304
|
+
.sm\:max-w-5xl {
|
|
3305
|
+
max-width: 64rem;
|
|
3306
|
+
}
|
|
3243
3307
|
}.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{/*
|
|
3244
3308
|
/*rtl:begin:ignore*/left:0/*
|
|
3245
3309
|
/*rtl:end:ignore*/}/*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@genspectrum/dashboard-components",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.11",
|
|
4
4
|
"description": "GenSpectrum web components for building dashboards",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "AGPL-3.0-only",
|
|
@@ -67,6 +67,9 @@
|
|
|
67
67
|
"dayjs": "^1.11.10",
|
|
68
68
|
"flatpickr": "^4.6.13",
|
|
69
69
|
"gridjs": "^6.2.0",
|
|
70
|
+
"@iconify-json/mdi": "^1.1.67",
|
|
71
|
+
"@iconify-json/mdi-light": "^1.1.10",
|
|
72
|
+
"@iconify/tailwind": "^1.1.1",
|
|
70
73
|
"lit": "^3.1.3",
|
|
71
74
|
"object-hash": "^3.0.0",
|
|
72
75
|
"preact": "^10.20.1",
|
|
@@ -8,7 +8,8 @@ import { LapisUrlContext } from '../LapisUrlContext';
|
|
|
8
8
|
import { CsvDownloadButton } from '../components/csv-download-button';
|
|
9
9
|
import { ErrorBoundary } from '../components/error-boundary';
|
|
10
10
|
import { ErrorDisplay } from '../components/error-display';
|
|
11
|
-
import
|
|
11
|
+
import { Fullscreen } from '../components/fullscreen';
|
|
12
|
+
import Info, { InfoHeadline1, InfoParagraph } from '../components/info';
|
|
12
13
|
import { LoadingDisplay } from '../components/loading-display';
|
|
13
14
|
import { NoDataDisplay } from '../components/no-data-display';
|
|
14
15
|
import { ResizeContainer } from '../components/resize-container';
|
|
@@ -93,18 +94,27 @@ const AggregatedDataTabs: FunctionComponent<AggregatedDataTabsProps> = ({ data,
|
|
|
93
94
|
|
|
94
95
|
const tabs = views.map((view) => getTab(view));
|
|
95
96
|
|
|
96
|
-
return <Tabs tabs={tabs} toolbar={<Toolbar data={data} />} />;
|
|
97
|
+
return <Tabs tabs={tabs} toolbar={<Toolbar data={data} fields={fields} />} />;
|
|
97
98
|
};
|
|
98
99
|
|
|
99
100
|
type ToolbarProps = {
|
|
100
101
|
data: AggregateData;
|
|
102
|
+
fields: string[];
|
|
101
103
|
};
|
|
102
104
|
|
|
103
|
-
const Toolbar: FunctionComponent<ToolbarProps> = ({ data }) => {
|
|
105
|
+
const Toolbar: FunctionComponent<ToolbarProps> = ({ data, fields }) => {
|
|
104
106
|
return (
|
|
105
107
|
<div class='flex flex-row'>
|
|
106
108
|
<CsvDownloadButton className='mx-1 btn btn-xs' getData={() => data} filename='aggregate.csv' />
|
|
107
|
-
<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>
|
|
117
|
+
<Fullscreen />
|
|
108
118
|
</div>
|
|
109
119
|
);
|
|
110
120
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Chart, type ChartConfiguration } from 'chart.js';
|
|
2
|
-
import { useEffect, useRef } from 'preact/hooks';
|
|
2
|
+
import { type MutableRef, useEffect, useRef } from 'preact/hooks';
|
|
3
3
|
|
|
4
4
|
export interface GsChartProps {
|
|
5
5
|
configuration: ChartConfiguration;
|
|
@@ -21,6 +21,8 @@ const GsChart = ({ configuration }: GsChartProps) => {
|
|
|
21
21
|
|
|
22
22
|
chartRef.current = new Chart(ctx, configuration);
|
|
23
23
|
|
|
24
|
+
resizeChartAfterFullscreenChange(chartRef, ctx, configuration);
|
|
25
|
+
|
|
24
26
|
return () => {
|
|
25
27
|
chartRef.current?.destroy();
|
|
26
28
|
};
|
|
@@ -30,3 +32,14 @@ const GsChart = ({ configuration }: GsChartProps) => {
|
|
|
30
32
|
};
|
|
31
33
|
|
|
32
34
|
export default GsChart;
|
|
35
|
+
|
|
36
|
+
const resizeChartAfterFullscreenChange = (
|
|
37
|
+
chartRef: MutableRef<Chart | null>,
|
|
38
|
+
ctx: CanvasRenderingContext2D,
|
|
39
|
+
configuration: ChartConfiguration,
|
|
40
|
+
) => {
|
|
41
|
+
document.addEventListener('fullscreenchange', () => {
|
|
42
|
+
chartRef.current?.destroy();
|
|
43
|
+
chartRef.current = new Chart(ctx, configuration);
|
|
44
|
+
});
|
|
45
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'preact/hooks';
|
|
2
|
+
|
|
3
|
+
export const Fullscreen = () => {
|
|
4
|
+
const element = useRef<HTMLButtonElement>(null);
|
|
5
|
+
const isFullscreen = useFullscreenStatus();
|
|
6
|
+
return (
|
|
7
|
+
<button
|
|
8
|
+
ref={element}
|
|
9
|
+
onClick={async () => {
|
|
10
|
+
if (element.current) {
|
|
11
|
+
if (isFullscreen) {
|
|
12
|
+
await document.exitFullscreen();
|
|
13
|
+
} else {
|
|
14
|
+
const componentRoot = findComponentRoot(element.current);
|
|
15
|
+
if (componentRoot) {
|
|
16
|
+
await componentRoot.requestFullscreen();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}}
|
|
21
|
+
className={`mt-0.5 iconify text-2xl ${isFullscreen ? 'mdi--fullscreen-exit hover:scale-90' : 'mdi--fullscreen hover:scale-110'}`}
|
|
22
|
+
title={isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}
|
|
23
|
+
/>
|
|
24
|
+
);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function findComponentRoot(element: HTMLElement) {
|
|
28
|
+
return findShadowRoot(element)?.children[0];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function findShadowRoot(element: HTMLElement) {
|
|
32
|
+
let current: Node = element;
|
|
33
|
+
while (current) {
|
|
34
|
+
if (current instanceof ShadowRoot) {
|
|
35
|
+
return current;
|
|
36
|
+
}
|
|
37
|
+
if (current.parentNode === null) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
current = current.parentNode;
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function useFullscreenStatus(): boolean {
|
|
46
|
+
const [isFullscreen, setIsFullscreen] = useState<boolean>(document.fullscreenElement !== null);
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
const handleFullscreenChange = () => {
|
|
49
|
+
setIsFullscreen(document.fullscreenElement !== null);
|
|
50
|
+
};
|
|
51
|
+
document.addEventListener('fullscreenchange', handleFullscreenChange);
|
|
52
|
+
return () => {
|
|
53
|
+
document.removeEventListener('fullscreenchange', handleFullscreenChange);
|
|
54
|
+
};
|
|
55
|
+
}, []);
|
|
56
|
+
return isFullscreen;
|
|
57
|
+
}
|
|
@@ -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, '"').replace(/'/g, ''')}
|
|
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
|
+
}
|
|
@@ -10,5 +10,9 @@ export interface ResizeContainerProps {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export const ResizeContainer: FunctionComponent<ResizeContainerProps> = ({ children, size }) => {
|
|
13
|
-
return
|
|
13
|
+
return (
|
|
14
|
+
<div style={size} className='bg-white'>
|
|
15
|
+
{children}
|
|
16
|
+
</div>
|
|
17
|
+
);
|
|
14
18
|
};
|
|
@@ -11,6 +11,7 @@ import { type DisplayedSegment, SegmentSelector, useDisplayedSegments } from '..
|
|
|
11
11
|
import { CsvDownloadButton } from '../components/csv-download-button';
|
|
12
12
|
import { ErrorBoundary } from '../components/error-boundary';
|
|
13
13
|
import { ErrorDisplay } from '../components/error-display';
|
|
14
|
+
import { Fullscreen } from '../components/fullscreen';
|
|
14
15
|
import Info from '../components/info';
|
|
15
16
|
import { LoadingDisplay } from '../components/loading-display';
|
|
16
17
|
import { type DisplayedMutationType, MutationTypeSelector } from '../components/mutation-type-selector';
|
|
@@ -189,6 +190,7 @@ const Toolbar: FunctionComponent<ToolbarProps> = ({
|
|
|
189
190
|
filename='mutation_comparison.csv'
|
|
190
191
|
/>
|
|
191
192
|
<Info>Info for mutation comparison</Info>
|
|
193
|
+
<Fullscreen />
|
|
192
194
|
</>
|
|
193
195
|
);
|
|
194
196
|
};
|
|
@@ -18,7 +18,8 @@ import { type DisplayedSegment, SegmentSelector, useDisplayedSegments } from '..
|
|
|
18
18
|
import { CsvDownloadButton } from '../components/csv-download-button';
|
|
19
19
|
import { ErrorBoundary } from '../components/error-boundary';
|
|
20
20
|
import { ErrorDisplay } from '../components/error-display';
|
|
21
|
-
import
|
|
21
|
+
import { Fullscreen } from '../components/fullscreen';
|
|
22
|
+
import Info, { InfoHeadline1, InfoHeadline2, InfoLink, InfoParagraph } from '../components/info';
|
|
22
23
|
import { LoadingDisplay } from '../components/loading-display';
|
|
23
24
|
import { type DisplayedMutationType, MutationTypeSelector } from '../components/mutation-type-selector';
|
|
24
25
|
import { NoDataDisplay } from '../components/no-data-display';
|
|
@@ -207,7 +208,38 @@ const Toolbar: FunctionComponent<ToolbarProps> = ({
|
|
|
207
208
|
filename='insertions.csv'
|
|
208
209
|
/>
|
|
209
210
|
)}
|
|
210
|
-
<
|
|
211
|
+
<MutationsInfo />
|
|
212
|
+
<Fullscreen />
|
|
211
213
|
</>
|
|
212
214
|
);
|
|
213
215
|
};
|
|
216
|
+
|
|
217
|
+
const MutationsInfo = () => (
|
|
218
|
+
<Info>
|
|
219
|
+
<InfoHeadline1>Mutations</InfoHeadline1>
|
|
220
|
+
<InfoParagraph>
|
|
221
|
+
This shows mutations of a variant. There are three types of mutations:{' '}
|
|
222
|
+
<InfoLink href='https://www.genome.gov/genetics-glossary/Substitution'>substitutions</InfoLink>,{' '}
|
|
223
|
+
<InfoLink href='https://www.genome.gov/genetics-glossary/Deletion'>deletions</InfoLink> and{' '}
|
|
224
|
+
<InfoLink href='https://www.genome.gov/genetics-glossary/Insertion'>insertions</InfoLink>.
|
|
225
|
+
</InfoParagraph>
|
|
226
|
+
<InfoHeadline2>Proportion calculation</InfoHeadline2>
|
|
227
|
+
<InfoParagraph>
|
|
228
|
+
The proportion of a mutation is calculated by dividing the number of sequences with the mutation by the
|
|
229
|
+
total number of sequences with a non-ambiguous symbol at the position.
|
|
230
|
+
</InfoParagraph>
|
|
231
|
+
<InfoParagraph>
|
|
232
|
+
<b>Example:</b> Assume we look at nucleotide mutations at position 5 where the reference has a T and assume
|
|
233
|
+
there are 10 sequences in total:
|
|
234
|
+
<ul className='list-disc list-inside ml-2'>
|
|
235
|
+
<li>3 sequences have a C,</li>
|
|
236
|
+
<li>2 sequences have a T,</li>
|
|
237
|
+
<li>1 sequence has a G,</li>
|
|
238
|
+
<li>3 sequences have an N,</li>
|
|
239
|
+
<li>1 sequence has a Y (which means T or C),</li>
|
|
240
|
+
</ul>
|
|
241
|
+
then the proportion of the T5C mutation is 50%. The 4 sequences that have an N or Y are excluded from the
|
|
242
|
+
calculation.
|
|
243
|
+
</InfoParagraph>
|
|
244
|
+
</Info>
|
|
245
|
+
);
|