visual updates
This commit is contained in:
@@ -124,9 +124,9 @@
|
||||
min-width: var(--chart-min-width);
|
||||
}
|
||||
|
||||
/* Mobile: fit the chart to the viewport instead of forcing a min-width
|
||||
/* Below lg: fit the chart to the viewport instead of forcing a min-width
|
||||
sideways scroll (which fights touch inspection). Pinch to zoom for detail. */
|
||||
@media (max-width: 767px) {
|
||||
@media (max-width: 1023px) {
|
||||
.chart-container {
|
||||
min-width: 0;
|
||||
}
|
||||
@@ -135,7 +135,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
@media (min-width: 1024px) {
|
||||
.chart-bleed {
|
||||
margin-left: -1.5rem;
|
||||
margin-right: -1.5rem;
|
||||
|
||||
@@ -19,11 +19,11 @@
|
||||
</ChartToolbar>
|
||||
-->
|
||||
<script module lang="ts">
|
||||
export type { DownloadableChart } from './downloadChartsPng';
|
||||
export type { ExportableChart } from './downloadChartsPng';
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { type DownloadableChart, downloadChartsPng } from './downloadChartsPng';
|
||||
import { type ExportableChart, downloadChartsPng } from './downloadChartsPng';
|
||||
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
|
||||
interface Props {
|
||||
/** Chart components available for download (undefined entries are skipped) */
|
||||
charts?: Array<DownloadableChart | undefined | null>;
|
||||
charts?: Array<ExportableChart | undefined | null>;
|
||||
/** Base file name for downloaded images (without extension) */
|
||||
fileName?: string;
|
||||
/** Optional CSS class for the outer container */
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
/**
|
||||
* Shared PNG export for charts.
|
||||
*
|
||||
* Stitches one or more chart images vertically onto a single canvas (over the
|
||||
* current theme background) and triggers a download. Used by both the standalone
|
||||
* ChartToolbar and the inline toolbar buttons.
|
||||
* Each chart composites itself (plot + icon bands + optional title + legend)
|
||||
* onto a canvas via `getExportImage`; those are stacked vertically over the
|
||||
* current theme background and downloaded as one PNG.
|
||||
*/
|
||||
|
||||
/** Anything that can export itself as a PNG data URL (e.g. a CanvasChart). */
|
||||
export interface DownloadableChart {
|
||||
getPngDataUrl(): string | null;
|
||||
/** A chart that can composite itself (plot + icons + legend) for export. */
|
||||
export interface ExportableChart {
|
||||
getExportImage(opts?: { title?: string }): Promise<HTMLCanvasElement | null>;
|
||||
}
|
||||
|
||||
function loadImage(src: string): Promise<HTMLImageElement> {
|
||||
return new Promise((resolve) => {
|
||||
const img = new Image();
|
||||
img.onload = () => resolve(img);
|
||||
img.onerror = () => resolve(img);
|
||||
img.src = src;
|
||||
});
|
||||
/** One chart to export, with the title to render above it (e.g. panel name). */
|
||||
export interface ChartExportItem {
|
||||
chart: ExportableChart | null | undefined;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
/** Resolves the page background so exports match the current theme. */
|
||||
@@ -38,25 +35,30 @@ function triggerDownload(url: string, name: string): void {
|
||||
});
|
||||
}
|
||||
|
||||
/** Normalise either a bare chart or a {chart, title} item. */
|
||||
function toItem(x: ChartExportItem | ExportableChart | null | undefined): ChartExportItem {
|
||||
if (x && 'getExportImage' in x) return { chart: x };
|
||||
return (x as ChartExportItem) ?? { chart: null };
|
||||
}
|
||||
|
||||
/**
|
||||
* Stitch the given charts into one PNG and download it. Resolves once the
|
||||
* download has been triggered (or immediately if there is nothing to export).
|
||||
*/
|
||||
export async function downloadChartsPng(
|
||||
charts: Array<DownloadableChart | undefined | null>,
|
||||
charts: Array<ChartExportItem | ExportableChart | null | undefined>,
|
||||
fileName: string
|
||||
): Promise<void> {
|
||||
const dataUrls = charts
|
||||
.filter((chart): chart is DownloadableChart => chart != null)
|
||||
.map((chart) => chart.getPngDataUrl())
|
||||
.filter((url): url is string => url !== null);
|
||||
if (dataUrls.length === 0) return;
|
||||
const items = charts.map(toItem).filter((it) => it.chart != null);
|
||||
if (items.length === 0) return;
|
||||
|
||||
const images = (await Promise.all(dataUrls.map(loadImage))).filter((img) => img.naturalWidth > 0);
|
||||
if (images.length === 0) return;
|
||||
const canvases = (
|
||||
await Promise.all(items.map((it) => it.chart!.getExportImage({ title: it.title })))
|
||||
).filter((c): c is HTMLCanvasElement => c != null && c.width > 0 && c.height > 0);
|
||||
if (canvases.length === 0) return;
|
||||
|
||||
const maxWidth = Math.max(...images.map((img) => img.naturalWidth));
|
||||
const totalHeight = images.reduce((sum, img) => sum + img.naturalHeight, 0);
|
||||
const maxWidth = Math.max(...canvases.map((c) => c.width));
|
||||
const totalHeight = canvases.reduce((sum, c) => sum + c.height, 0);
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = maxWidth;
|
||||
@@ -69,9 +71,9 @@ export async function downloadChartsPng(
|
||||
ctx.fillRect(0, 0, maxWidth, totalHeight);
|
||||
|
||||
let y = 0;
|
||||
for (const img of images) {
|
||||
ctx.drawImage(img, 0, y);
|
||||
y += img.naturalHeight;
|
||||
for (const c of canvases) {
|
||||
ctx.drawImage(c, 0, y);
|
||||
y += c.height;
|
||||
}
|
||||
|
||||
triggerDownload(canvas.toDataURL('image/png'), `${fileName}.png`);
|
||||
|
||||
@@ -9,4 +9,4 @@
|
||||
|
||||
export { default as ChartContainer } from './ChartContainer.svelte';
|
||||
export { default as ChartToolbar } from './ChartToolbar.svelte';
|
||||
export { downloadChartsPng, type DownloadableChart } from './downloadChartsPng';
|
||||
export { downloadChartsPng, type ExportableChart, type ChartExportItem } from './downloadChartsPng';
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
>
|
||||
<!-- Mobile menu toggle -->
|
||||
<button
|
||||
class="flex h-8 w-8 items-center justify-center rounded-md transition-colors hover:bg-black/5 md:hidden dark:hover:bg-white/10"
|
||||
class="-ms-1 flex h-10 w-10 shrink-0 cursor-pointer items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-muted hover:text-foreground md:hidden"
|
||||
onclick={onMenuToggle}
|
||||
aria-label="Toggle menu"
|
||||
>
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
</div>
|
||||
{#if !collapsed}
|
||||
<span class="text-sm font-bold tracking-tight whitespace-nowrap text-sidebar-foreground">
|
||||
Drizzli
|
||||
Drizz.li
|
||||
</span>
|
||||
{/if}
|
||||
</a>
|
||||
@@ -129,7 +129,7 @@
|
||||
{#if onToggle}
|
||||
<div class="border-t border-sidebar-border px-2 py-3">
|
||||
<button
|
||||
class="relative flex w-full items-center rounded-md px-2.5 py-2 text-sm font-medium text-sidebar-foreground opacity-70 transition-colors duration-150 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:opacity-100"
|
||||
class="relative flex w-full cursor-pointer items-center rounded-md px-2.5 py-2 text-sm font-medium text-sidebar-foreground opacity-70 transition-colors duration-150 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:opacity-100"
|
||||
onclick={onToggle}
|
||||
title={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
||||
>
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
|
||||
<Popover.Root>
|
||||
<Popover.Trigger
|
||||
class="flex h-9 shrink-0 cursor-pointer items-center gap-1.5 rounded-full border border-border/70 px-2.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground data-[state=open]:bg-muted data-[state=open]:text-foreground"
|
||||
class="flex h-9 w-9 shrink-0 cursor-pointer items-center justify-center rounded-full border border-border/70 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground data-[state=open]:bg-muted data-[state=open]:text-foreground"
|
||||
aria-label="Choose measurement units"
|
||||
title="Units"
|
||||
>
|
||||
@@ -63,7 +63,6 @@
|
||||
/>
|
||||
<circle cx="12" cy="15" r="1" fill="currentColor" stroke="none" />
|
||||
</svg>
|
||||
<span class="hidden text-xs font-semibold sm:inline">Units</span>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content align="end" class="w-64 border-border">
|
||||
<div class="flex flex-col gap-4">
|
||||
|
||||
Reference in New Issue
Block a user