@cfasim-ui/docs 0.4.15 → 0.5.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/charts/BarChart/BarChart.md +134 -0
- package/charts/BarChart/BarChart.vue +512 -9
- package/charts/DataTable/DataTable.md +168 -3
- package/charts/DataTable/DataTable.vue +133 -11
- package/charts/LineChart/LineChart.md +5 -1
- package/charts/LineChart/LineChart.vue +35 -0
- package/charts/_shared/chartProps.ts +7 -0
- package/charts/_shared/contrast.ts +100 -0
- package/charts/_shared/index.ts +7 -0
- package/charts/_shared/useChartFoundation.ts +6 -0
- package/charts/_shared/useChartMenu.ts +18 -1
- package/charts/env.d.ts +4 -0
- package/components/Icon/Icon.md +122 -1
- package/components/Icon/Icon.vue +43 -51
- package/components/Icon/defaultIcons.ts +92 -0
- package/components/Icon/github.svg +1 -0
- package/components/Icon/registry.ts +68 -0
- package/components/ParamEditor/ParamEditorImpl.vue +8 -5
- package/components/index.ts +3 -0
- package/components/svg.d.ts +5 -0
- package/index.json +1 -1
- package/package.json +1 -1
|
@@ -124,6 +124,56 @@ e.g. `"percent:1"`), a printf-style format string, or a function
|
|
|
124
124
|
</template>
|
|
125
125
|
</ComponentDemo>
|
|
126
126
|
|
|
127
|
+
### Wrapping long content
|
|
128
|
+
|
|
129
|
+
By default, cell content stays on one line and truncates with an ellipsis
|
|
130
|
+
when it's wider than the column — so a long string in one column can't
|
|
131
|
+
spill into the next. Set `wrap: true` on a column to let it grow
|
|
132
|
+
vertically instead.
|
|
133
|
+
|
|
134
|
+
<ComponentDemo>
|
|
135
|
+
<DataTable
|
|
136
|
+
:data="{
|
|
137
|
+
drug: ['Compound A', 'Compound B', 'Compound C'],
|
|
138
|
+
note: [
|
|
139
|
+
'Well tolerated at all tested doses; no adverse events reported.',
|
|
140
|
+
'Mild GI symptoms in 4% of participants; resolved without intervention.',
|
|
141
|
+
'Discontinued at week 6 after liver-enzyme elevation in two participants.',
|
|
142
|
+
],
|
|
143
|
+
}"
|
|
144
|
+
:column-config="{
|
|
145
|
+
drug: { width: 'small' },
|
|
146
|
+
note: { wrap: true, width: 'large' },
|
|
147
|
+
}"
|
|
148
|
+
/>
|
|
149
|
+
|
|
150
|
+
<template #code>
|
|
151
|
+
|
|
152
|
+
```vue
|
|
153
|
+
<DataTable
|
|
154
|
+
:data="{
|
|
155
|
+
drug: ['Compound A', 'Compound B', 'Compound C'],
|
|
156
|
+
note: [
|
|
157
|
+
'Well tolerated at all tested doses; no adverse events reported.',
|
|
158
|
+
'Mild GI symptoms in 4% of participants; resolved without intervention.',
|
|
159
|
+
'Discontinued at week 6 after liver-enzyme elevation in two participants.',
|
|
160
|
+
],
|
|
161
|
+
}"
|
|
162
|
+
:column-config="{
|
|
163
|
+
drug: { width: 'small' },
|
|
164
|
+
note: { wrap: true, width: 'large' },
|
|
165
|
+
}"
|
|
166
|
+
/>
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
</template>
|
|
170
|
+
</ComponentDemo>
|
|
171
|
+
|
|
172
|
+
Use `columnClass` to attach a class to both the header and every body
|
|
173
|
+
cell in a column — handy when whole-column styling (background, border,
|
|
174
|
+
font weight) should match across the header and data. `cellClass` keeps
|
|
175
|
+
its meaning (body cells only).
|
|
176
|
+
|
|
127
177
|
### Full width
|
|
128
178
|
|
|
129
179
|
By default the table sizes to its content (columns default to a fixed
|
|
@@ -157,9 +207,8 @@ the available space equally.
|
|
|
157
207
|
|
|
158
208
|
A `⋯` menu appears in the top-right corner of every table with a
|
|
159
209
|
**Download** item that exports the data as CSV. Use `download-menu-link`
|
|
160
|
-
to customize the menu item label
|
|
161
|
-
filename
|
|
162
|
-
to hide the menu entirely.
|
|
210
|
+
to customize the menu item label and `filename` to control the
|
|
211
|
+
downloaded filename. Pass `:menu="false"` to hide the menu entirely.
|
|
163
212
|
|
|
164
213
|
<ComponentDemo>
|
|
165
214
|
<DataTable
|
|
@@ -184,6 +233,115 @@ to hide the menu entirely.
|
|
|
184
233
|
</template>
|
|
185
234
|
</ComponentDemo>
|
|
186
235
|
|
|
236
|
+
### Custom CSV download
|
|
237
|
+
|
|
238
|
+
By default, the Download menu item exports the displayed table as CSV.
|
|
239
|
+
Use the `csv` prop to supply your own content — for example, to include
|
|
240
|
+
ISO dates, extra columns that aren't in the table, or values formatted
|
|
241
|
+
differently from the on-screen rendering. Accepts a raw string or a
|
|
242
|
+
function returning one (called lazily on click).
|
|
243
|
+
|
|
244
|
+
<ComponentDemo>
|
|
245
|
+
<DataTable
|
|
246
|
+
:data="{ day: [0, 1, 2, 3, 4], cases: [1, 21, 56, 101, 141] }"
|
|
247
|
+
filename="sir-cases"
|
|
248
|
+
:csv="'date,day,cases\n2024-01-01,0,1\n2024-01-02,1,21\n2024-01-03,2,56\n2024-01-04,3,101\n2024-01-05,4,141'"
|
|
249
|
+
/>
|
|
250
|
+
|
|
251
|
+
<template #code>
|
|
252
|
+
|
|
253
|
+
```vue
|
|
254
|
+
<DataTable
|
|
255
|
+
:data="{
|
|
256
|
+
day: [0, 1, 2, 3, 4],
|
|
257
|
+
cases: [1, 21, 56, 101, 141],
|
|
258
|
+
}"
|
|
259
|
+
filename="sir-cases"
|
|
260
|
+
:csv="`date,day,cases
|
|
261
|
+
2024-01-01,0,1
|
|
262
|
+
2024-01-02,1,21
|
|
263
|
+
2024-01-03,2,56
|
|
264
|
+
2024-01-04,3,101
|
|
265
|
+
2024-01-05,4,141`"
|
|
266
|
+
/>
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
</template>
|
|
270
|
+
</ComponentDemo>
|
|
271
|
+
|
|
272
|
+
### Download button
|
|
273
|
+
|
|
274
|
+
Pass `download-button` to render a visible, labeled button beneath the
|
|
275
|
+
table instead of exposing the download only via the top-right menu. The
|
|
276
|
+
button uses `download-menu-link` as its label, and the menu's Download
|
|
277
|
+
item is suppressed so the action isn't duplicated. The button has the
|
|
278
|
+
class `data-table-download-button` and its styles are unscoped, so it
|
|
279
|
+
can be targeted directly from custom CSS without specificity battles.
|
|
280
|
+
|
|
281
|
+
<ComponentDemo>
|
|
282
|
+
<DataTable
|
|
283
|
+
:data="{ day: [0, 1, 2, 3, 4], cases: [1, 21, 56, 101, 141] }"
|
|
284
|
+
filename="sir-cases"
|
|
285
|
+
download-menu-link="Download CSV"
|
|
286
|
+
download-button
|
|
287
|
+
:csv="'date,day,cases\n2024-01-01,0,1\n2024-01-02,1,21\n2024-01-03,2,56\n2024-01-04,3,101\n2024-01-05,4,141'"
|
|
288
|
+
/>
|
|
289
|
+
|
|
290
|
+
<template #code>
|
|
291
|
+
|
|
292
|
+
```vue
|
|
293
|
+
<DataTable
|
|
294
|
+
:data="{
|
|
295
|
+
day: [0, 1, 2, 3, 4],
|
|
296
|
+
cases: [1, 21, 56, 101, 141],
|
|
297
|
+
}"
|
|
298
|
+
filename="sir-cases"
|
|
299
|
+
download-menu-link="Download CSV"
|
|
300
|
+
download-button
|
|
301
|
+
:csv="`date,day,cases
|
|
302
|
+
2024-01-01,0,1
|
|
303
|
+
2024-01-02,1,21
|
|
304
|
+
2024-01-03,2,56
|
|
305
|
+
2024-01-04,3,101
|
|
306
|
+
2024-01-05,4,141`"
|
|
307
|
+
/>
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
</template>
|
|
311
|
+
</ComponentDemo>
|
|
312
|
+
|
|
313
|
+
### Download link
|
|
314
|
+
|
|
315
|
+
Pass `download-link` to render a plain text link beneath the table
|
|
316
|
+
instead of (or alongside) the menu. It's a real `<a href download>` —
|
|
317
|
+
right-click → Save As works. Pass `true` for the default "Download data
|
|
318
|
+
(CSV)" label, or a string to customize. The menu's Download item is
|
|
319
|
+
suppressed when this is set. If both `download-link` and
|
|
320
|
+
`download-button` are set, the button wins.
|
|
321
|
+
|
|
322
|
+
<ComponentDemo>
|
|
323
|
+
<DataTable
|
|
324
|
+
:data="{ day: [0, 1, 2, 3, 4], cases: [1, 21, 56, 101, 141] }"
|
|
325
|
+
filename="sir-cases"
|
|
326
|
+
download-link
|
|
327
|
+
/>
|
|
328
|
+
|
|
329
|
+
<template #code>
|
|
330
|
+
|
|
331
|
+
```vue
|
|
332
|
+
<DataTable
|
|
333
|
+
:data="{
|
|
334
|
+
day: [0, 1, 2, 3, 4],
|
|
335
|
+
cases: [1, 21, 56, 101, 141],
|
|
336
|
+
}"
|
|
337
|
+
filename="sir-cases"
|
|
338
|
+
download-link
|
|
339
|
+
/>
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
</template>
|
|
343
|
+
</ComponentDemo>
|
|
344
|
+
|
|
187
345
|
## Props
|
|
188
346
|
|
|
189
347
|
| Prop | Type | Required | Default |
|
|
@@ -195,6 +353,8 @@ to hide the menu entirely.
|
|
|
195
353
|
| `csv` | `string \| (() => string)` | No | — |
|
|
196
354
|
| `filename` | `string` | No | — |
|
|
197
355
|
| `downloadMenuLink` | `string` | No | `"Download"` |
|
|
356
|
+
| `downloadButton` | `boolean` | No | `false` |
|
|
357
|
+
| `downloadLink` | `boolean \| string` | No | — |
|
|
198
358
|
| `fullWidth` | `boolean` | No | `false` |
|
|
199
359
|
|
|
200
360
|
|
|
@@ -205,7 +365,12 @@ interface ColumnConfig {
|
|
|
205
365
|
label?: string;
|
|
206
366
|
width?: "small" | "medium" | "large" | number;
|
|
207
367
|
align?: "left" | "center" | "right";
|
|
368
|
+
/** Class applied to body `<td>` cells only. */
|
|
208
369
|
cellClass?: string;
|
|
370
|
+
/** Class applied to both the header `<th>` and body `<td>` cells. */
|
|
371
|
+
columnClass?: string;
|
|
372
|
+
/** Allow cell contents to wrap. Default `false` (nowrap + ellipsis). */
|
|
373
|
+
wrap?: boolean;
|
|
209
374
|
format?: NumberFormat | ((value: CellValue, row: number) => string);
|
|
210
375
|
}
|
|
211
376
|
|
|
@@ -21,7 +21,22 @@ export interface ColumnConfig {
|
|
|
21
21
|
label?: string;
|
|
22
22
|
width?: ColumnWidth | number;
|
|
23
23
|
align?: ColumnAlign;
|
|
24
|
+
/** Class applied to every body `<td>` in this column. */
|
|
24
25
|
cellClass?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Class applied to both the header `<th>` and every body `<td>` in
|
|
28
|
+
* this column. Use for styling that should span the whole column
|
|
29
|
+
* (borders, backgrounds, fonts). For body-only styling, use
|
|
30
|
+
* `cellClass`.
|
|
31
|
+
*/
|
|
32
|
+
columnClass?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Allow cell contents to wrap to multiple lines when wider than the
|
|
35
|
+
* column. Default `false` — cells stay on one line and truncate with
|
|
36
|
+
* an ellipsis. Set to `true` for text-heavy columns where the full
|
|
37
|
+
* value should remain visible.
|
|
38
|
+
*/
|
|
39
|
+
wrap?: boolean;
|
|
25
40
|
/**
|
|
26
41
|
* Custom formatter for cell values in this column. Accepts a
|
|
27
42
|
* {@link NumberFormat} (preset name, printf-style string, or
|
|
@@ -54,14 +69,34 @@ const props = withDefaults(
|
|
|
54
69
|
/** Filename (without extension) for downloaded CSV files. */
|
|
55
70
|
filename?: string;
|
|
56
71
|
/**
|
|
57
|
-
* Label for the Download item in the table's top-right menu
|
|
58
|
-
* Defaults to
|
|
72
|
+
* Label for the Download item in the table's top-right menu, and for
|
|
73
|
+
* the button rendered when `downloadButton` is true. Defaults to
|
|
74
|
+
* "Download".
|
|
59
75
|
*/
|
|
60
76
|
downloadMenuLink?: string;
|
|
77
|
+
/**
|
|
78
|
+
* Render a visible "Download" button beneath the table instead of
|
|
79
|
+
* exposing the action only via the top-right menu. When enabled, the
|
|
80
|
+
* menu's Download item is suppressed to avoid duplicate controls.
|
|
81
|
+
*/
|
|
82
|
+
downloadButton?: boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Render a plain text link beneath the table for downloading CSV.
|
|
85
|
+
* Pass `true` for the default "Download data (CSV)" label, or a
|
|
86
|
+
* string to customize. When set, the menu's Download item is
|
|
87
|
+
* suppressed. Mutually exclusive with `downloadButton`; if both are
|
|
88
|
+
* set, `downloadButton` wins.
|
|
89
|
+
*/
|
|
90
|
+
downloadLink?: boolean | string;
|
|
61
91
|
/** Stretch the table to fill its container's width. */
|
|
62
92
|
fullWidth?: boolean;
|
|
63
93
|
}>(),
|
|
64
|
-
{
|
|
94
|
+
{
|
|
95
|
+
menu: true,
|
|
96
|
+
fullWidth: false,
|
|
97
|
+
downloadMenuLink: "Download",
|
|
98
|
+
downloadButton: false,
|
|
99
|
+
},
|
|
65
100
|
);
|
|
66
101
|
|
|
67
102
|
function columnLabel(name: string): string {
|
|
@@ -84,6 +119,20 @@ function columnAlignStyle(name: string): CSSProperties | undefined {
|
|
|
84
119
|
return { textAlign: align };
|
|
85
120
|
}
|
|
86
121
|
|
|
122
|
+
/** Classes shared by `<th>` and `<td>` for a column: `columnClass` + wrap flag. */
|
|
123
|
+
function columnSharedClass(name: string): (string | undefined)[] {
|
|
124
|
+
const cfg = props.columnConfig?.[name];
|
|
125
|
+
return [cfg?.columnClass, cfg?.wrap ? "cell-wrap" : undefined];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function headerCellClass(name: string): (string | undefined)[] {
|
|
129
|
+
return columnSharedClass(name);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function bodyCellClass(name: string): (string | undefined)[] {
|
|
133
|
+
return [...columnSharedClass(name), props.columnConfig?.[name]?.cellClass];
|
|
134
|
+
}
|
|
135
|
+
|
|
87
136
|
function isModelOutput(d: TableData): d is ModelOutput {
|
|
88
137
|
return typeof (d as ModelOutput).column === "function";
|
|
89
138
|
}
|
|
@@ -165,14 +214,30 @@ function toCsv(): string {
|
|
|
165
214
|
return lines.join("\n");
|
|
166
215
|
}
|
|
167
216
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
]
|
|
217
|
+
function triggerDownload() {
|
|
218
|
+
downloadCsv(toCsv(), menuFilename());
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const menuItems = computed<ChartMenuItem[]>(() => {
|
|
222
|
+
if (props.downloadButton || props.downloadLink) return [];
|
|
223
|
+
return [{ label: props.downloadMenuLink, action: triggerDownload }];
|
|
224
|
+
});
|
|
174
225
|
|
|
175
|
-
const
|
|
226
|
+
const downloadLinkText = computed<string | null>(() => {
|
|
227
|
+
if (props.downloadButton) return null;
|
|
228
|
+
const v = props.downloadLink;
|
|
229
|
+
if (!v) return null;
|
|
230
|
+
return typeof v === "string" ? v : "Download data (CSV)";
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const csvHref = computed<string | null>(() => {
|
|
234
|
+
if (!downloadLinkText.value) return null;
|
|
235
|
+
return `data:text/csv;charset=utf-8,${encodeURIComponent(toCsv())}`;
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const showMenu = computed(
|
|
239
|
+
() => Boolean(props.menu) && menuItems.value.length > 0,
|
|
240
|
+
);
|
|
176
241
|
</script>
|
|
177
242
|
|
|
178
243
|
<template>
|
|
@@ -195,6 +260,7 @@ const showMenu = computed(() => Boolean(props.menu));
|
|
|
195
260
|
<th
|
|
196
261
|
v-for="col in columns"
|
|
197
262
|
:key="col.name"
|
|
263
|
+
:class="headerCellClass(col.name)"
|
|
198
264
|
:style="columnAlignStyle(col.name)"
|
|
199
265
|
>
|
|
200
266
|
{{ columnLabel(col.name) }}
|
|
@@ -206,7 +272,7 @@ const showMenu = computed(() => Boolean(props.menu));
|
|
|
206
272
|
<td
|
|
207
273
|
v-for="col in columns"
|
|
208
274
|
:key="col.name"
|
|
209
|
-
:class="
|
|
275
|
+
:class="bodyCellClass(col.name)"
|
|
210
276
|
:style="columnAlignStyle(col.name)"
|
|
211
277
|
>
|
|
212
278
|
{{ cellValue(col, row - 1) }}
|
|
@@ -215,6 +281,22 @@ const showMenu = computed(() => Boolean(props.menu));
|
|
|
215
281
|
</tbody>
|
|
216
282
|
</table>
|
|
217
283
|
</div>
|
|
284
|
+
<button
|
|
285
|
+
v-if="downloadButton"
|
|
286
|
+
type="button"
|
|
287
|
+
class="data-table-download-button"
|
|
288
|
+
@click="triggerDownload"
|
|
289
|
+
>
|
|
290
|
+
{{ downloadMenuLink }}
|
|
291
|
+
</button>
|
|
292
|
+
<a
|
|
293
|
+
v-else-if="downloadLinkText"
|
|
294
|
+
class="data-table-download-link"
|
|
295
|
+
:href="csvHref!"
|
|
296
|
+
:download="`${menuFilename()}.csv`"
|
|
297
|
+
>
|
|
298
|
+
{{ downloadLinkText }}
|
|
299
|
+
</a>
|
|
218
300
|
</div>
|
|
219
301
|
</template>
|
|
220
302
|
|
|
@@ -257,9 +339,18 @@ const showMenu = computed(() => Boolean(props.menu));
|
|
|
257
339
|
.Table td {
|
|
258
340
|
padding: 0.75em 1.25em;
|
|
259
341
|
white-space: nowrap;
|
|
342
|
+
overflow: hidden;
|
|
343
|
+
text-overflow: ellipsis;
|
|
260
344
|
text-align: left;
|
|
261
345
|
}
|
|
262
346
|
|
|
347
|
+
.Table th.cell-wrap,
|
|
348
|
+
.Table td.cell-wrap {
|
|
349
|
+
white-space: normal;
|
|
350
|
+
overflow: visible;
|
|
351
|
+
text-overflow: clip;
|
|
352
|
+
}
|
|
353
|
+
|
|
263
354
|
.Table th {
|
|
264
355
|
font-weight: 600;
|
|
265
356
|
border-bottom: 1px solid var(--color-border-header);
|
|
@@ -288,3 +379,34 @@ const showMenu = computed(() => Boolean(props.menu));
|
|
|
288
379
|
padding-right: 2.5em;
|
|
289
380
|
}
|
|
290
381
|
</style>
|
|
382
|
+
|
|
383
|
+
<style>
|
|
384
|
+
.data-table-download-button {
|
|
385
|
+
display: inline-flex;
|
|
386
|
+
align-items: center;
|
|
387
|
+
margin-top: 0.75em;
|
|
388
|
+
padding: 0.5em 1em;
|
|
389
|
+
border: 1px solid var(--color-border);
|
|
390
|
+
border-radius: 0.25em;
|
|
391
|
+
background: var(--color-bg-0, #fff);
|
|
392
|
+
color: var(--color-text);
|
|
393
|
+
font-size: var(--font-size-sm);
|
|
394
|
+
cursor: pointer;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.data-table-download-button:hover {
|
|
398
|
+
background: var(--color-bg-1, rgba(0, 0, 0, 0.05));
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
.data-table-download-button:focus-visible {
|
|
402
|
+
outline: 2px solid var(--color-primary);
|
|
403
|
+
outline-offset: 2px;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
.data-table-download-link {
|
|
407
|
+
display: block;
|
|
408
|
+
text-align: right;
|
|
409
|
+
font-size: var(--font-size-sm);
|
|
410
|
+
margin-top: 0.25em;
|
|
411
|
+
}
|
|
412
|
+
</style>
|
|
@@ -941,7 +941,10 @@ dates, categorical labels, or extra columns that aren't plotted). Use
|
|
|
941
941
|
`filename` to control the download filename (shared by SVG, PNG and CSV).
|
|
942
942
|
|
|
943
943
|
Pass `download-link` to also render a plain text link below the chart — set
|
|
944
|
-
it to `true` for the default label, or pass a string to customize it.
|
|
944
|
+
it to `true` for the default label, or pass a string to customize it. Use
|
|
945
|
+
`download-button` instead to render a styled `<button>` (with the class
|
|
946
|
+
`line-chart-download-button`, available for custom CSS) in place of the
|
|
947
|
+
link.
|
|
945
948
|
|
|
946
949
|
<ComponentDemo>
|
|
947
950
|
<LineChart
|
|
@@ -1004,6 +1007,7 @@ until the user clicks Download:
|
|
|
1004
1007
|
| `csv` | `string \| (() => string)` | No | — |
|
|
1005
1008
|
| `filename` | `string` | No | — |
|
|
1006
1009
|
| `downloadLink` | `boolean \| string` | No | — |
|
|
1010
|
+
| `downloadButton` | `boolean \| string` | No | — |
|
|
1007
1011
|
| `annotations` | `readonly ChartAnnotation[]` | No | — |
|
|
1008
1012
|
| `chartPadding` | `ChartPadding` | No | — |
|
|
1009
1013
|
| `y` | `LineChartData` | No | — |
|
|
@@ -929,6 +929,8 @@ const {
|
|
|
929
929
|
menuItems,
|
|
930
930
|
downloadLinkText,
|
|
931
931
|
csvHref,
|
|
932
|
+
downloadButtonText,
|
|
933
|
+
triggerCsvDownload,
|
|
932
934
|
menuFilename,
|
|
933
935
|
isFullscreen,
|
|
934
936
|
} = useChartFoundation({
|
|
@@ -944,6 +946,7 @@ const {
|
|
|
944
946
|
tooltipClamp: () => props.tooltipClamp,
|
|
945
947
|
filename: () => props.filename,
|
|
946
948
|
downloadLink: () => props.downloadLink,
|
|
949
|
+
downloadButton: () => props.downloadButton,
|
|
947
950
|
chartPadding: () => props.chartPadding,
|
|
948
951
|
inlineLegendLabels: () => inlineLegendLabels.value,
|
|
949
952
|
hasTooltipSlot: () => hasTooltipSlot.value,
|
|
@@ -1403,6 +1406,14 @@ const positionedLegendItems = computed(() => {
|
|
|
1403
1406
|
>
|
|
1404
1407
|
{{ downloadLinkText }}
|
|
1405
1408
|
</a>
|
|
1409
|
+
<button
|
|
1410
|
+
v-if="downloadButtonText"
|
|
1411
|
+
type="button"
|
|
1412
|
+
class="line-chart-download-button"
|
|
1413
|
+
@click="triggerCsvDownload"
|
|
1414
|
+
>
|
|
1415
|
+
{{ downloadButtonText }}
|
|
1416
|
+
</button>
|
|
1406
1417
|
</div>
|
|
1407
1418
|
</template>
|
|
1408
1419
|
|
|
@@ -1438,3 +1449,27 @@ const positionedLegendItems = computed(() => {
|
|
|
1438
1449
|
flex-shrink: 0;
|
|
1439
1450
|
}
|
|
1440
1451
|
</style>
|
|
1452
|
+
|
|
1453
|
+
<style>
|
|
1454
|
+
.line-chart-download-button {
|
|
1455
|
+
display: inline-flex;
|
|
1456
|
+
align-items: center;
|
|
1457
|
+
margin-top: 0.5em;
|
|
1458
|
+
padding: 0.5em 1em;
|
|
1459
|
+
border: 1px solid var(--color-border);
|
|
1460
|
+
border-radius: 0.25em;
|
|
1461
|
+
background: var(--color-bg-0, #fff);
|
|
1462
|
+
color: var(--color-text);
|
|
1463
|
+
font-size: var(--font-size-sm);
|
|
1464
|
+
cursor: pointer;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
.line-chart-download-button:hover {
|
|
1468
|
+
background: var(--color-bg-1, rgba(0, 0, 0, 0.05));
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
.line-chart-download-button:focus-visible {
|
|
1472
|
+
outline: 2px solid var(--color-primary);
|
|
1473
|
+
outline-offset: 2px;
|
|
1474
|
+
}
|
|
1475
|
+
</style>
|
|
@@ -141,6 +141,13 @@ export interface ChartCommonProps {
|
|
|
141
141
|
* for the default label or a string to customize.
|
|
142
142
|
*/
|
|
143
143
|
downloadLink?: boolean | string;
|
|
144
|
+
/**
|
|
145
|
+
* Show a `<button>` below the chart to download CSV. Pass `true` for
|
|
146
|
+
* the default label or a string to customize. When set, the CSV menu
|
|
147
|
+
* item is suppressed. Mutually exclusive with `downloadLink`; if both
|
|
148
|
+
* are set, `downloadButton` wins.
|
|
149
|
+
*/
|
|
150
|
+
downloadButton?: boolean | string;
|
|
144
151
|
/** Annotations rendered as the top layer of the chart. */
|
|
145
152
|
annotations?: readonly ChartAnnotation[];
|
|
146
153
|
/**
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Color helpers for picking a readable text color against a bar fill.
|
|
3
|
+
* The luminance math is pure and unit-testable; resolving CSS values
|
|
4
|
+
* that aren't plain hex/rgb (named colors, `var(--x)`) needs the DOM and
|
|
5
|
+
* is handled separately by {@link resolveColorToRgb}.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type Rgb = [number, number, number];
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Parse a hex (`#rgb`, `#rrggbb`, `#rrggbbaa`) or `rgb()/rgba()` string to
|
|
12
|
+
* `[r, g, b]` in 0..255. Returns null for anything else (named colors,
|
|
13
|
+
* `var(...)`, `hsl(...)`) — resolve those via {@link resolveColorToRgb}.
|
|
14
|
+
*/
|
|
15
|
+
export function parseRgb(color: string): Rgb | null {
|
|
16
|
+
const c = color.trim();
|
|
17
|
+
if (c.startsWith("#")) {
|
|
18
|
+
let hex = c.slice(1);
|
|
19
|
+
if (hex.length === 3 || hex.length === 4) {
|
|
20
|
+
hex = hex
|
|
21
|
+
.slice(0, 3)
|
|
22
|
+
.split("")
|
|
23
|
+
.map((ch) => ch + ch)
|
|
24
|
+
.join("");
|
|
25
|
+
} else if (hex.length === 6 || hex.length === 8) {
|
|
26
|
+
hex = hex.slice(0, 6);
|
|
27
|
+
} else {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const n = Number.parseInt(hex, 16);
|
|
31
|
+
if (Number.isNaN(n)) return null;
|
|
32
|
+
return [(n >> 16) & 0xff, (n >> 8) & 0xff, n & 0xff];
|
|
33
|
+
}
|
|
34
|
+
const m = c.match(/^rgba?\(([^)]+)\)$/i);
|
|
35
|
+
if (m) {
|
|
36
|
+
const parts = m[1].split(/[,/\s]+/).filter(Boolean);
|
|
37
|
+
if (parts.length < 3) return null;
|
|
38
|
+
const rgb = parts.slice(0, 3).map((p) => {
|
|
39
|
+
if (p.endsWith("%")) return Math.round((parseFloat(p) / 100) * 255);
|
|
40
|
+
return Math.round(parseFloat(p));
|
|
41
|
+
});
|
|
42
|
+
if (rgb.some((v) => Number.isNaN(v))) return null;
|
|
43
|
+
return [rgb[0], rgb[1], rgb[2]];
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** WCAG relative luminance (0 = black, 1 = white) for an RGB triple. */
|
|
49
|
+
export function relativeLuminance([r, g, b]: Rgb): number {
|
|
50
|
+
const channel = (v: number) => {
|
|
51
|
+
const s = v / 255;
|
|
52
|
+
return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
|
|
53
|
+
};
|
|
54
|
+
return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let probe: HTMLSpanElement | null = null;
|
|
58
|
+
const resolveCache = new Map<string, Rgb | null>();
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Resolve any CSS color string (including named colors and `var(--x)`) to
|
|
62
|
+
* an RGB triple by letting the browser compute it on a hidden probe
|
|
63
|
+
* element. Cached per input. Returns null in non-DOM environments or when
|
|
64
|
+
* the value can't be resolved. Plain hex/rgb shortcut through
|
|
65
|
+
* {@link parseRgb} without touching the DOM.
|
|
66
|
+
*/
|
|
67
|
+
export function resolveColorToRgb(color: string): Rgb | null {
|
|
68
|
+
const direct = parseRgb(color);
|
|
69
|
+
if (direct) return direct;
|
|
70
|
+
if (resolveCache.has(color)) return resolveCache.get(color) ?? null;
|
|
71
|
+
if (typeof document === "undefined") return null;
|
|
72
|
+
if (!probe) {
|
|
73
|
+
probe = document.createElement("span");
|
|
74
|
+
probe.style.cssText =
|
|
75
|
+
"position:absolute;width:0;height:0;visibility:hidden;pointer-events:none";
|
|
76
|
+
document.body.appendChild(probe);
|
|
77
|
+
}
|
|
78
|
+
probe.style.color = "";
|
|
79
|
+
probe.style.color = color;
|
|
80
|
+
const computed = getComputedStyle(probe).color;
|
|
81
|
+
const rgb = parseRgb(computed);
|
|
82
|
+
resolveCache.set(color, rgb);
|
|
83
|
+
return rgb;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Pick the more readable of `light`/`dark` text colors for a given fill.
|
|
88
|
+
* Uses the WCAG luminance threshold (0.179) that maximizes contrast
|
|
89
|
+
* against black-or-white text. When the fill can't be resolved (e.g. an
|
|
90
|
+
* unresolvable CSS var in a non-DOM context), falls back to `light`.
|
|
91
|
+
*/
|
|
92
|
+
export function pickContrastColor(
|
|
93
|
+
fill: string,
|
|
94
|
+
light = "#ffffff",
|
|
95
|
+
dark = "#1a1a1a",
|
|
96
|
+
): string {
|
|
97
|
+
const rgb = resolveColorToRgb(fill);
|
|
98
|
+
if (!rgb) return light;
|
|
99
|
+
return relativeLuminance(rgb) > 0.179 ? dark : light;
|
|
100
|
+
}
|
package/charts/_shared/index.ts
CHANGED
|
@@ -25,6 +25,7 @@ export interface ChartFoundationOptions {
|
|
|
25
25
|
tooltipClamp: () => TooltipClamp | undefined;
|
|
26
26
|
filename: () => string | undefined;
|
|
27
27
|
downloadLink: () => boolean | string | undefined;
|
|
28
|
+
downloadButton: () => boolean | string | undefined;
|
|
28
29
|
chartPadding: () => ChartPadding | undefined;
|
|
29
30
|
// Chart-specific hooks that the composable can't infer.
|
|
30
31
|
/**
|
|
@@ -61,6 +62,8 @@ export function useChartFoundation(opts: ChartFoundationOptions) {
|
|
|
61
62
|
items: menuItems,
|
|
62
63
|
downloadLinkText,
|
|
63
64
|
csvHref,
|
|
65
|
+
downloadButtonText,
|
|
66
|
+
triggerCsvDownload,
|
|
64
67
|
resolvedFilename: menuFilename,
|
|
65
68
|
isFullscreen,
|
|
66
69
|
} = useChartMenu({
|
|
@@ -68,6 +71,7 @@ export function useChartFoundation(opts: ChartFoundationOptions) {
|
|
|
68
71
|
legacyMenuLabel: opts.menu,
|
|
69
72
|
getCsv: opts.getCsv,
|
|
70
73
|
downloadLink: opts.downloadLink,
|
|
74
|
+
downloadButton: opts.downloadButton,
|
|
71
75
|
fullscreen: true,
|
|
72
76
|
});
|
|
73
77
|
|
|
@@ -130,6 +134,8 @@ export function useChartFoundation(opts: ChartFoundationOptions) {
|
|
|
130
134
|
menuItems,
|
|
131
135
|
downloadLinkText,
|
|
132
136
|
csvHref,
|
|
137
|
+
downloadButtonText,
|
|
138
|
+
triggerCsvDownload,
|
|
133
139
|
menuFilename,
|
|
134
140
|
isFullscreen,
|
|
135
141
|
measuredHeight,
|