@cfasim-ui/docs 0.3.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.
Files changed (60) hide show
  1. package/LICENSE +201 -0
  2. package/charts/ChartMenu/ChartMenu.vue +140 -0
  3. package/charts/ChartMenu/download.ts +44 -0
  4. package/charts/ChartTooltip/ChartTooltip.vue +97 -0
  5. package/charts/ChoroplethMap/ChoroplethMap.md +398 -0
  6. package/charts/ChoroplethMap/ChoroplethMap.vue +777 -0
  7. package/charts/ChoroplethMap/hsaMapping.ts +4116 -0
  8. package/charts/DataTable/DataTable.md +143 -0
  9. package/charts/DataTable/DataTable.vue +277 -0
  10. package/charts/LineChart/LineChart.md +472 -0
  11. package/charts/LineChart/LineChart.vue +1216 -0
  12. package/charts/index.ts +23 -0
  13. package/charts/tooltip-position.ts +49 -0
  14. package/components/Box/Box.md +49 -0
  15. package/components/Box/Box.vue +52 -0
  16. package/components/Button/Button.md +67 -0
  17. package/components/Button/Button.vue +81 -0
  18. package/components/Expander/Expander.md +34 -0
  19. package/components/Expander/Expander.vue +95 -0
  20. package/components/Hint/Hint.md +29 -0
  21. package/components/Hint/Hint.vue +83 -0
  22. package/components/Icon/Icon.md +67 -0
  23. package/components/Icon/Icon.vue +112 -0
  24. package/components/LightDarkToggle/LightDarkToggle.vue +49 -0
  25. package/components/NumberInput/NumberInput.md +305 -0
  26. package/components/NumberInput/NumberInput.vue +531 -0
  27. package/components/SelectBox/SelectBox.md +110 -0
  28. package/components/SelectBox/SelectBox.vue +195 -0
  29. package/components/SidebarLayout/SidebarLayout.md +104 -0
  30. package/components/SidebarLayout/SidebarLayout.vue +466 -0
  31. package/components/Spinner/Spinner.md +51 -0
  32. package/components/Spinner/Spinner.vue +55 -0
  33. package/components/TextInput/TextInput.md +82 -0
  34. package/components/TextInput/TextInput.vue +94 -0
  35. package/components/Toggle/Toggle.md +81 -0
  36. package/components/Toggle/Toggle.vue +81 -0
  37. package/components/index.ts +15 -0
  38. package/index.json +121 -0
  39. package/package.json +24 -0
  40. package/pyodide/index.ts +7 -0
  41. package/pyodide/pyodide.worker.ts +233 -0
  42. package/pyodide/pyodideWorkerApi.ts +102 -0
  43. package/pyodide/useModel.ts +86 -0
  44. package/pyodide/vitePlugin.js +51 -0
  45. package/shared/ModelOutput.ts +88 -0
  46. package/shared/csv.ts +22 -0
  47. package/shared/index.ts +24 -0
  48. package/shared/transferUtils.ts +126 -0
  49. package/shared/useUrlParams.ts +296 -0
  50. package/theme/all.js +5 -0
  51. package/theme/base.css +176 -0
  52. package/theme/cfasim.css +3 -0
  53. package/theme/theme.css +113 -0
  54. package/theme/themes/cdc.css +22 -0
  55. package/theme/utilities.css +518 -0
  56. package/wasm/index.ts +2 -0
  57. package/wasm/useModel.ts +53 -0
  58. package/wasm/vitePlugin.js +35 -0
  59. package/wasm/wasm.worker.ts +74 -0
  60. package/wasm/wasmWorkerApi.ts +38 -0
