@jjlmoya/utils-science 1.42.0 → 1.44.0
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/package.json +1 -1
- package/src/category/index.ts +4 -1
- package/src/entries.ts +7 -1
- package/src/index.ts +3 -0
- package/src/tests/diacritics_density.test.ts +140 -0
- package/src/tests/inverted_punctuation.test.ts +84 -0
- package/src/tests/locale_completeness.test.ts +2 -2
- package/src/tests/script_density.test.ts +94 -0
- package/src/tests/tool_validation.test.ts +2 -3
- package/src/tool/black-hole-event-horizon-simulator/bibliography.astro +14 -0
- package/src/tool/black-hole-event-horizon-simulator/bibliography.ts +16 -0
- package/src/tool/black-hole-event-horizon-simulator/black-hole-event-horizon-simulator.css +366 -0
- package/src/tool/black-hole-event-horizon-simulator/component.astro +302 -0
- package/src/tool/black-hole-event-horizon-simulator/entry.ts +29 -0
- package/src/tool/black-hole-event-horizon-simulator/i18n/de.ts +179 -0
- package/src/tool/black-hole-event-horizon-simulator/i18n/en.ts +259 -0
- package/src/tool/black-hole-event-horizon-simulator/i18n/es.ts +259 -0
- package/src/tool/black-hole-event-horizon-simulator/i18n/fr.ts +179 -0
- package/src/tool/black-hole-event-horizon-simulator/i18n/id.ts +259 -0
- package/src/tool/black-hole-event-horizon-simulator/i18n/it.ts +179 -0
- package/src/tool/black-hole-event-horizon-simulator/i18n/ja.ts +179 -0
- package/src/tool/black-hole-event-horizon-simulator/i18n/ko.ts +259 -0
- package/src/tool/black-hole-event-horizon-simulator/i18n/nl.ts +179 -0
- package/src/tool/black-hole-event-horizon-simulator/i18n/pl.ts +259 -0
- package/src/tool/black-hole-event-horizon-simulator/i18n/pt.ts +179 -0
- package/src/tool/black-hole-event-horizon-simulator/i18n/ru.ts +259 -0
- package/src/tool/black-hole-event-horizon-simulator/i18n/sv.ts +179 -0
- package/src/tool/black-hole-event-horizon-simulator/i18n/tr.ts +179 -0
- package/src/tool/black-hole-event-horizon-simulator/i18n/zh.ts +179 -0
- package/src/tool/black-hole-event-horizon-simulator/index.ts +11 -0
- package/src/tool/black-hole-event-horizon-simulator/logic.ts +68 -0
- package/src/tool/black-hole-event-horizon-simulator/seo.astro +15 -0
- package/src/tool/conway-life-rule-lab/i18n/de.ts +62 -5
- package/src/tool/conway-life-rule-lab/i18n/es.ts +62 -5
- package/src/tool/conway-life-rule-lab/i18n/fr.ts +62 -5
- package/src/tool/conway-life-rule-lab/i18n/id.ts +102 -45
- package/src/tool/conway-life-rule-lab/i18n/it.ts +62 -5
- package/src/tool/conway-life-rule-lab/i18n/ja.ts +62 -5
- package/src/tool/conway-life-rule-lab/i18n/ko.ts +62 -5
- package/src/tool/conway-life-rule-lab/i18n/nl.ts +102 -45
- package/src/tool/conway-life-rule-lab/i18n/pl.ts +62 -5
- package/src/tool/conway-life-rule-lab/i18n/pt.ts +62 -5
- package/src/tool/conway-life-rule-lab/i18n/ru.ts +62 -5
- package/src/tool/conway-life-rule-lab/i18n/sv.ts +62 -5
- package/src/tool/conway-life-rule-lab/i18n/tr.ts +62 -5
- package/src/tool/conway-life-rule-lab/i18n/zh.ts +62 -5
- package/src/tool/cosmic-inflation/i18n/es.ts +5 -5
- package/src/tool/cosmic-inflation/i18n/it.ts +51 -51
- package/src/tool/cosmic-inflation/i18n/ru.ts +42 -150
- package/src/tool/double-slit-decoherence/i18n/fr.ts +42 -86
- package/src/tool/double-slit-decoherence/i18n/it.ts +28 -72
- package/src/tool/double-slit-decoherence/i18n/pt.ts +35 -79
- package/src/tool/double-slit-decoherence/i18n/ru.ts +59 -59
- package/src/tool/double-slit-decoherence/i18n/sv.ts +33 -81
- package/src/tool/double-slit-decoherence/i18n/tr.ts +55 -55
- package/src/tool/dyson-sphere-energy-capture/i18n/de.ts +42 -123
- package/src/tool/dyson-sphere-energy-capture/i18n/es.ts +6 -6
- package/src/tool/dyson-sphere-energy-capture/i18n/fr.ts +63 -131
- package/src/tool/dyson-sphere-energy-capture/i18n/it.ts +36 -113
- package/src/tool/dyson-sphere-energy-capture/i18n/ja.ts +66 -42
- package/src/tool/dyson-sphere-energy-capture/i18n/ko.ts +66 -42
- package/src/tool/dyson-sphere-energy-capture/i18n/pl.ts +50 -133
- package/src/tool/dyson-sphere-energy-capture/i18n/pt.ts +51 -132
- package/src/tool/dyson-sphere-energy-capture/i18n/ru.ts +61 -98
- package/src/tool/dyson-sphere-energy-capture/i18n/sv.ts +56 -137
- package/src/tool/dyson-sphere-energy-capture/i18n/tr.ts +61 -142
- package/src/tool/dyson-sphere-energy-capture/i18n/zh.ts +67 -43
- package/src/tool/epidemic-sir-simulator/bibliography.astro +15 -0
- package/src/tool/epidemic-sir-simulator/bibliography.ts +37 -0
- package/src/tool/epidemic-sir-simulator/component.astro +384 -0
- package/src/tool/epidemic-sir-simulator/entry.ts +30 -0
- package/src/tool/epidemic-sir-simulator/epidemic-sir-simulator.css +624 -0
- package/src/tool/epidemic-sir-simulator/i18n/de.ts +347 -0
- package/src/tool/epidemic-sir-simulator/i18n/en.ts +347 -0
- package/src/tool/epidemic-sir-simulator/i18n/es.ts +347 -0
- package/src/tool/epidemic-sir-simulator/i18n/fr.ts +347 -0
- package/src/tool/epidemic-sir-simulator/i18n/id.ts +347 -0
- package/src/tool/epidemic-sir-simulator/i18n/it.ts +347 -0
- package/src/tool/epidemic-sir-simulator/i18n/ja.ts +347 -0
- package/src/tool/epidemic-sir-simulator/i18n/ko.ts +347 -0
- package/src/tool/epidemic-sir-simulator/i18n/nl.ts +347 -0
- package/src/tool/epidemic-sir-simulator/i18n/pl.ts +347 -0
- package/src/tool/epidemic-sir-simulator/i18n/pt.ts +347 -0
- package/src/tool/epidemic-sir-simulator/i18n/ru.ts +347 -0
- package/src/tool/epidemic-sir-simulator/i18n/sv.ts +347 -0
- package/src/tool/epidemic-sir-simulator/i18n/tr.ts +347 -0
- package/src/tool/epidemic-sir-simulator/i18n/zh.ts +347 -0
- package/src/tool/epidemic-sir-simulator/index.ts +12 -0
- package/src/tool/epidemic-sir-simulator/logic.ts +163 -0
- package/src/tool/epidemic-sir-simulator/seo.astro +16 -0
- package/src/tool/fermi-paradox-filter-lab/bibliography.astro +14 -0
- package/src/tool/fermi-paradox-filter-lab/bibliography.ts +24 -0
- package/src/tool/fermi-paradox-filter-lab/component.astro +257 -0
- package/src/tool/fermi-paradox-filter-lab/entry.ts +26 -0
- package/src/tool/fermi-paradox-filter-lab/fermi-paradox-filter-lab.css +228 -0
- package/src/tool/fermi-paradox-filter-lab/i18n/de.ts +192 -0
- package/src/tool/fermi-paradox-filter-lab/i18n/en.ts +192 -0
- package/src/tool/fermi-paradox-filter-lab/i18n/es.ts +193 -0
- package/src/tool/fermi-paradox-filter-lab/i18n/fr.ts +192 -0
- package/src/tool/fermi-paradox-filter-lab/i18n/id.ts +192 -0
- package/src/tool/fermi-paradox-filter-lab/i18n/it.ts +192 -0
- package/src/tool/fermi-paradox-filter-lab/i18n/ja.ts +192 -0
- package/src/tool/fermi-paradox-filter-lab/i18n/ko.ts +192 -0
- package/src/tool/fermi-paradox-filter-lab/i18n/nl.ts +192 -0
- package/src/tool/fermi-paradox-filter-lab/i18n/pl.ts +193 -0
- package/src/tool/fermi-paradox-filter-lab/i18n/pt.ts +192 -0
- package/src/tool/fermi-paradox-filter-lab/i18n/ru.ts +192 -0
- package/src/tool/fermi-paradox-filter-lab/i18n/sv.ts +192 -0
- package/src/tool/fermi-paradox-filter-lab/i18n/tr.ts +192 -0
- package/src/tool/fermi-paradox-filter-lab/i18n/zh.ts +192 -0
- package/src/tool/fermi-paradox-filter-lab/index.ts +11 -0
- package/src/tool/fermi-paradox-filter-lab/logic.ts +125 -0
- package/src/tool/fermi-paradox-filter-lab/seo.astro +15 -0
- package/src/tool/global-albedo-snowball-simulator/i18n/ru.ts +50 -63
- package/src/tool/lorenz-attractor/i18n/de.ts +52 -78
- package/src/tool/lorenz-attractor/i18n/es.ts +5 -5
- package/src/tool/lorenz-attractor/i18n/fr.ts +51 -77
- package/src/tool/lorenz-attractor/i18n/it.ts +36 -36
- package/src/tool/lorenz-attractor/i18n/pt.ts +51 -75
- package/src/tool/mandelbrot-fractal/i18n/de.ts +31 -58
- package/src/tool/mandelbrot-fractal/i18n/es.ts +5 -5
- package/src/tool/mandelbrot-fractal/i18n/fr.ts +39 -57
- package/src/tool/mandelbrot-fractal/i18n/it.ts +30 -48
- package/src/tool/mandelbrot-fractal/i18n/pl.ts +34 -52
- package/src/tool/mandelbrot-fractal/i18n/pt.ts +30 -30
- package/src/tool/mandelbrot-fractal/i18n/ru.ts +42 -60
- package/src/tool/mandelbrot-fractal/i18n/sv.ts +38 -65
- package/src/tool/natural-selection-drift/i18n/it.ts +18 -95
- package/src/tool/natural-selection-drift/i18n/pt.ts +76 -19
- package/src/tool/natural-selection-drift/i18n/ru.ts +27 -24
- package/src/tool/natural-selection-drift/i18n/sv.ts +9 -6
- package/src/tool/phase-diagram-critical-points/i18n/de.ts +30 -68
- package/src/tool/phase-diagram-critical-points/i18n/es.ts +4 -4
- package/src/tool/phase-diagram-critical-points/i18n/fr.ts +37 -75
- package/src/tool/phase-diagram-critical-points/i18n/it.ts +8 -8
- package/src/tool/planet-atmosphere-survival/i18n/ru.ts +49 -192
- package/src/tool/radioactive-decay/i18n/de.ts +16 -32
- package/src/tool/radioactive-decay/i18n/es.ts +5 -5
- package/src/tool/radioactive-decay/i18n/fr.ts +24 -40
- package/src/tool/radioactive-decay/i18n/it.ts +18 -35
- package/src/tool/radioactive-decay/i18n/pt.ts +34 -34
- package/src/tool/roche-limit-satellite-disruption/i18n/pl.ts +52 -128
- package/src/tool/temperature-timeline/i18n/de.ts +46 -174
- package/src/tool/temperature-timeline/i18n/es.ts +5 -5
- package/src/tool/temperature-timeline/i18n/fr.ts +69 -144
- package/src/tool/temperature-timeline/i18n/it.ts +34 -133
- package/src/tool/temperature-timeline/i18n/pt.ts +35 -174
- package/src/tool/temperature-timeline/i18n/ru.ts +48 -174
- package/src/tool/twin-paradox-visualizer/i18n/es.ts +5 -5
- package/src/tool/twin-paradox-visualizer/i18n/fr.ts +34 -34
- package/src/tool/twin-paradox-visualizer/i18n/pt.ts +31 -71
- package/src/tools.ts +6 -0
|
@@ -2,21 +2,45 @@ import { bibliography } from '../bibliography';
|
|
|
2
2
|
import type { ToolLocaleContent } from '../../../types';
|
|
3
3
|
|
|
4
4
|
const slug = 'dyson-sphere-energy-capture';
|
|
5
|
-
const title = '
|
|
6
|
-
const description = '
|
|
5
|
+
const title = '戴森球能量捕获模拟器';
|
|
6
|
+
const description = '通过捕获功率、轨道半径、材料质量和卡尔达肖夫尺度覆盖率,比较戴森群、环形、刚性壳和静态镜云等巨型结构设计。';
|
|
7
7
|
|
|
8
8
|
const howTo = [
|
|
9
|
-
{
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
{
|
|
10
|
+
name: '选择恒星类型',
|
|
11
|
+
text: '选择M型矮星、类太阳G型星、A型星、红巨星或蓝巨星,以设定光度和质量。',
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
name: '选择收集器结构',
|
|
15
|
+
text: '比较戴森群、赤道环、刚性壳和静态镜云的不同捕获假设。',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: '调整覆盖率和温度',
|
|
19
|
+
text: '改变覆盖率和工作温度,观察捕获功率和安全轨道半径如何变化。',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: '查看卡尔达肖夫目标',
|
|
23
|
+
text: '使用卡尔达肖夫目标滑块估算达到所选文明能量等级所需的恒星输出比例。',
|
|
24
|
+
},
|
|
13
25
|
];
|
|
14
26
|
|
|
15
27
|
const faq = [
|
|
16
|
-
{
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
28
|
+
{
|
|
29
|
+
question: '戴森群是什么?',
|
|
30
|
+
answer: '戴森群是围绕恒星运行的大量独立收集器的集合,通常比刚性壳更具可行性。',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
question: '为什么轨道半径会变化?',
|
|
34
|
+
answer: '轨道半径由收集器的工作温度和恒星光度共同决定。温度越高的收集器可以在更近的轨道上运行。',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
question: '卡尔达肖夫评级是什么意思?',
|
|
38
|
+
answer: '它将捕获的瓦特数转换为对数制的文明能量等级。',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
question: '材料质量精确吗?',
|
|
42
|
+
answer: '不精确。这是基于面积、面密度和稳定性假设的教育性估算。',
|
|
43
|
+
},
|
|
20
44
|
];
|
|
21
45
|
|
|
22
46
|
export const content: ToolLocaleContent = {
|
|
@@ -24,41 +48,41 @@ export const content: ToolLocaleContent = {
|
|
|
24
48
|
title,
|
|
25
49
|
description,
|
|
26
50
|
ui: {
|
|
27
|
-
visualization: '
|
|
28
|
-
starType: '
|
|
29
|
-
structureType: '
|
|
30
|
-
coverage: '
|
|
31
|
-
operatingTemp: '
|
|
32
|
-
kardashevTarget: '
|
|
33
|
-
kardashevRating: '
|
|
34
|
-
capturedPower: '
|
|
35
|
-
optimalRadius: '
|
|
36
|
-
targetCoverage: '
|
|
37
|
-
materialMass: '
|
|
38
|
-
captureMeter: '
|
|
39
|
-
statusReady: '
|
|
40
|
-
statusUnderbuilt: '
|
|
41
|
-
statusBalanced: '
|
|
42
|
-
statusExtreme: '
|
|
43
|
-
orbitalPeriod: '
|
|
44
|
-
collectorArea: '
|
|
45
|
-
mercuryMasses: '{value}
|
|
46
|
-
kilograms: '{value}
|
|
47
|
-
daysUnit: '{value}
|
|
48
|
-
starMDwarf: 'M
|
|
49
|
-
starSun: '
|
|
50
|
-
starA: 'A
|
|
51
|
-
starRedGiant: '
|
|
52
|
-
starBlueGiant: '
|
|
53
|
-
structureSwarm: '
|
|
54
|
-
structureRing: '
|
|
55
|
-
structureShell: '
|
|
56
|
-
structureStatite: '
|
|
51
|
+
visualization: '戴森收集器可视化',
|
|
52
|
+
starType: '恒星类型',
|
|
53
|
+
structureType: '结构',
|
|
54
|
+
coverage: '收集器覆盖率',
|
|
55
|
+
operatingTemp: '工作温度',
|
|
56
|
+
kardashevTarget: '卡尔达肖夫目标',
|
|
57
|
+
kardashevRating: '当前评级',
|
|
58
|
+
capturedPower: '捕获功率',
|
|
59
|
+
optimalRadius: '最优半径',
|
|
60
|
+
targetCoverage: '目标覆盖率',
|
|
61
|
+
materialMass: '材料质量',
|
|
62
|
+
captureMeter: '目标捕获进度',
|
|
63
|
+
statusReady: '调整系统以估算收集器需求。',
|
|
64
|
+
statusUnderbuilt: '覆盖率低于所选目标。',
|
|
65
|
+
statusBalanced: '覆盖率接近所选目标。',
|
|
66
|
+
statusExtreme: '此配置大幅超过目标。',
|
|
67
|
+
orbitalPeriod: '轨道周期',
|
|
68
|
+
collectorArea: '收集器面积',
|
|
69
|
+
mercuryMasses: '{value} 水星质量',
|
|
70
|
+
kilograms: '{value} 千克',
|
|
71
|
+
daysUnit: '{value} 天',
|
|
72
|
+
starMDwarf: 'M型矮星',
|
|
73
|
+
starSun: '类太阳G型星',
|
|
74
|
+
starA: 'A型星',
|
|
75
|
+
starRedGiant: '红巨星',
|
|
76
|
+
starBlueGiant: '蓝巨星',
|
|
77
|
+
structureSwarm: '戴森群',
|
|
78
|
+
structureRing: '赤道环',
|
|
79
|
+
structureShell: '刚性壳',
|
|
80
|
+
structureStatite: '静态镜云',
|
|
57
81
|
},
|
|
58
82
|
seo: [
|
|
59
|
-
{ type: 'title', text: '
|
|
60
|
-
{ type: 'paragraph', html: '
|
|
61
|
-
{ type: 'paragraph', html: '
|
|
83
|
+
{ type: 'title', text: '戴森球能量捕获模拟器', level: 2 },
|
|
84
|
+
{ type: 'paragraph', html: '本模拟器使用一阶恒星能量计算,比较戴森群、环形、壳体和静态云等概念。' },
|
|
85
|
+
{ type: 'paragraph', html: '估算捕获功率、热轨道半径、收集器面积、轨道周期、材料质量以及达到卡尔达肖夫目标所需的覆盖率。' },
|
|
62
86
|
],
|
|
63
87
|
faq,
|
|
64
88
|
bibliography,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { epidemicSirSimulator } from './index';
|
|
4
|
+
import type { KnownLocale } from '../../types';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
locale?: KnownLocale;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { locale = 'en' } = Astro.props;
|
|
11
|
+
const content = await epidemicSirSimulator.i18n[locale]?.();
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
{content && <SharedBibliography links={content.bibliography} />}
|
|
15
|
+
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { BibliographyEntry } from '../../types';
|
|
2
|
+
|
|
3
|
+
export const bibliography: BibliographyEntry[] = [
|
|
4
|
+
{
|
|
5
|
+
name: 'Kermack and McKendrick, A contribution to the mathematical theory of epidemics',
|
|
6
|
+
url: 'https://royalsocietypublishing.org/doi/10.1098/rspa.1927.0118',
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
name: 'Centers for Disease Control and Prevention, Principles of Epidemiology',
|
|
10
|
+
url: 'https://www.cdc.gov/csels/dsepd/ss1978/index.html',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: 'World Health Organization, Vaccines and immunization',
|
|
14
|
+
url: 'https://www.who.int/health-topics/vaccines-and-immunization',
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: 'Anderson and May, Infectious Diseases of Humans: Dynamics and Control',
|
|
18
|
+
url: 'https://global.oup.com/academic/product/infectious-diseases-of-humans-9780198540403',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: 'Keeling and Rohani, Modeling Infectious Diseases in Humans and Animals',
|
|
22
|
+
url: 'https://press.princeton.edu/books/hardcover/9780691116174/modeling-infectious-diseases-in-humans-and-animals',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: 'WHO, Estimating mortality from COVID-19',
|
|
26
|
+
url: 'https://www.who.int/news-room/commentaries/detail/estimating-mortality-from-covid-19',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: 'Fine, Herd immunity: history, theory, practice',
|
|
30
|
+
url: 'https://pmc.ncbi.nlm.nih.gov/articles/PMC1447608/',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'Delamater et al., Complexity of the Basic Reproduction Number (R0)',
|
|
34
|
+
url: 'https://pmc.ncbi.nlm.nih.gov/articles/PMC6537322/',
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
---
|
|
2
|
+
import './epidemic-sir-simulator.css';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
ui: Record<string, string>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const { ui } = Astro.props;
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
<div
|
|
12
|
+
class="sir-lab"
|
|
13
|
+
id="sir-lab"
|
|
14
|
+
data-day-label={ui.dayLabel}
|
|
15
|
+
data-days-unit={ui.daysUnit}
|
|
16
|
+
data-infectious-label={ui.infectiousLabel}
|
|
17
|
+
data-re-label={ui.reLabel}
|
|
18
|
+
>
|
|
19
|
+
<div class="sir-card">
|
|
20
|
+
<header class="sir-map-top">
|
|
21
|
+
<div>
|
|
22
|
+
<p>{ui.liveScenario}</p>
|
|
23
|
+
<h2>{ui.curveChart}</h2>
|
|
24
|
+
</div>
|
|
25
|
+
</header>
|
|
26
|
+
|
|
27
|
+
<div class="sir-card-body">
|
|
28
|
+
<section class="sir-map" aria-label={ui.curveChart}>
|
|
29
|
+
<div class="sir-chart-shell">
|
|
30
|
+
<svg class="sir-chart" id="sir-chart" viewBox="0 0 760 430" role="img" aria-label={ui.curveChart}>
|
|
31
|
+
<defs>
|
|
32
|
+
<linearGradient id="sir-grid-fade" x1="0" x2="1" y1="0" y2="1">
|
|
33
|
+
<stop offset="0%" stop-color="#eef7f4" />
|
|
34
|
+
<stop offset="100%" stop-color="#ffffff" />
|
|
35
|
+
</linearGradient>
|
|
36
|
+
</defs>
|
|
37
|
+
<rect class="sir-chart-bg" x="0" y="0" width="760" height="430" rx="28"></rect>
|
|
38
|
+
<g class="sir-grid" id="sir-grid"></g>
|
|
39
|
+
<line class="sir-baseline" x1="40" x2="720" y1="364" y2="364"></line>
|
|
40
|
+
<path class="sir-area sir-area-susceptible" id="sir-area-susceptible"></path>
|
|
41
|
+
<path class="sir-area sir-area-infected" id="sir-area-infected"></path>
|
|
42
|
+
<path class="sir-area sir-area-recovered" id="sir-area-recovered"></path>
|
|
43
|
+
<path class="sir-line sir-line-susceptible" id="sir-line-susceptible"></path>
|
|
44
|
+
<path class="sir-line sir-line-infected" id="sir-line-infected"></path>
|
|
45
|
+
<path class="sir-line sir-line-recovered" id="sir-line-recovered"></path>
|
|
46
|
+
<path class="sir-line sir-line-deaths" id="sir-line-deaths"></path>
|
|
47
|
+
<g class="sir-day-marker" id="sir-day-marker">
|
|
48
|
+
<line y1="34" y2="364"></line>
|
|
49
|
+
<circle r="7"></circle>
|
|
50
|
+
</g>
|
|
51
|
+
</svg>
|
|
52
|
+
<div class="sir-readout" id="sir-readout" aria-hidden="true">
|
|
53
|
+
<strong id="sir-readout-day">{ui.dayLabel} 0</strong>
|
|
54
|
+
<span><i class="sir-key sir-key-s"></i><b id="sir-readout-s">0</b></span>
|
|
55
|
+
<span><i class="sir-key sir-key-i"></i><b id="sir-readout-i">0</b></span>
|
|
56
|
+
<span><i class="sir-key sir-key-r"></i><b id="sir-readout-r">0</b></span>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<div class="sir-timeline">
|
|
61
|
+
<input id="sir-day" type="range" min="0" max="160" value="80" aria-label={ui.inspectDay} />
|
|
62
|
+
<output id="sir-day-output">{ui.dayLabel} 80</output>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div class="sir-legend" aria-label={ui.legend}>
|
|
66
|
+
<span><i class="sir-key sir-key-s"></i>{ui.susceptible}</span>
|
|
67
|
+
<span><i class="sir-key sir-key-i"></i>{ui.infected}</span>
|
|
68
|
+
<span><i class="sir-key sir-key-r"></i>{ui.recovered}</span>
|
|
69
|
+
<span><i class="sir-key sir-key-d"></i>{ui.deaths}</span>
|
|
70
|
+
</div>
|
|
71
|
+
</section>
|
|
72
|
+
|
|
73
|
+
<section class="sir-control-deck" aria-label={ui.controls}>
|
|
74
|
+
<div class="sir-control-header">
|
|
75
|
+
<div>
|
|
76
|
+
<p>{ui.controls}</p>
|
|
77
|
+
<strong>{ui.modelName}</strong>
|
|
78
|
+
</div>
|
|
79
|
+
<div class="sir-pill" id="sir-effective-r">{ui.reLabel} 0.00</div>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<div class="sir-metrics">
|
|
83
|
+
<div>
|
|
84
|
+
<span>{ui.peakInfected}</span>
|
|
85
|
+
<strong id="sir-peak">0</strong>
|
|
86
|
+
</div>
|
|
87
|
+
<div>
|
|
88
|
+
<span>{ui.peakDay}</span>
|
|
89
|
+
<strong id="sir-peak-day">0</strong>
|
|
90
|
+
</div>
|
|
91
|
+
<div>
|
|
92
|
+
<span>{ui.attackRate}</span>
|
|
93
|
+
<strong id="sir-attack">0%</strong>
|
|
94
|
+
</div>
|
|
95
|
+
<div>
|
|
96
|
+
<span>{ui.estimatedDeaths}</span>
|
|
97
|
+
<strong id="sir-deaths">0</strong>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<div class="sir-sliders">
|
|
102
|
+
<label class="sir-slider" for="sir-r0">
|
|
103
|
+
<span>{ui.r0}</span>
|
|
104
|
+
<output id="sir-r0-output">2.4</output>
|
|
105
|
+
<input id="sir-r0" type="range" min="0.6" max="7" step="0.1" value="2.4" />
|
|
106
|
+
</label>
|
|
107
|
+
|
|
108
|
+
<label class="sir-slider" for="sir-lethality">
|
|
109
|
+
<span>{ui.lethality}</span>
|
|
110
|
+
<output id="sir-lethality-output">1.0%</output>
|
|
111
|
+
<input id="sir-lethality" type="range" min="0" max="12" step="0.1" value="1" />
|
|
112
|
+
</label>
|
|
113
|
+
|
|
114
|
+
<label class="sir-slider" for="sir-vaccination">
|
|
115
|
+
<span>{ui.vaccination}</span>
|
|
116
|
+
<output id="sir-vaccination-output">35%</output>
|
|
117
|
+
<input id="sir-vaccination" type="range" min="0" max="95" step="1" value="35" />
|
|
118
|
+
</label>
|
|
119
|
+
|
|
120
|
+
<label class="sir-slider" for="sir-efficacy">
|
|
121
|
+
<span>{ui.vaccineEffectiveness}</span>
|
|
122
|
+
<output id="sir-efficacy-output">85%</output>
|
|
123
|
+
<input id="sir-efficacy" type="range" min="0" max="100" step="1" value="85" />
|
|
124
|
+
</label>
|
|
125
|
+
|
|
126
|
+
<label class="sir-slider" for="sir-infectious-days">
|
|
127
|
+
<span>{ui.infectiousDays}</span>
|
|
128
|
+
<output id="sir-infectious-output">7 {ui.daysUnit}</output>
|
|
129
|
+
<input id="sir-infectious-days" type="range" min="2" max="21" step="1" value="7" />
|
|
130
|
+
</label>
|
|
131
|
+
|
|
132
|
+
<label class="sir-slider" for="sir-initial">
|
|
133
|
+
<span>{ui.initialInfected}</span>
|
|
134
|
+
<output id="sir-initial-output">0.5%</output>
|
|
135
|
+
<input id="sir-initial" type="range" min="0.1" max="8" step="0.1" value="0.5" />
|
|
136
|
+
</label>
|
|
137
|
+
</div>
|
|
138
|
+
</section>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<script>
|
|
144
|
+
import { runSirSimulation } from './logic';
|
|
145
|
+
|
|
146
|
+
const root = document.getElementById('sir-lab');
|
|
147
|
+
const r0Input = document.getElementById('sir-r0') as HTMLInputElement | null;
|
|
148
|
+
const lethalityInput = document.getElementById('sir-lethality') as HTMLInputElement | null;
|
|
149
|
+
const vaccinationInput = document.getElementById('sir-vaccination') as HTMLInputElement | null;
|
|
150
|
+
const efficacyInput = document.getElementById('sir-efficacy') as HTMLInputElement | null;
|
|
151
|
+
const infectiousInput = document.getElementById('sir-infectious-days') as HTMLInputElement | null;
|
|
152
|
+
const initialInput = document.getElementById('sir-initial') as HTMLInputElement | null;
|
|
153
|
+
const dayInput = document.getElementById('sir-day') as HTMLInputElement | null;
|
|
154
|
+
const chart = document.getElementById('sir-chart') as unknown as SVGSVGElement | null;
|
|
155
|
+
const grid = document.getElementById('sir-grid');
|
|
156
|
+
const readout = document.getElementById('sir-readout');
|
|
157
|
+
const marker = document.getElementById('sir-day-marker');
|
|
158
|
+
const markerLine = marker?.querySelector('line');
|
|
159
|
+
const markerCircle = marker?.querySelector('circle');
|
|
160
|
+
const numberFormat = new Intl.NumberFormat('en', { maximumFractionDigits: 0 });
|
|
161
|
+
const storageKey = 'epidemic-sir-simulator:last-state';
|
|
162
|
+
const labels = {
|
|
163
|
+
day: root?.dataset.dayLabel ?? '',
|
|
164
|
+
days: root?.dataset.daysUnit ?? '',
|
|
165
|
+
infectious: root?.dataset.infectiousLabel ?? '',
|
|
166
|
+
re: root?.dataset.reLabel ?? '',
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const paths = {
|
|
170
|
+
susceptible: document.getElementById('sir-line-susceptible'),
|
|
171
|
+
infected: document.getElementById('sir-line-infected'),
|
|
172
|
+
recovered: document.getElementById('sir-line-recovered'),
|
|
173
|
+
deaths: document.getElementById('sir-line-deaths'),
|
|
174
|
+
susceptibleArea: document.getElementById('sir-area-susceptible'),
|
|
175
|
+
infectedArea: document.getElementById('sir-area-infected'),
|
|
176
|
+
recoveredArea: document.getElementById('sir-area-recovered'),
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
function getValue(input: HTMLInputElement | null, fallback: number) {
|
|
180
|
+
return input ? Number(input.value) : fallback;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function trackedInputs() {
|
|
184
|
+
return [r0Input, lethalityInput, vaccinationInput, efficacyInput, infectiousInput, initialInput, dayInput];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function restoreState() {
|
|
188
|
+
try {
|
|
189
|
+
const raw = window.localStorage.getItem(storageKey);
|
|
190
|
+
if (!raw) return;
|
|
191
|
+
const state = JSON.parse(raw) as Record<string, string>;
|
|
192
|
+
|
|
193
|
+
trackedInputs().forEach((input) => {
|
|
194
|
+
if (!input || typeof state[input.id] !== 'string') return;
|
|
195
|
+
input.value = state[input.id];
|
|
196
|
+
});
|
|
197
|
+
} catch {
|
|
198
|
+
window.localStorage.removeItem(storageKey);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function saveState() {
|
|
203
|
+
const state: Record<string, string> = {};
|
|
204
|
+
trackedInputs().forEach((input) => {
|
|
205
|
+
if (!input) return;
|
|
206
|
+
state[input.id] = input.value;
|
|
207
|
+
});
|
|
208
|
+
window.localStorage.setItem(storageKey, JSON.stringify(state));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function scalePoint(day: number, value: number, days: number, population: number) {
|
|
212
|
+
const width = 680;
|
|
213
|
+
const height = 330;
|
|
214
|
+
const x = 40 + day / days * width;
|
|
215
|
+
const y = 364 - value / population * height;
|
|
216
|
+
return { x, y };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function makePath(values: number[], days: number, population: number) {
|
|
220
|
+
return values.map((value, index) => {
|
|
221
|
+
const point = scalePoint(index, value, days, population);
|
|
222
|
+
return `${index === 0 ? 'M' : 'L'}${point.x.toFixed(2)} ${point.y.toFixed(2)}`;
|
|
223
|
+
}).join(' ');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function makeArea(values: number[], days: number, population: number) {
|
|
227
|
+
const line = makePath(values, days, population);
|
|
228
|
+
return `${line} L720 364 L40 364 Z`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function currentConfig(days: number, population: number) {
|
|
232
|
+
return {
|
|
233
|
+
population,
|
|
234
|
+
initialInfectedPercent: getValue(initialInput, 0.5),
|
|
235
|
+
r0: getValue(r0Input, 2.4),
|
|
236
|
+
infectiousDays: getValue(infectiousInput, 7),
|
|
237
|
+
vaccinationPercent: getValue(vaccinationInput, 35),
|
|
238
|
+
vaccineEffectiveness: getValue(efficacyInput, 85),
|
|
239
|
+
fatalityPercent: getValue(lethalityInput, 1),
|
|
240
|
+
days,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function setText(id: string, text: string) {
|
|
245
|
+
const node = document.getElementById(id);
|
|
246
|
+
if (!node) return;
|
|
247
|
+
node.classList.toggle('sir-value-long', text.length >= 6);
|
|
248
|
+
node.classList.toggle('sir-value-extra-long', text.length >= 8);
|
|
249
|
+
if (node.textContent === text) return;
|
|
250
|
+
node.textContent = text;
|
|
251
|
+
node.classList.remove('sir-value-flash');
|
|
252
|
+
node.getBoundingClientRect();
|
|
253
|
+
node.classList.add('sir-value-flash');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function retraceCurves() {
|
|
257
|
+
Object.values(paths).forEach((path) => {
|
|
258
|
+
if (!path || path.id.includes('area')) return;
|
|
259
|
+
path.style.animation = 'none';
|
|
260
|
+
path.getBoundingClientRect();
|
|
261
|
+
path.style.animation = '';
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function positionSliderValue(input: HTMLInputElement | null, outputId: string) {
|
|
266
|
+
const output = document.getElementById(outputId);
|
|
267
|
+
if (!input || !output) return;
|
|
268
|
+
const min = Number(input.min);
|
|
269
|
+
const max = Number(input.max);
|
|
270
|
+
const value = Number(input.value);
|
|
271
|
+
const progress = (value - min) / (max - min);
|
|
272
|
+
output.style.setProperty('--value-x', `${Math.max(0, Math.min(1, progress)) * 100}%`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function drawGrid() {
|
|
276
|
+
if (!grid || grid.childElementCount > 0) return;
|
|
277
|
+
const ns = 'http://www.w3.org/2000/svg';
|
|
278
|
+
|
|
279
|
+
for (let row = 0; row <= 4; row += 1) {
|
|
280
|
+
const line = document.createElementNS(ns, 'line');
|
|
281
|
+
const y = 34 + row * 82.5;
|
|
282
|
+
line.setAttribute('x1', '40');
|
|
283
|
+
line.setAttribute('x2', '720');
|
|
284
|
+
line.setAttribute('y1', y.toString());
|
|
285
|
+
line.setAttribute('y2', y.toString());
|
|
286
|
+
grid.append(line);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
for (let column = 0; column <= 5; column += 1) {
|
|
290
|
+
const line = document.createElementNS(ns, 'line');
|
|
291
|
+
const x = 40 + column * 136;
|
|
292
|
+
line.setAttribute('x1', x.toString());
|
|
293
|
+
line.setAttribute('x2', x.toString());
|
|
294
|
+
line.setAttribute('y1', '34');
|
|
295
|
+
line.setAttribute('y2', '364');
|
|
296
|
+
grid.append(line);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function drawCurves(result: ReturnType<typeof runSirSimulation>, days: number, population: number) {
|
|
301
|
+
const susceptible = result.points.map((point) => point.susceptible);
|
|
302
|
+
const infected = result.points.map((point) => point.infected);
|
|
303
|
+
const recovered = result.points.map((point) => point.recovered + point.vaccinated);
|
|
304
|
+
const deaths = result.points.map((point) => point.deaths);
|
|
305
|
+
|
|
306
|
+
paths.susceptible?.setAttribute('d', makePath(susceptible, days, population));
|
|
307
|
+
paths.infected?.setAttribute('d', makePath(infected, days, population));
|
|
308
|
+
paths.recovered?.setAttribute('d', makePath(recovered, days, population));
|
|
309
|
+
paths.deaths?.setAttribute('d', makePath(deaths, days, population));
|
|
310
|
+
paths.susceptibleArea?.setAttribute('d', makeArea(susceptible, days, population));
|
|
311
|
+
paths.infectedArea?.setAttribute('d', makeArea(infected, days, population));
|
|
312
|
+
paths.recoveredArea?.setAttribute('d', makeArea(recovered, days, population));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function updateSliderOutputs() {
|
|
316
|
+
setText('sir-r0-output', getValue(r0Input, 2.4).toFixed(1));
|
|
317
|
+
setText('sir-lethality-output', `${getValue(lethalityInput, 1).toFixed(1)}%`);
|
|
318
|
+
setText('sir-vaccination-output', `${getValue(vaccinationInput, 35).toFixed(0)}%`);
|
|
319
|
+
setText('sir-efficacy-output', `${getValue(efficacyInput, 85).toFixed(0)}%`);
|
|
320
|
+
setText('sir-infectious-output', `${getValue(infectiousInput, 7).toFixed(0)} ${labels.days}`);
|
|
321
|
+
setText('sir-initial-output', `${getValue(initialInput, 0.5).toFixed(1)}%`);
|
|
322
|
+
positionSliderValue(r0Input, 'sir-r0-output');
|
|
323
|
+
positionSliderValue(lethalityInput, 'sir-lethality-output');
|
|
324
|
+
positionSliderValue(vaccinationInput, 'sir-vaccination-output');
|
|
325
|
+
positionSliderValue(efficacyInput, 'sir-efficacy-output');
|
|
326
|
+
positionSliderValue(infectiousInput, 'sir-infectious-output');
|
|
327
|
+
positionSliderValue(initialInput, 'sir-initial-output');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function updateMetrics(result: ReturnType<typeof runSirSimulation>) {
|
|
331
|
+
setText('sir-peak', numberFormat.format(result.summary.peakInfected));
|
|
332
|
+
setText('sir-peak-day', result.summary.peakDay.toString());
|
|
333
|
+
setText('sir-attack', `${(result.summary.attackRate * 100).toFixed(1)}%`);
|
|
334
|
+
setText('sir-deaths', numberFormat.format(result.summary.finalDeaths));
|
|
335
|
+
setText('sir-effective-r', `${labels.re} ${result.summary.effectiveR0.toFixed(2)}`);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function updateInspection(result: ReturnType<typeof runSirSimulation>, days: number, population: number) {
|
|
339
|
+
const inspectedDay = Math.min(days, getValue(dayInput, 80));
|
|
340
|
+
const inspected = result.points[inspectedDay] ?? result.points[0];
|
|
341
|
+
const infectedPoint = scalePoint(inspectedDay, inspected.infected, days, population);
|
|
342
|
+
markerLine?.setAttribute('x1', infectedPoint.x.toFixed(2));
|
|
343
|
+
markerLine?.setAttribute('x2', infectedPoint.x.toFixed(2));
|
|
344
|
+
markerCircle?.setAttribute('cx', infectedPoint.x.toFixed(2));
|
|
345
|
+
markerCircle?.setAttribute('cy', infectedPoint.y.toFixed(2));
|
|
346
|
+
setText('sir-day-output', `${labels.day} ${inspectedDay}: ${numberFormat.format(inspected.infected)} ${labels.infectious}`);
|
|
347
|
+
setText('sir-readout-day', `${labels.day} ${inspectedDay}`);
|
|
348
|
+
setText('sir-readout-s', numberFormat.format(inspected.susceptible));
|
|
349
|
+
setText('sir-readout-i', numberFormat.format(inspected.infected));
|
|
350
|
+
setText('sir-readout-r', numberFormat.format(inspected.recovered + inspected.vaccinated));
|
|
351
|
+
|
|
352
|
+
if (readout) {
|
|
353
|
+
const x = infectedPoint.x / 760 * 100;
|
|
354
|
+
readout.style.setProperty('--readout-x', `${Math.max(4, Math.min(76, x))}%`);
|
|
355
|
+
readout.style.setProperty('--readout-y', `${Math.max(12, Math.min(82, infectedPoint.y / 430 * 100))}%`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function update() {
|
|
360
|
+
saveState();
|
|
361
|
+
const days = 160;
|
|
362
|
+
const population = 100000;
|
|
363
|
+
const result = runSirSimulation(currentConfig(days, population));
|
|
364
|
+
drawCurves(result, days, population);
|
|
365
|
+
updateSliderOutputs();
|
|
366
|
+
updateMetrics(result);
|
|
367
|
+
updateInspection(result, days, population);
|
|
368
|
+
retraceCurves();
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
drawGrid();
|
|
372
|
+
restoreState();
|
|
373
|
+
[r0Input, lethalityInput, vaccinationInput, efficacyInput, infectiousInput, initialInput, dayInput].forEach((input) => {
|
|
374
|
+
input?.addEventListener('input', update);
|
|
375
|
+
});
|
|
376
|
+
chart?.addEventListener('pointermove', (event) => {
|
|
377
|
+
if (!dayInput) return;
|
|
378
|
+
const rect = chart.getBoundingClientRect();
|
|
379
|
+
const localX = Math.max(40, Math.min(720, (event.clientX - rect.left) / rect.width * 760));
|
|
380
|
+
dayInput.value = Math.round((localX - 40) / 680 * 160).toString();
|
|
381
|
+
update();
|
|
382
|
+
});
|
|
383
|
+
update();
|
|
384
|
+
</script>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ScienceToolEntry, ToolLocaleContent } from '../../types';
|
|
2
|
+
|
|
3
|
+
export interface EpidemicSirSimulatorUI {
|
|
4
|
+
[key: string]: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export type EpidemicSirSimulatorLocaleContent = ToolLocaleContent<EpidemicSirSimulatorUI>;
|
|
8
|
+
|
|
9
|
+
export const epidemicSirSimulator: ScienceToolEntry<EpidemicSirSimulatorUI> = {
|
|
10
|
+
id: 'epidemic-sir-simulator',
|
|
11
|
+
icons: { bg: 'mdi:virus-outline', fg: 'mdi:chart-bell-curve' },
|
|
12
|
+
i18n: {
|
|
13
|
+
de: () => import('./i18n/de').then((m) => m.content),
|
|
14
|
+
en: () => import('./i18n/en').then((m) => m.content),
|
|
15
|
+
es: () => import('./i18n/es').then((m) => m.content),
|
|
16
|
+
fr: () => import('./i18n/fr').then((m) => m.content),
|
|
17
|
+
id: () => import('./i18n/id').then((m) => m.content),
|
|
18
|
+
it: () => import('./i18n/it').then((m) => m.content),
|
|
19
|
+
ja: () => import('./i18n/ja').then((m) => m.content),
|
|
20
|
+
ko: () => import('./i18n/ko').then((m) => m.content),
|
|
21
|
+
nl: () => import('./i18n/nl').then((m) => m.content),
|
|
22
|
+
pl: () => import('./i18n/pl').then((m) => m.content),
|
|
23
|
+
pt: () => import('./i18n/pt').then((m) => m.content),
|
|
24
|
+
ru: () => import('./i18n/ru').then((m) => m.content),
|
|
25
|
+
sv: () => import('./i18n/sv').then((m) => m.content),
|
|
26
|
+
tr: () => import('./i18n/tr').then((m) => m.content),
|
|
27
|
+
zh: () => import('./i18n/zh').then((m) => m.content),
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|