feat: migrate to apache echarts (#4)
Co-authored-by: terraputix <terraputix@mailbox.org> Reviewed-on: useweb/ombrella#4
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
<!--
|
||||
ChartToolbar.svelte — Chart action bar with download and display controls
|
||||
|
||||
Provides a toolbar row with:
|
||||
- Download full meteogram as PNG button
|
||||
- Download full meteogram as SVG button
|
||||
- Slot for additional custom controls (e.g. legend toggle)
|
||||
|
||||
When multiple charts are provided, they are stitched into a single
|
||||
combined image on download.
|
||||
|
||||
Usage:
|
||||
<ChartToolbar
|
||||
charts={chartInstances}
|
||||
fileName="model-comparison"
|
||||
>
|
||||
{#snippet controls()}
|
||||
<Switch bind:checked={showLegend} />
|
||||
{/snippet}
|
||||
</ChartToolbar>
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { downloadMeteogram } from '$lib/utils/echarts/download';
|
||||
|
||||
import type { ExportFormat } from '$lib/utils/echarts/download';
|
||||
import type * as echarts from 'echarts';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
// ─── Props ──────────────────────────────────────────────────────────────────
|
||||
|
||||
interface Props {
|
||||
/** Array of ECharts instances available for download */
|
||||
charts?: echarts.ECharts[];
|
||||
/** Base file name for downloaded images (without extension) */
|
||||
fileName?: string;
|
||||
/** Pixel ratio for PNG exports (default: 2) */
|
||||
pixelRatio?: number;
|
||||
/** Optional CSS class for the outer container */
|
||||
class?: string;
|
||||
/** Slot for additional controls (switches, checkboxes, etc.) */
|
||||
controls?: Snippet;
|
||||
}
|
||||
|
||||
let {
|
||||
charts = [],
|
||||
fileName = 'open-meteo-chart',
|
||||
pixelRatio = 2,
|
||||
class: className = '',
|
||||
controls
|
||||
}: Props = $props();
|
||||
|
||||
// ─── State ──────────────────────────────────────────────────────────────────
|
||||
|
||||
let downloadingFormat: ExportFormat | null = $state(null);
|
||||
|
||||
// ─── Computed ───────────────────────────────────────────────────────────────
|
||||
|
||||
let hasCharts = $derived(charts.length > 0);
|
||||
|
||||
// ─── Handlers ───────────────────────────────────────────────────────────────
|
||||
|
||||
async function handleDownload(format: ExportFormat): Promise<void> {
|
||||
if (!hasCharts || downloadingFormat) return;
|
||||
|
||||
downloadingFormat = format;
|
||||
|
||||
try {
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
downloadMeteogram(charts, { fileName, format, pixelRatio });
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
downloadingFormat = null;
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="chart-toolbar flex flex-col items-center gap-4 md:flex-row md:justify-between {className}"
|
||||
>
|
||||
<!-- Left side: Custom controls slot -->
|
||||
<div class="flex flex-wrap items-center gap-4 md:gap-6">
|
||||
{#if controls}
|
||||
{@render controls()}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Right side: Download buttons -->
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<!-- Download as PNG -->
|
||||
<button
|
||||
type="button"
|
||||
class="toolbar-btn"
|
||||
disabled={!hasCharts || downloadingFormat !== null}
|
||||
onclick={() => handleDownload('png')}
|
||||
title="Download meteogram as PNG image"
|
||||
>
|
||||
{#if downloadingFormat === 'png'}
|
||||
<svg
|
||||
class="h-4 w-4 animate-spin"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
||||
</svg>
|
||||
{:else}
|
||||
<svg
|
||||
class="h-4 w-4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
||||
<polyline points="7 10 12 15 17 10" />
|
||||
<line x1="12" y1="15" x2="12" y2="3" />
|
||||
</svg>
|
||||
{/if}
|
||||
<span>PNG</span>
|
||||
</button>
|
||||
|
||||
<!-- Download as SVG -->
|
||||
<button
|
||||
type="button"
|
||||
class="toolbar-btn"
|
||||
disabled={!hasCharts || downloadingFormat !== null}
|
||||
onclick={() => handleDownload('svg')}
|
||||
title="Download meteogram as SVG vector image"
|
||||
>
|
||||
{#if downloadingFormat === 'svg'}
|
||||
<svg
|
||||
class="h-4 w-4 animate-spin"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
||||
</svg>
|
||||
{:else}
|
||||
<svg
|
||||
class="h-4 w-4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
||||
<polyline points="7 10 12 15 17 10" />
|
||||
<line x1="12" y1="15" x2="12" y2="3" />
|
||||
</svg>
|
||||
{/if}
|
||||
<span>SVG</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.toolbar-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.25rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
background: hsl(var(--muted) / 0.5);
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: var(--radius, 0.375rem);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
color 150ms ease,
|
||||
background-color 150ms ease,
|
||||
border-color 150ms ease;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.toolbar-btn:hover:not(:disabled) {
|
||||
color: hsl(var(--foreground));
|
||||
background: hsl(var(--muted));
|
||||
border-color: hsl(var(--foreground) / 0.2);
|
||||
}
|
||||
|
||||
.toolbar-btn:active:not(:disabled) {
|
||||
background: hsl(var(--muted) / 0.8);
|
||||
transform: translateY(0.5px);
|
||||
}
|
||||
|
||||
.toolbar-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.toolbar-btn:focus-visible {
|
||||
outline: 2px solid hsl(var(--ring));
|
||||
outline-offset: 2px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user