@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.
- package/LICENSE +201 -0
- package/charts/ChartMenu/ChartMenu.vue +140 -0
- package/charts/ChartMenu/download.ts +44 -0
- package/charts/ChartTooltip/ChartTooltip.vue +97 -0
- package/charts/ChoroplethMap/ChoroplethMap.md +398 -0
- package/charts/ChoroplethMap/ChoroplethMap.vue +777 -0
- package/charts/ChoroplethMap/hsaMapping.ts +4116 -0
- package/charts/DataTable/DataTable.md +143 -0
- package/charts/DataTable/DataTable.vue +277 -0
- package/charts/LineChart/LineChart.md +472 -0
- package/charts/LineChart/LineChart.vue +1216 -0
- package/charts/index.ts +23 -0
- package/charts/tooltip-position.ts +49 -0
- package/components/Box/Box.md +49 -0
- package/components/Box/Box.vue +52 -0
- package/components/Button/Button.md +67 -0
- package/components/Button/Button.vue +81 -0
- package/components/Expander/Expander.md +34 -0
- package/components/Expander/Expander.vue +95 -0
- package/components/Hint/Hint.md +29 -0
- package/components/Hint/Hint.vue +83 -0
- package/components/Icon/Icon.md +67 -0
- package/components/Icon/Icon.vue +112 -0
- package/components/LightDarkToggle/LightDarkToggle.vue +49 -0
- package/components/NumberInput/NumberInput.md +305 -0
- package/components/NumberInput/NumberInput.vue +531 -0
- package/components/SelectBox/SelectBox.md +110 -0
- package/components/SelectBox/SelectBox.vue +195 -0
- package/components/SidebarLayout/SidebarLayout.md +104 -0
- package/components/SidebarLayout/SidebarLayout.vue +466 -0
- package/components/Spinner/Spinner.md +51 -0
- package/components/Spinner/Spinner.vue +55 -0
- package/components/TextInput/TextInput.md +82 -0
- package/components/TextInput/TextInput.vue +94 -0
- package/components/Toggle/Toggle.md +81 -0
- package/components/Toggle/Toggle.vue +81 -0
- package/components/index.ts +15 -0
- package/index.json +121 -0
- package/package.json +24 -0
- package/pyodide/index.ts +7 -0
- package/pyodide/pyodide.worker.ts +233 -0
- package/pyodide/pyodideWorkerApi.ts +102 -0
- package/pyodide/useModel.ts +86 -0
- package/pyodide/vitePlugin.js +51 -0
- package/shared/ModelOutput.ts +88 -0
- package/shared/csv.ts +22 -0
- package/shared/index.ts +24 -0
- package/shared/transferUtils.ts +126 -0
- package/shared/useUrlParams.ts +296 -0
- package/theme/all.js +5 -0
- package/theme/base.css +176 -0
- package/theme/cfasim.css +3 -0
- package/theme/theme.css +113 -0
- package/theme/themes/cdc.css +22 -0
- package/theme/utilities.css +518 -0
- package/wasm/index.ts +2 -0
- package/wasm/useModel.ts +53 -0
- package/wasm/vitePlugin.js +35 -0
- package/wasm/wasm.worker.ts +74 -0
- 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<string, ColumnConfig>` | No | — |
|
|
128
|
+
| `menu` | `boolean \| string` | No | `true` |
|
|
129
|
+
| `csv` | `string \| (() => 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>
|