@@ -0,0 +1,143 @@
1
+ # DataTable
2
+
3
+ A table for displaying columnar data. Accepts a plain record of arrays or a `ModelOutput` from a simulation.
4
+
5
+ ## Examples
6
+
7
+ ### Basic usage
8
+
9
+ <ComponentDemo>
10
+ <DataTable :data="{ day: [0, 1, 2, 3, 4], susceptible: [1000, 980, 945, 900, 860], infected: [1, 21, 56, 101, 141] }" />
11
+
12
+ <template #code>
13
+
14
+ ```vue
15
+ <DataTable
16
+ :data="{
17
+ day: [0, 1, 2, 3, 4],
18
+ susceptible: [1000, 980, 945, 900, 860],
19
+ infected: [1, 21, 56, 101, 141],
20
+ }"
21
+ />
22
+ ```
23
+
24
+ </template>
25
+ </ComponentDemo>
26
+
27
+ ### Column labels and width
28
+
29
+ <ComponentDemo>
30
+ <DataTable
31
+ :data="{ day: [0, 1, 2, 3, 4], susceptible: [1000, 980, 945, 900, 860], infected: [1, 21, 56, 101, 141] }"
32
+ :column-config="{
33
+ day: { label: 'Day', width: 'small' },
34
+ susceptible: { label: 'Susceptible' },
35
+ infected: { label: 'Infected' },
36
+ }"
37
+ />
38
+
39
+ <template #code>
40
+
41
+ ```vue
42
+ <DataTable
43
+ :data="{
44
+ day: [0, 1, 2, 3, 4],
45
+ susceptible: [1000, 980, 945, 900, 860],
46
+ infected: [1, 21, 56, 101, 141],
47
+ }"
48
+ :column-config="{
49
+ day: { label: 'Day', width: 'small' },
50
+ susceptible: { label: 'Susceptible' },
51
+ infected: { label: 'Infected' },
52
+ }"
53
+ />
54
+ ```
55
+
56
+ </template>
57
+ </ComponentDemo>
58
+
59
+ ### Cell class and max rows
60
+
61
+ <ComponentDemo>
62
+ <DataTable
63
+ :data="{ generation: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], cases: [1, 3, 8, 15, 28, 45, 62, 71, 55, 30] }"
64
+ :max-rows="5"
65
+ :column-config="{
66
+ generation: { label: 'Gen', cellClass: 'text-secondary', width: 50 },
67
+ cases: { label: 'Cases' },
68
+ }"
69
+ />
70
+
71
+ <template #code>
72
+
73
+ ```vue
74
+ <DataTable
75
+ :data="{
76
+ generation: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
77
+ cases: [1, 3, 8, 15, 28, 45, 62, 71, 55, 30],
78
+ }"
79
+ :max-rows="5"
80
+ :column-config="{
81
+ generation: { label: 'Gen', cellClass: 'text-secondary', width: 50 },
82
+ cases: { label: 'Cases' },
83
+ }"
84
+ />
85
+ ```
86
+
87
+ </template>
88
+ </ComponentDemo>
89
+
90
+ ### Download data link
91
+
92
+ Pass `download-link` to render a plain text link below the table for
93
+ downloading the CSV data. Set it to `true` for the default label or a
94
+ string to customize it. When set, the Download CSV menu item is hidden.
95
+ Use `filename` to control the downloaded filename, and `csv` to supply
96
+ custom CSV content.
97
+
98
+ <ComponentDemo>
99
+ <DataTable
100
+ :data="{ day: [0, 1, 2, 3, 4], cases: [1, 21, 56, 101, 141] }"
101
+ filename="sir-cases"
102
+ download-link="Download cases (CSV)"
103
+ />
104
+
105
+ <template #code>
106
+
107
+ ```vue
108
+ <DataTable
109
+ :data="{
110
+ day: [0, 1, 2, 3, 4],
111
+ cases: [1, 21, 56, 101, 141],
112
+ }"
113
+ filename="sir-cases"
114
+ download-link="Download cases (CSV)"
115
+ />
116
+ ```
117
+
118
+ </template>
119
+ </ComponentDemo>
120
+
121
+ ## Props
122
+
123
+ | Prop | Type | Required | Default |
124
+ |------|------|----------|---------|
125
+ | `data` | `TableData` | Yes | — |
126
+ | `maxRows` | `number` | No | — |
127
+ | `columnConfig` | `Record&lt;string, ColumnConfig&gt;` | No | — |
128
+ | `menu` | `boolean \| string` | No | `true` |
129
+ | `csv` | `string \| (() =&gt; string)` | No | — |
130
+ | `filename` | `string` | No | — |
131
+ | `downloadLink` | `boolean \| string` | No | — |
132
+
133
+
134
+ ### ColumnConfig
135
+
136
+ ```ts
137
+ interface ColumnConfig {
138
+ label?: string;
139
+ width?: "small" | "medium" | "large" | number;
140
+ align?: "left" | "center" | "right";
141
+ cellClass?: string;
142
+ }
143
+ ```
@@ -0,0 +1,277 @@
1
+ <script setup lang="ts">
2
+ import { computed } from "vue";
3
+ import type { CSSProperties } from "vue";
4
+ import type { ModelOutput } from "@cfasim-ui/shared";
5
+ import ChartMenu from "../ChartMenu/ChartMenu.vue";
6
+ import type { ChartMenuItem } from "../ChartMenu/ChartMenu.vue";
7
+ import { downloadCsv } from "../ChartMenu/download.js";
8
+
9
+ export type TableRecord = Record<string, ArrayLike<number | string | boolean>>;
10
+ export type TableData = TableRecord | ModelOutput;
11
+ export type ColumnWidth = "small" | "medium" | "large";
12
+ export type ColumnAlign = "left" | "center" | "right";
13
+
14
+ export interface ColumnConfig {
15
+ label?: string;
16
+ width?: ColumnWidth | number;
17
+ align?: ColumnAlign;
18
+ cellClass?: string;
19
+ }
20
+
21
+ const COLUMN_WIDTHS: Record<ColumnWidth, string> = {
22
+ small: "80px",
23
+ medium: "150px",
24
+ large: "250px",
25
+ };
26
+
27
+ const props = withDefaults(
28
+ defineProps<{
29
+ data: TableData;
30
+ maxRows?: number;
31
+ columnConfig?: Record<string, ColumnConfig>;
32
+ menu?: boolean | string;
33
+ /**
34
+ * Custom CSV content for the Download CSV menu item and download link.
35
+ * Can be a raw CSV string or a function returning one. When omitted, CSV
36
+ * is generated from the table data.
37
+ */
38
+ csv?: string | (() => string);
39
+ /** Filename (without extension) for downloaded CSV files. */
40
+ filename?: string;
41
+ /**
42
+ * Show a plain text link below the table to download the CSV data.
43
+ * Pass `true` for the default label ("Download data (CSV)") or a string
44
+ * to customize the link text. When set, the Download CSV menu item is
45
+ * hidden.
46
+ */
47
+ downloadLink?: boolean | string;
48
+ }>(),
49
+ { menu: true },
50
+ );
51
+
52
+ function columnLabel(name: string): string {
53
+ return props.columnConfig?.[name]?.label ?? name;
54
+ }
55
+
56
+ function columnStyle(name: string): Record<string, string> | undefined {
57
+ const w = props.columnConfig?.[name]?.width;
58
+ if (w == null) return undefined;
59
+ const value = typeof w === "number" ? `${w}px` : COLUMN_WIDTHS[w];
60
+ return { width: value, minWidth: value };
61
+ }
62
+
63
+ function columnAlignStyle(name: string): CSSProperties | undefined {
64
+ const align = props.columnConfig?.[name]?.align;
65
+ if (!align) return undefined;
66
+ return { textAlign: align };
67
+ }
68
+
69
+ function isModelOutput(d: TableData): d is ModelOutput {
70
+ return typeof (d as ModelOutput).column === "function";
71
+ }
72
+
73
+ interface Column {
74
+ name: string;
75
+ values: ArrayLike<number | string | boolean>;
76
+ enumLabels?: string[];
77
+ }
78
+
79
+ const columns = computed<Column[]>(() => {
80
+ const d = props.data;
81
+ if (isModelOutput(d)) {
82
+ return d.columns.map((col) => ({
83
+ name: col.name,
84
+ values: d.column(col.name),
85
+ enumLabels: col.enumLabels,
86
+ }));
87
+ }
88
+ return Object.entries(d).map(([name, values]) => ({ name, values }));
89
+ });
90
+
91
+ const rowCount = computed(() => {
92
+ const cols = columns.value;
93
+ if (cols.length === 0) return 0;
94
+ let max = 0;
95
+ for (const col of cols) max = Math.max(max, col.values.length);
96
+ return props.maxRows ? Math.min(max, props.maxRows) : max;
97
+ });
98
+
99
+ function cellValue(col: Column, row: number): string {
100
+ const v = col.values[row];
101
+ if (v === undefined || v === null) return "";
102
+ if (col.enumLabels && typeof v === "number")
103
+ return col.enumLabels[v] ?? String(v);
104
+ if (typeof v === "number") {
105
+ if (Number.isInteger(v)) return v.toString();
106
+ return v.toFixed(4);
107
+ }
108
+ if (typeof v === "boolean") return v ? "true" : "false";
109
+ return String(v);
110
+ }
111
+
112
+ function menuFilename() {
113
+ if (props.filename) return props.filename;
114
+ return typeof props.menu === "string" ? props.menu : "data";
115
+ }
116
+
117
+ function escapeCsvField(val: string): string {
118
+ if (val.includes(",") || val.includes('"') || val.includes("\n")) {
119
+ return `"${val.replace(/"/g, '""')}"`;
120
+ }
121
+ return val;
122
+ }
123
+
124
+ function toCsv(): string {
125
+ if (typeof props.csv === "function") return props.csv();
126
+ if (typeof props.csv === "string") return props.csv;
127
+ const cols = columns.value;
128
+ const rows = rowCount.value;
129
+ const headers = cols.map((c) => escapeCsvField(columnLabel(c.name)));
130
+ const lines = [headers.join(",")];
131
+ for (let r = 0; r < rows; r++) {
132
+ const cells = cols.map((c) => escapeCsvField(cellValue(c, r)));
133
+ lines.push(cells.join(","));
134
+ }
135
+ return lines.join("\n");
136
+ }
137
+
138
+ const menuItems = computed<ChartMenuItem[]>(() => {
139
+ if (props.downloadLink) return [];
140
+ return [
141
+ {
142
+ label: "Download CSV",
143
+ action: () => downloadCsv(toCsv(), menuFilename()),
144
+ },
145
+ ];
146
+ });
147
+
148
+ const downloadLinkText = computed(() => {
149
+ if (!props.downloadLink) return null;
150
+ return typeof props.downloadLink === "string"
151
+ ? props.downloadLink
152
+ : "Download data (CSV)";
153
+ });
154
+
155
+ const csvHref = computed(() => {
156
+ if (!props.downloadLink) return null;
157
+ return `data:text/csv;charset=utf-8,${encodeURIComponent(toCsv())}`;
158
+ });
159
+
160
+ const showMenu = computed(() => props.menu && menuItems.value.length > 0);
161
+ </script>
162
+
163
+ <template>
164
+ <div class="TableOuter" :class="{ 'has-menu': showMenu }">
165
+ <ChartMenu v-if="showMenu" :items="menuItems" />
166
+ <div class="TableWrapper">
167
+ <table class="Table">
168
+ <colgroup>
169
+ <col
170
+ v-for="col in columns"
171
+ :key="col.name"
172
+ :style="columnStyle(col.name)"
173
+ />
174
+ </colgroup>
175
+ <thead>
176
+ <tr>
177
+ <th
178
+ v-for="col in columns"
179
+ :key="col.name"
180
+ :style="columnAlignStyle(col.name)"
181
+ >
182
+ {{ columnLabel(col.name) }}
183
+ </th>
184
+ </tr>
185
+ </thead>
186
+ <tbody>
187
+ <tr v-for="row in rowCount" :key="row">
188
+ <td
189
+ v-for="col in columns"
190
+ :key="col.name"
191
+ :class="columnConfig?.[col.name]?.cellClass"
192
+ :style="columnAlignStyle(col.name)"
193
+ >
194
+ {{ cellValue(col, row - 1) }}
195
+ </td>
196
+ </tr>
197
+ </tbody>
198
+ </table>
199
+ </div>
200
+ <a
201
+ v-if="downloadLinkText"
202
+ class="data-table-download-link"
203
+ :href="csvHref!"
204
+ :download="`${menuFilename()}.csv`"
205
+ >
206
+ {{ downloadLinkText }}
207
+ </a>
208
+ </div>
209
+ </template>
210
+
211
+ <style scoped>
212
+ .TableOuter {
213
+ position: relative;
214
+ display: inline-block;
215
+ }
216
+
217
+ .TableOuter.has-menu {
218
+ margin-top: 32px;
219
+ }
220
+
221
+ .TableOuter :deep(.chart-menu-trigger-area) {
222
+ top: -32px;
223
+ right: 0;
224
+ }
225
+
226
+ .TableOuter:hover :deep(.chart-menu-button) {
227
+ opacity: 1;
228
+ }
229
+
230
+ .TableWrapper {
231
+ overflow-x: auto;
232
+ font-size: var(--font-size-sm);
233
+ }
234
+
235
+ .Table {
236
+ display: table;
237
+ margin: 0;
238
+ border-collapse: collapse;
239
+ font-variant-numeric: tabular-nums;
240
+ border: 1px solid var(--color-border);
241
+ }
242
+
243
+ .Table tr,
244
+ .Table th,
245
+ .Table td {
246
+ background: transparent;
247
+ border: none;
248
+ }
249
+
250
+ .Table th,
251
+ .Table td {
252
+ padding: 0.75em 1.25em;
253
+ white-space: nowrap;
254
+ }
255
+
256
+ .Table th {
257
+ font-weight: 600;
258
+ border-bottom: 1px solid var(--color-border-header);
259
+ position: sticky;
260
+ top: 0;
261
+ }
262
+
263
+ .Table tbody td {
264
+ border-bottom: 1px solid var(--color-border);
265
+ }
266
+
267
+ .Table tbody tr:last-child td {
268
+ border-bottom: none;
269
+ }
270
+
271
+ .data-table-download-link {
272
+ display: block;
273
+ text-align: right;
274
+ font-size: var(--font-size-sm);
275
+ margin-top: 0.25em;
276
+ }
277
+ </style>