Skip to content

Commit b180fd6

Browse files
committed
add export to png function
1 parent cbc181d commit b180fd6

File tree

6 files changed

+136
-7
lines changed

6 files changed

+136
-7
lines changed

config/vite.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@ export const baseConfig = {
8181
'dayjs/plugin/timezone',
8282
'dayjs/plugin/utc',
8383
'markdown-it > argparse',
84-
'markdown-it > entities'
84+
'markdown-it > entities',
85+
'file-saver'
8586
],
8687
exclude: [
8788
'markdown-it'

histoire.config.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,6 @@ export default defineConfig({
2626
define: {
2727
__DOCS_HOST__: JSON.stringify(getDocsHost())
2828
},
29-
optimizeDeps: { noDiscovery: true } // vite 6 compat
29+
optimizeDeps: { ...baseConfig.optimizeDeps, noDiscovery: true } // vite 6 compat,
3030
}
3131
})

lib/support/Chart.ts

+48-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
* https://github.com/radix-ui/colors/blob/main/LICENSE
77
*/
88

9+
import FileSaver from 'file-saver'
10+
import html2canvas from 'html2canvas'
11+
912
const isLightDarkSupported
1013
= typeof CSS !== 'undefined' && CSS.supports('color', 'light-dark(#000, #fff)')
1114

@@ -50,7 +53,7 @@ export function scheme<T extends { key: string; color?: ChartColor }>(
5053
data: T[],
5154
colors: ChartColor[] = Object.keys(rest),
5255
unknown: ChartColor = 'gray'
53-
) {
56+
): (d: T) => string {
5457
const map = new Map<string, string>()
5558

5659
for (let i = 0; i < data.length; i++) {
@@ -63,3 +66,47 @@ export function scheme<T extends { key: string; color?: ChartColor }>(
6366
return map.get(d.key) ?? unknown
6467
}
6568
}
69+
70+
export async function exportAsPng(_el: any, fileName = 'chart.png', delay = 0): Promise<void> {
71+
if (!_el) { return }
72+
await new Promise((resolve) => setTimeout(resolve, delay))
73+
74+
const el = '$el' in _el ? _el.$el : _el
75+
if (!(el instanceof HTMLElement)) { return }
76+
77+
const SCard = el.closest('.SCard')
78+
if (!(SCard instanceof HTMLElement)) { return }
79+
80+
const canvas = await html2canvas(SCard, {
81+
scale: 2,
82+
backgroundColor: getBackgroundColor(el),
83+
logging: false,
84+
ignoreElements: (el) => el.classList.contains('SControlActionBar')
85+
})
86+
87+
const dataUrl = canvas.toDataURL('image/png')
88+
FileSaver.saveAs(dataUrl, fileName)
89+
}
90+
91+
function getBackgroundColor(el: HTMLElement | null): string {
92+
const defaultColor = 'rgba(0, 0, 0, 0)'
93+
let color = 'rgba(0, 0, 0, 0)'
94+
95+
while (el) {
96+
const bgColor = window.getComputedStyle(el).backgroundColor
97+
if (bgColor !== defaultColor) {
98+
color = bgColor
99+
break
100+
}
101+
102+
el = el.parentElement
103+
}
104+
105+
if (color === defaultColor) {
106+
return document.documentElement.classList.contains('dark')
107+
? 'rgba(0, 0, 0, 1)'
108+
: 'rgba(255, 255, 255, 1)'
109+
}
110+
111+
return color
112+
}

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
"@types/qs": "^6.9.18",
7777
"d3": "^7.9.0",
7878
"file-saver": "^2.0.5",
79+
"html2canvas": "^1.4.1",
7980
"magic-string": "^0.30.17",
8081
"ofetch": "^1.4.1",
8182
"qs": "^6.14.0",

pnpm-lock.yaml

+39
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

stories/components/SChart.01_Playground.story.vue

+45-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
<script setup lang="ts">
2+
import IconDownload from '~icons/ph/download-simple-bold'
23
import SCard from 'sefirot/components/SCard.vue'
34
import SCardBlock from 'sefirot/components/SCardBlock.vue'
4-
import SChartBar, { type KV } from 'sefirot/components/SChartBar.vue'
5+
import SChartBar from 'sefirot/components/SChartBar.vue'
56
import SChartPie from 'sefirot/components/SChartPie.vue'
6-
import { ref } from 'vue'
7+
import SControl from 'sefirot/components/SControl.vue'
8+
import SControlActionBar from 'sefirot/components/SControlActionBar.vue'
9+
import SControlActionBarButton from 'sefirot/components/SControlActionBarButton.vue'
10+
import SControlLeft from 'sefirot/components/SControlLeft.vue'
11+
import SControlRight from 'sefirot/components/SControlRight.vue'
12+
import SControlText from 'sefirot/components/SControlText.vue'
13+
import { type KV, exportAsPng } from 'sefirot/support/Chart'
14+
import { ref, useTemplateRef } from 'vue'
715
816
const title = 'Components / SChart / 01. Playground'
917
@@ -18,6 +26,9 @@ const data = ref<KV[]>([
1826
{ key: '2025', value: 45 }
1927
])
2028
29+
const barRef = useTemplateRef('bar-chart')
30+
const pieRef = useTemplateRef('pie-chart')
31+
2132
function tooltipFormat(d: KV) {
2233
return `${d.key} &ndash; <span class="tooltip-number">${d.value}</span>`
2334
}
@@ -203,11 +214,26 @@ function state() {
203214
<Board :title="title">
204215
<SCard class="chart-card">
205216
<SCardBlock size="medium" class="s-px-24" bg="elv-2">
206-
<div class="s-font-14 s-font-w-500">Bar Chart</div>
217+
<SControl>
218+
<SControlLeft>
219+
<SControlText class="s-font-w-500">
220+
Bar Chart
221+
</SControlText>
222+
</SControlLeft>
223+
<SControlRight>
224+
<SControlActionBar>
225+
<SControlActionBarButton
226+
:icon="IconDownload"
227+
@click="exportAsPng(barRef, 'bar-chart.png')"
228+
/>
229+
</SControlActionBar>
230+
</SControlRight>
231+
</SControl>
207232
</SCardBlock>
208233
<SCardBlock bg="elv-2">
209234
<div class="s-w-full s-h-320">
210235
<SChartBar
236+
ref="bar-chart"
211237
:data
212238
:margins="{
213239
top: state.barMarginTop,
@@ -232,11 +258,26 @@ function state() {
232258
<br>
233259
<SCard class="chart-card">
234260
<SCardBlock size="medium" class="s-px-24" bg="elv-2">
235-
<div class="s-font-14 s-font-w-500">Pie Chart</div>
261+
<SControl>
262+
<SControlLeft>
263+
<SControlText class="s-font-w-500">
264+
Pie Chart
265+
</SControlText>
266+
</SControlLeft>
267+
<SControlRight>
268+
<SControlActionBar>
269+
<SControlActionBarButton
270+
:icon="IconDownload"
271+
@click="exportAsPng(pieRef, 'pie-chart.png')"
272+
/>
273+
</SControlActionBar>
274+
</SControlRight>
275+
</SControl>
236276
</SCardBlock>
237277
<SCardBlock bg="elv-2">
238278
<div class="s-w-full s-h-320 s-pb-24">
239279
<SChartPie
280+
ref="pie-chart"
240281
:data
241282
:margins="{
242283
top: state.pieMarginTop,

0 commit comments

Comments
 (0)