@cfasim-ui/charts 0.1.9 → 0.2.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/dist/ChartMenu/ChartMenu.d.ts +9 -0
- package/dist/ChartMenu/download.d.ts +4 -0
- package/dist/ChartTooltip/ChartTooltip.d.ts +37 -0
- package/dist/ChartTooltip/ChartTooltip.test.d.ts +1 -0
- package/dist/ChoroplethMap/ChoroplethMap.d.ts +91 -0
- package/dist/ChoroplethMap/ChoroplethMap.test.d.ts +1 -0
- package/dist/ChoroplethMap/hsaMapping.d.ts +4 -0
- package/dist/DataTable/DataTable.d.ts +21 -0
- package/dist/DataTable/DataTable.test.d.ts +1 -0
- package/dist/LineChart/LineChart.d.ts +96 -0
- package/dist/LineChart/LineChart.spec.d.ts +1 -0
- package/dist/LineChart/LineChart.test.d.ts +1 -0
- package/dist/index.css +2 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5236 -0
- package/package.json +18 -4
- package/src/ChartMenu/ChartMenu.vue +0 -140
- package/src/ChartMenu/download.ts +0 -44
- package/src/ChoroplethMap/ChoroplethMap.md +0 -330
- package/src/ChoroplethMap/ChoroplethMap.test.ts +0 -468
- package/src/ChoroplethMap/ChoroplethMap.vue +0 -629
- package/src/ChoroplethMap/hsaMapping.ts +0 -4116
- package/src/DataTable/DataTable.md +0 -101
- package/src/DataTable/DataTable.test.ts +0 -177
- package/src/DataTable/DataTable.vue +0 -217
- package/src/LineChart/LineChart.md +0 -182
- package/src/LineChart/LineChart.spec.ts +0 -11
- package/src/LineChart/LineChart.vue +0 -465
- package/src/index.ts +0 -21
|
@@ -1,101 +0,0 @@
|
|
|
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
|
-
<!--@include: ./_api/data-table.md-->
|
|
91
|
-
|
|
92
|
-
### ColumnConfig
|
|
93
|
-
|
|
94
|
-
```ts
|
|
95
|
-
interface ColumnConfig {
|
|
96
|
-
label?: string;
|
|
97
|
-
width?: "small" | "medium" | "large" | number;
|
|
98
|
-
align?: "left" | "center" | "right";
|
|
99
|
-
cellClass?: string;
|
|
100
|
-
}
|
|
101
|
-
```
|
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { mount } from "@vue/test-utils";
|
|
3
|
-
import DataTable from "./DataTable.vue";
|
|
4
|
-
import { ModelOutput } from "@cfasim-ui/shared";
|
|
5
|
-
|
|
6
|
-
describe("Table", () => {
|
|
7
|
-
it("renders a plain record", () => {
|
|
8
|
-
const wrapper = mount(DataTable, {
|
|
9
|
-
props: {
|
|
10
|
-
data: {
|
|
11
|
-
day: [0, 1, 2],
|
|
12
|
-
infected: [10, 25, 50],
|
|
13
|
-
},
|
|
14
|
-
},
|
|
15
|
-
});
|
|
16
|
-
const headers = wrapper.findAll("th");
|
|
17
|
-
expect(headers.map((h) => h.text())).toEqual(["day", "infected"]);
|
|
18
|
-
const rows = wrapper.findAll("tbody tr");
|
|
19
|
-
expect(rows).toHaveLength(3);
|
|
20
|
-
expect(rows[0].findAll("td").map((td) => td.text())).toEqual(["0", "10"]);
|
|
21
|
-
expect(rows[2].findAll("td").map((td) => td.text())).toEqual(["2", "50"]);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("formats floats to 4 decimal places", () => {
|
|
25
|
-
const wrapper = mount(DataTable, {
|
|
26
|
-
props: {
|
|
27
|
-
data: { value: [1.23456789] },
|
|
28
|
-
},
|
|
29
|
-
});
|
|
30
|
-
const cell = wrapper.find("tbody td");
|
|
31
|
-
expect(cell.text()).toBe("1.2346");
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it("renders a ModelOutput", () => {
|
|
35
|
-
const output = new ModelOutput(
|
|
36
|
-
2,
|
|
37
|
-
[
|
|
38
|
-
{ name: "day", type: "i32" },
|
|
39
|
-
{ name: "cases", type: "f64" },
|
|
40
|
-
],
|
|
41
|
-
[new Int32Array([0, 1]), new Float64Array([100.5, 200.75])],
|
|
42
|
-
);
|
|
43
|
-
const wrapper = mount(DataTable, { props: { data: output } });
|
|
44
|
-
const headers = wrapper.findAll("th");
|
|
45
|
-
expect(headers.map((h) => h.text())).toEqual(["day", "cases"]);
|
|
46
|
-
const rows = wrapper.findAll("tbody tr");
|
|
47
|
-
expect(rows).toHaveLength(2);
|
|
48
|
-
expect(rows[0].findAll("td").map((td) => td.text())).toEqual([
|
|
49
|
-
"0",
|
|
50
|
-
"100.5000",
|
|
51
|
-
]);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it("renders enum labels from ModelOutput", () => {
|
|
55
|
-
const output = new ModelOutput(
|
|
56
|
-
2,
|
|
57
|
-
[{ name: "status", type: "enum", enumLabels: ["S", "I", "R"] }],
|
|
58
|
-
[new Uint32Array([0, 2])],
|
|
59
|
-
);
|
|
60
|
-
const wrapper = mount(DataTable, { props: { data: output } });
|
|
61
|
-
const cells = wrapper.findAll("tbody td");
|
|
62
|
-
expect(cells.map((c) => c.text())).toEqual(["S", "R"]);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("respects maxRows", () => {
|
|
66
|
-
const wrapper = mount(DataTable, {
|
|
67
|
-
props: {
|
|
68
|
-
data: { x: [1, 2, 3, 4, 5] },
|
|
69
|
-
maxRows: 3,
|
|
70
|
-
},
|
|
71
|
-
});
|
|
72
|
-
expect(wrapper.findAll("tbody tr")).toHaveLength(3);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("handles empty data", () => {
|
|
76
|
-
const wrapper = mount(DataTable, { props: { data: {} } });
|
|
77
|
-
expect(wrapper.findAll("th")).toHaveLength(0);
|
|
78
|
-
expect(wrapper.findAll("tbody tr")).toHaveLength(0);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it("uses columnConfig labels", () => {
|
|
82
|
-
const wrapper = mount(DataTable, {
|
|
83
|
-
props: {
|
|
84
|
-
data: { day: [0], infected: [10] },
|
|
85
|
-
columnConfig: {
|
|
86
|
-
day: { label: "Day #" },
|
|
87
|
-
infected: { label: "Total Infected" },
|
|
88
|
-
},
|
|
89
|
-
},
|
|
90
|
-
});
|
|
91
|
-
const headers = wrapper.findAll("th");
|
|
92
|
-
expect(headers.map((h) => h.text())).toEqual(["Day #", "Total Infected"]);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it("falls back to column name when no label configured", () => {
|
|
96
|
-
const wrapper = mount(DataTable, {
|
|
97
|
-
props: {
|
|
98
|
-
data: { day: [0], infected: [10] },
|
|
99
|
-
columnConfig: { day: { label: "Day #" } },
|
|
100
|
-
},
|
|
101
|
-
});
|
|
102
|
-
const headers = wrapper.findAll("th");
|
|
103
|
-
expect(headers.map((h) => h.text())).toEqual(["Day #", "infected"]);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it("applies named column widths", () => {
|
|
107
|
-
const wrapper = mount(DataTable, {
|
|
108
|
-
props: {
|
|
109
|
-
data: { day: [0], infected: [10] },
|
|
110
|
-
columnConfig: {
|
|
111
|
-
day: { width: "small" },
|
|
112
|
-
infected: { width: "large" },
|
|
113
|
-
},
|
|
114
|
-
},
|
|
115
|
-
});
|
|
116
|
-
const cols = wrapper.findAll("col");
|
|
117
|
-
expect(cols[0].attributes("style")).toContain("width: 80px");
|
|
118
|
-
expect(cols[1].attributes("style")).toContain("width: 250px");
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("applies pixel column widths", () => {
|
|
122
|
-
const wrapper = mount(DataTable, {
|
|
123
|
-
props: {
|
|
124
|
-
data: { day: [0] },
|
|
125
|
-
columnConfig: { day: { width: 120 } },
|
|
126
|
-
},
|
|
127
|
-
});
|
|
128
|
-
const col = wrapper.find("col");
|
|
129
|
-
expect(col.attributes("style")).toContain("width: 120px");
|
|
130
|
-
expect(col.attributes("style")).toContain("min-width: 120px");
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it("applies column alignment to th and td", () => {
|
|
134
|
-
const wrapper = mount(DataTable, {
|
|
135
|
-
props: {
|
|
136
|
-
data: { day: [0], infected: [10] },
|
|
137
|
-
columnConfig: {
|
|
138
|
-
day: { align: "center" },
|
|
139
|
-
infected: { align: "left" },
|
|
140
|
-
},
|
|
141
|
-
},
|
|
142
|
-
});
|
|
143
|
-
const headers = wrapper.findAll("th");
|
|
144
|
-
expect(headers[0].attributes("style")).toContain("text-align: center");
|
|
145
|
-
expect(headers[1].attributes("style")).toContain("text-align: left");
|
|
146
|
-
const cells = wrapper.findAll("tbody td");
|
|
147
|
-
expect(cells[0].attributes("style")).toContain("text-align: center");
|
|
148
|
-
expect(cells[1].attributes("style")).toContain("text-align: left");
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it("uses default alignment when no align configured", () => {
|
|
152
|
-
const wrapper = mount(DataTable, {
|
|
153
|
-
props: {
|
|
154
|
-
data: { day: [0] },
|
|
155
|
-
},
|
|
156
|
-
});
|
|
157
|
-
const th = wrapper.find("th");
|
|
158
|
-
expect(th.attributes("style")).toBeUndefined();
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it("applies cellClass to td elements", () => {
|
|
162
|
-
const wrapper = mount(DataTable, {
|
|
163
|
-
props: {
|
|
164
|
-
data: { day: [0, 1], value: [10, 20] },
|
|
165
|
-
columnConfig: {
|
|
166
|
-
day: { cellClass: "text-secondary" },
|
|
167
|
-
},
|
|
168
|
-
},
|
|
169
|
-
});
|
|
170
|
-
const rows = wrapper.findAll("tbody tr");
|
|
171
|
-
for (const row of rows) {
|
|
172
|
-
const cells = row.findAll("td");
|
|
173
|
-
expect(cells[0].classes()).toContain("text-secondary");
|
|
174
|
-
expect(cells[1].classes()).not.toContain("text-secondary");
|
|
175
|
-
}
|
|
176
|
-
});
|
|
177
|
-
});
|
|
@@ -1,217 +0,0 @@
|
|
|
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
|
-
{ menu: true },
|
|
35
|
-
);
|
|
36
|
-
|
|
37
|
-
function columnLabel(name: string): string {
|
|
38
|
-
return props.columnConfig?.[name]?.label ?? name;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function columnStyle(name: string): Record<string, string> | undefined {
|
|
42
|
-
const w = props.columnConfig?.[name]?.width;
|
|
43
|
-
if (w == null) return undefined;
|
|
44
|
-
const value = typeof w === "number" ? `${w}px` : COLUMN_WIDTHS[w];
|
|
45
|
-
return { width: value, minWidth: value };
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function columnAlignStyle(name: string): CSSProperties | undefined {
|
|
49
|
-
const align = props.columnConfig?.[name]?.align;
|
|
50
|
-
if (!align) return undefined;
|
|
51
|
-
return { textAlign: align };
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function isModelOutput(d: TableData): d is ModelOutput {
|
|
55
|
-
return typeof (d as ModelOutput).column === "function";
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
interface Column {
|
|
59
|
-
name: string;
|
|
60
|
-
values: ArrayLike<number | string | boolean>;
|
|
61
|
-
enumLabels?: string[];
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const columns = computed<Column[]>(() => {
|
|
65
|
-
const d = props.data;
|
|
66
|
-
if (isModelOutput(d)) {
|
|
67
|
-
return d.columns.map((col) => ({
|
|
68
|
-
name: col.name,
|
|
69
|
-
values: d.column(col.name),
|
|
70
|
-
enumLabels: col.enumLabels,
|
|
71
|
-
}));
|
|
72
|
-
}
|
|
73
|
-
return Object.entries(d).map(([name, values]) => ({ name, values }));
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
const rowCount = computed(() => {
|
|
77
|
-
const cols = columns.value;
|
|
78
|
-
if (cols.length === 0) return 0;
|
|
79
|
-
let max = 0;
|
|
80
|
-
for (const col of cols) max = Math.max(max, col.values.length);
|
|
81
|
-
return props.maxRows ? Math.min(max, props.maxRows) : max;
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
function cellValue(col: Column, row: number): string {
|
|
85
|
-
const v = col.values[row];
|
|
86
|
-
if (v === undefined || v === null) return "";
|
|
87
|
-
if (col.enumLabels && typeof v === "number")
|
|
88
|
-
return col.enumLabels[v] ?? String(v);
|
|
89
|
-
if (typeof v === "number") {
|
|
90
|
-
if (Number.isInteger(v)) return v.toString();
|
|
91
|
-
return v.toFixed(4);
|
|
92
|
-
}
|
|
93
|
-
if (typeof v === "boolean") return v ? "true" : "false";
|
|
94
|
-
return String(v);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function menuFilename() {
|
|
98
|
-
return typeof props.menu === "string" ? props.menu : "data";
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function escapeCsvField(val: string): string {
|
|
102
|
-
if (val.includes(",") || val.includes('"') || val.includes("\n")) {
|
|
103
|
-
return `"${val.replace(/"/g, '""')}"`;
|
|
104
|
-
}
|
|
105
|
-
return val;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function toCsv(): string {
|
|
109
|
-
const cols = columns.value;
|
|
110
|
-
const rows = rowCount.value;
|
|
111
|
-
const headers = cols.map((c) => escapeCsvField(columnLabel(c.name)));
|
|
112
|
-
const lines = [headers.join(",")];
|
|
113
|
-
for (let r = 0; r < rows; r++) {
|
|
114
|
-
const cells = cols.map((c) => escapeCsvField(cellValue(c, r)));
|
|
115
|
-
lines.push(cells.join(","));
|
|
116
|
-
}
|
|
117
|
-
return lines.join("\n");
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const menuItems = computed<ChartMenuItem[]>(() => [
|
|
121
|
-
{ label: "Download CSV", action: () => downloadCsv(toCsv(), menuFilename()) },
|
|
122
|
-
]);
|
|
123
|
-
</script>
|
|
124
|
-
|
|
125
|
-
<template>
|
|
126
|
-
<div class="TableOuter" :class="{ 'has-menu': menu }">
|
|
127
|
-
<ChartMenu v-if="menu" :items="menuItems" />
|
|
128
|
-
<div class="TableWrapper">
|
|
129
|
-
<table class="Table">
|
|
130
|
-
<colgroup>
|
|
131
|
-
<col
|
|
132
|
-
v-for="col in columns"
|
|
133
|
-
:key="col.name"
|
|
134
|
-
:style="columnStyle(col.name)"
|
|
135
|
-
/>
|
|
136
|
-
</colgroup>
|
|
137
|
-
<thead>
|
|
138
|
-
<tr>
|
|
139
|
-
<th
|
|
140
|
-
v-for="col in columns"
|
|
141
|
-
:key="col.name"
|
|
142
|
-
:style="columnAlignStyle(col.name)"
|
|
143
|
-
>
|
|
144
|
-
{{ columnLabel(col.name) }}
|
|
145
|
-
</th>
|
|
146
|
-
</tr>
|
|
147
|
-
</thead>
|
|
148
|
-
<tbody>
|
|
149
|
-
<tr v-for="row in rowCount" :key="row">
|
|
150
|
-
<td
|
|
151
|
-
v-for="col in columns"
|
|
152
|
-
:key="col.name"
|
|
153
|
-
:class="columnConfig?.[col.name]?.cellClass"
|
|
154
|
-
:style="columnAlignStyle(col.name)"
|
|
155
|
-
>
|
|
156
|
-
{{ cellValue(col, row - 1) }}
|
|
157
|
-
</td>
|
|
158
|
-
</tr>
|
|
159
|
-
</tbody>
|
|
160
|
-
</table>
|
|
161
|
-
</div>
|
|
162
|
-
</div>
|
|
163
|
-
</template>
|
|
164
|
-
|
|
165
|
-
<style scoped>
|
|
166
|
-
.TableOuter {
|
|
167
|
-
position: relative;
|
|
168
|
-
display: inline-block;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
.TableOuter.has-menu {
|
|
172
|
-
padding-top: 32px;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
.TableOuter:hover :deep(.chart-menu-button) {
|
|
176
|
-
opacity: 1;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
.TableWrapper {
|
|
180
|
-
overflow-x: auto;
|
|
181
|
-
font-size: var(--font-size-sm);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
.Table {
|
|
185
|
-
border-collapse: collapse;
|
|
186
|
-
font-variant-numeric: tabular-nums;
|
|
187
|
-
border: 1px solid var(--color-border);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
.Table tr,
|
|
191
|
-
.Table th,
|
|
192
|
-
.Table td {
|
|
193
|
-
background: transparent;
|
|
194
|
-
border: none;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
.Table th,
|
|
198
|
-
.Table td {
|
|
199
|
-
padding: 0.75em 1.25em;
|
|
200
|
-
white-space: nowrap;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
.Table th {
|
|
204
|
-
font-weight: 600;
|
|
205
|
-
border-bottom: 1px solid var(--color-border-header);
|
|
206
|
-
position: sticky;
|
|
207
|
-
top: 0;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
.Table tbody td {
|
|
211
|
-
border-bottom: 1px solid var(--color-border);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
.Table tbody tr:last-child td {
|
|
215
|
-
border-bottom: none;
|
|
216
|
-
}
|
|
217
|
-
</style>
|
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
# LineChart
|
|
2
|
-
|
|
3
|
-
A responsive SVG line chart with support for multiple series, axis labels, and custom styling.
|
|
4
|
-
|
|
5
|
-
## Examples
|
|
6
|
-
|
|
7
|
-
### Single series
|
|
8
|
-
|
|
9
|
-
<ComponentDemo>
|
|
10
|
-
<LineChart :data="[0, 4, 8, 15, 22, 30, 28, 20, 12, 5, 2]" :height="200" x-label="Days" y-label="Cases" />
|
|
11
|
-
|
|
12
|
-
<template #code>
|
|
13
|
-
|
|
14
|
-
```vue
|
|
15
|
-
<LineChart
|
|
16
|
-
:data="[0, 4, 8, 15, 22, 30, 28, 20, 12, 5, 2]"
|
|
17
|
-
:height="200"
|
|
18
|
-
x-label="Days"
|
|
19
|
-
y-label="Cases"
|
|
20
|
-
/>
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
</template>
|
|
24
|
-
</ComponentDemo>
|
|
25
|
-
|
|
26
|
-
### Multiple series
|
|
27
|
-
|
|
28
|
-
<ComponentDemo>
|
|
29
|
-
<LineChart
|
|
30
|
-
:series="[
|
|
31
|
-
{ data: [0, 10, 25, 45, 60, 55, 40, 20, 8], color: '#fb7e38', strokeWidth: 3 },
|
|
32
|
-
{ data: [0, 5, 12, 20, 28, 25, 18, 10, 4], color: '#0057b7', strokeWidth: 3 },
|
|
33
|
-
]"
|
|
34
|
-
:height="200"
|
|
35
|
-
x-label="Weeks"
|
|
36
|
-
y-label="Incidence"
|
|
37
|
-
/>
|
|
38
|
-
|
|
39
|
-
<template #code>
|
|
40
|
-
|
|
41
|
-
```vue
|
|
42
|
-
<LineChart
|
|
43
|
-
:series="[
|
|
44
|
-
{
|
|
45
|
-
data: [0, 10, 25, 45, 60, 55, 40, 20, 8],
|
|
46
|
-
color: '#fb7e38',
|
|
47
|
-
strokeWidth: 3,
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
data: [0, 5, 12, 20, 28, 25, 18, 10, 4],
|
|
51
|
-
color: '#0057b7',
|
|
52
|
-
strokeWidth: 3,
|
|
53
|
-
},
|
|
54
|
-
]"
|
|
55
|
-
:height="200"
|
|
56
|
-
x-label="Weeks"
|
|
57
|
-
y-label="Incidence"
|
|
58
|
-
/>
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
</template>
|
|
62
|
-
</ComponentDemo>
|
|
63
|
-
|
|
64
|
-
### Dashed baseline
|
|
65
|
-
|
|
66
|
-
<ComponentDemo>
|
|
67
|
-
<LineChart
|
|
68
|
-
:series="[
|
|
69
|
-
{ data: [0, 10, 25, 45, 60, 55, 40, 20, 8], color: '#999', dashed: true, strokeWidth: 2 },
|
|
70
|
-
{ data: [0, 5, 12, 20, 28, 25, 18, 10, 4], color: '#2563eb', strokeWidth: 2 },
|
|
71
|
-
]"
|
|
72
|
-
:height="200"
|
|
73
|
-
/>
|
|
74
|
-
|
|
75
|
-
<template #code>
|
|
76
|
-
|
|
77
|
-
```vue
|
|
78
|
-
<LineChart
|
|
79
|
-
:series="[
|
|
80
|
-
{
|
|
81
|
-
data: [0, 10, 25, 45, 60, 55, 40, 20, 8],
|
|
82
|
-
color: '#999',
|
|
83
|
-
dashed: true,
|
|
84
|
-
strokeWidth: 2,
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
data: [0, 5, 12, 20, 28, 25, 18, 10, 4],
|
|
88
|
-
color: '#2563eb',
|
|
89
|
-
strokeWidth: 2,
|
|
90
|
-
},
|
|
91
|
-
]"
|
|
92
|
-
:height="200"
|
|
93
|
-
/>
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
</template>
|
|
97
|
-
</ComponentDemo>
|
|
98
|
-
|
|
99
|
-
### Many trajectories with low opacity
|
|
100
|
-
|
|
101
|
-
<ComponentDemo>
|
|
102
|
-
<LineChart
|
|
103
|
-
:series="Array.from({ length: 20 }, (_, i) => ({
|
|
104
|
-
data: Array.from({ length: 50 }, (_, t) => Math.max(0, 30 * Math.sin(t / 8) + (Math.random() - 0.5) * 15 + i * 0.5)),
|
|
105
|
-
color: '#0057b7',
|
|
106
|
-
}))"
|
|
107
|
-
:height="250"
|
|
108
|
-
:line-opacity="0.15"
|
|
109
|
-
x-label="Days"
|
|
110
|
-
y-label="Incidence"
|
|
111
|
-
/>
|
|
112
|
-
|
|
113
|
-
<template #code>
|
|
114
|
-
|
|
115
|
-
```vue
|
|
116
|
-
<LineChart
|
|
117
|
-
:series="trajectories"
|
|
118
|
-
:height="250"
|
|
119
|
-
:line-opacity="0.15"
|
|
120
|
-
x-label="Days"
|
|
121
|
-
y-label="Incidence"
|
|
122
|
-
/>
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
</template>
|
|
126
|
-
</ComponentDemo>
|
|
127
|
-
|
|
128
|
-
### Grid lines
|
|
129
|
-
|
|
130
|
-
<ComponentDemo>
|
|
131
|
-
<LineChart
|
|
132
|
-
:series="[
|
|
133
|
-
{ data: [0, 10, 25, 45, 60, 55, 40, 20, 8], color: '#fb7e38', strokeWidth: 3 },
|
|
134
|
-
{ data: [0, 5, 12, 20, 28, 25, 18, 10, 4], color: '#0057b7', strokeWidth: 3 },
|
|
135
|
-
]"
|
|
136
|
-
:height="200"
|
|
137
|
-
x-label="Weeks"
|
|
138
|
-
y-label="Incidence"
|
|
139
|
-
x-grid
|
|
140
|
-
y-grid
|
|
141
|
-
/>
|
|
142
|
-
|
|
143
|
-
<template #code>
|
|
144
|
-
|
|
145
|
-
```vue
|
|
146
|
-
<LineChart
|
|
147
|
-
:series="[
|
|
148
|
-
{
|
|
149
|
-
data: [0, 10, 25, 45, 60, 55, 40, 20, 8],
|
|
150
|
-
color: '#fb7e38',
|
|
151
|
-
strokeWidth: 3,
|
|
152
|
-
},
|
|
153
|
-
{
|
|
154
|
-
data: [0, 5, 12, 20, 28, 25, 18, 10, 4],
|
|
155
|
-
color: '#0057b7',
|
|
156
|
-
strokeWidth: 3,
|
|
157
|
-
},
|
|
158
|
-
]"
|
|
159
|
-
:height="200"
|
|
160
|
-
x-label="Weeks"
|
|
161
|
-
y-label="Incidence"
|
|
162
|
-
x-grid
|
|
163
|
-
y-grid
|
|
164
|
-
/>
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
</template>
|
|
168
|
-
</ComponentDemo>
|
|
169
|
-
|
|
170
|
-
<!--@include: ./_api/line-chart.md-->
|
|
171
|
-
|
|
172
|
-
### Series
|
|
173
|
-
|
|
174
|
-
```ts
|
|
175
|
-
interface Series {
|
|
176
|
-
data: number[];
|
|
177
|
-
color?: string;
|
|
178
|
-
dashed?: boolean;
|
|
179
|
-
strokeWidth?: number;
|
|
180
|
-
opacity?: number;
|
|
181
|
-
}
|
|
182
|
-
```
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { test, expect } from "@playwright/test";
|
|
2
|
-
|
|
3
|
-
test("LineChart page renders demos", async ({ page }) => {
|
|
4
|
-
await page.goto("./cfasim-ui/charts/line-chart");
|
|
5
|
-
await expect(page.locator("h1")).toBeVisible();
|
|
6
|
-
const demos = page.locator(".demo-preview");
|
|
7
|
-
await expect(demos.first()).toBeVisible();
|
|
8
|
-
const chartSvg = demos.first().locator("svg").last();
|
|
9
|
-
await expect(chartSvg).toBeVisible();
|
|
10
|
-
await expect(chartSvg.locator("path")).toBeAttached();
|
|
11
|
-
});
|