diff --git a/.claude/hooks b/.claude/hooks
new file mode 100644
index 0000000..e69de29
diff --git a/.claude/launch.json b/.claude/launch.json
new file mode 100644
index 0000000..e69de29
diff --git a/.claude/routines b/.claude/routines
new file mode 100644
index 0000000..e69de29
diff --git a/.claude/scheduled_tasks.json b/.claude/scheduled_tasks.json
new file mode 100644
index 0000000..e69de29
diff --git a/.claude/settings.json b/.claude/settings.json
new file mode 100644
index 0000000..e69de29
diff --git a/.claude/skills b/.claude/skills
new file mode 100644
index 0000000..e69de29
diff --git a/.claude/workflows b/.claude/workflows
new file mode 100644
index 0000000..e69de29
diff --git a/.mcp.json b/.mcp.json
new file mode 100644
index 0000000..e69de29
diff --git a/package-lock.json b/package-lock.json
index bd54024..019fcde 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,11 +1,11 @@
{
- "name": "open-meteo-weather",
+ "name": "ombrella",
"version": "0.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "name": "open-meteo-weather",
+ "name": "ombrella",
"version": "0.0.1",
"devDependencies": {
"@eslint/compat": "^2.1.0",
@@ -23,7 +23,6 @@
"clsx": "^2.1.1",
"date-fns": "^4.4.0",
"date-fns-tz": "^3.2.0",
- "echarts": "^6.1.0",
"eslint": "^10.7.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-svelte": "^3.21.0",
@@ -2123,24 +2122,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/echarts": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/echarts/-/echarts-6.1.0.tgz",
- "integrity": "sha512-q0yaFPggC9FUdsWH4blavRWFmxdrIodbkoKNAjJudAI6CA9gNPxHtV2RcZNEepZVlk4yvBYkOkbk6HIVpIyHZA==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "tslib": "2.3.0",
- "zrender": "6.1.0"
- }
- },
- "node_modules/echarts/node_modules/tslib": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
- "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
- "dev": true,
- "license": "0BSD"
- },
"node_modules/enhanced-resolve": {
"version": "5.24.2",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.2.tgz",
@@ -4510,23 +4491,6 @@
"integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==",
"dev": true,
"license": "MIT"
- },
- "node_modules/zrender": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/zrender/-/zrender-6.1.0.tgz",
- "integrity": "sha512-oEGMDB6pOP2S6OwRR4PdVv610zrjnA3Bh+JnSG12fYJlBKjtNAoEb5fSUoCOOINlH96I2fU38/A2UpRKs67xYQ==",
- "dev": true,
- "license": "BSD-3-Clause",
- "dependencies": {
- "tslib": "2.3.0"
- }
- },
- "node_modules/zrender/node_modules/tslib": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
- "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
- "dev": true,
- "license": "0BSD"
}
}
}
diff --git a/package.json b/package.json
index c3549b4..68989c7 100644
--- a/package.json
+++ b/package.json
@@ -1,5 +1,5 @@
{
- "name": "open-meteo-weather",
+ "name": "ombrella",
"private": true,
"version": "0.0.1",
"type": "module",
@@ -32,7 +32,6 @@
"clsx": "^2.1.1",
"date-fns": "^4.4.0",
"date-fns-tz": "^3.2.0",
- "echarts": "^6.1.0",
"eslint": "^10.7.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-svelte": "^3.21.0",
diff --git a/src/lib/charts/CanvasChart.svelte b/src/lib/charts/CanvasChart.svelte
new file mode 100644
index 0000000..535a5c2
--- /dev/null
+++ b/src/lib/charts/CanvasChart.svelte
@@ -0,0 +1,829 @@
+
+
+
+
+
+
+ {#if showLegend && series.length > 0}
+
+ {#each series.filter((s) => s.showInLegend !== false) as s (s.name)}
+
+ {/each}
+
+ {/if}
+
+
+
+
+ {#if tooltipVisible}
+
+
+ {formatZoned(new Date(timestamps[hoverIdx] * 1000), timezone, 'EEE d MMM HH:mm')}
+
+ {#each tooltipRows as row (row.name)}
+
+
+ {row.name}:
+ {row.value}
+
+ {/each}
+
+ {/if}
+
+
diff --git a/src/lib/charts/bands.ts b/src/lib/charts/bands.ts
new file mode 100644
index 0000000..d97e9a2
--- /dev/null
+++ b/src/lib/charts/bands.ts
@@ -0,0 +1,24 @@
+/**
+ * Daylight Bands
+ *
+ * Converts sunrise/sunset timestamp arrays into neutral background band
+ * descriptors that CanvasChart renders as shaded daylight areas.
+ */
+
+/** A background band on the time axis, expressed in epoch seconds. */
+export interface DaylightBand {
+ /** Band start (epoch seconds) */
+ start: number;
+ /** Band end (epoch seconds) */
+ end: number;
+}
+
+/**
+ * Builds daylight bands from sunrise/sunset arrays.
+ *
+ * @param sunrise - Array of sunrise timestamps (unix seconds)
+ * @param sunset - Array of sunset timestamps (unix seconds)
+ */
+export function buildDaylightBands(sunrise: number[], sunset: number[]): DaylightBand[] {
+ return sunrise.map((r, i) => ({ start: r, end: sunset[i] }));
+}
diff --git a/src/lib/charts/data.ts b/src/lib/charts/data.ts
new file mode 100644
index 0000000..65e5731
--- /dev/null
+++ b/src/lib/charts/data.ts
@@ -0,0 +1,106 @@
+/**
+ * Chart Data Helpers
+ *
+ * Shared color palette and data-processing helpers used by the chart pages.
+ * Ported from the previous ECharts utilities so the visual identity and
+ * calculations stay identical.
+ */
+
+// ─── Color Palette ───────────────────────────────────────────────────────────
+
+/** Default series color palette matching the application's design system */
+export const SERIES_COLORS = [
+ '#5470c6',
+ '#91cc75',
+ '#fac858',
+ '#ee6666',
+ '#73c0de',
+ '#3ba272',
+ '#fc8452',
+ '#9a60b4',
+ '#ea7ccc',
+ '#4dc9f6'
+] as const;
+
+/** Semantic colors used for specific chart elements */
+export const CHART_COLORS = {
+ average: '#5e5e5e',
+ currentTimeLine: '#ef4444',
+ daylight: 'rgba(255, 255, 194, 0.3)',
+ memberLine: 'rgba(115, 192, 222, 0.45)'
+} as const;
+
+// ─── Utility: Detect column-type variables ───────────────────────────────────
+
+/** Units that should be rendered as bar/column charts instead of lines. */
+const COLUMN_UNITS = new Set(['mm', 'cm', 'inch', 'MJ/m²']);
+
+/**
+ * Returns true if the given unit should be rendered as a bar chart.
+ */
+export function isColumnUnit(unit: string): boolean {
+ return COLUMN_UNITS.has(unit);
+}
+
+// ─── Data Processing Helpers ─────────────────────────────────────────────────
+
+export interface AverageResult {
+ average: number[];
+ averageCount: number[];
+}
+
+/**
+ * Calculates per-timestep average and count from hourly model data.
+ *
+ * @param hourlyData - The `data.hourly` object from the API response
+ * @param variable - The variable prefix to filter on (e.g. 'temperature_2m')
+ * @param timeLength - Number of timesteps
+ * @returns Object containing running average and count arrays
+ */
+export function calculateAverage(
+ hourlyData: Record,
+ variable: string,
+ timeLength: number
+): AverageResult {
+ const average = new Array(timeLength).fill(0);
+ const averageCount = new Array(timeLength).fill(0);
+
+ for (const [model, values] of Object.entries(hourlyData)) {
+ if (model === 'time') continue;
+ if (!model.startsWith(variable)) continue;
+
+ for (const [index, val] of (values as number[]).entries()) {
+ if (val !== null && val !== undefined && isFinite(val)) {
+ average[index] += val;
+ averageCount[index]++;
+ }
+ }
+ }
+
+ // Finalize average values
+ for (let i = 0; i < timeLength; i++) {
+ if (averageCount[i] > 0) {
+ average[i] = Math.round((average[i] / averageCount[i]) * 10) / 10;
+ }
+ }
+
+ return { average, averageCount };
+}
+
+/**
+ * Finds the unit string for a given variable from the hourly_units map.
+ * Returns an empty string if the variable is not found.
+ */
+export function findUnit(
+ hourlyUnits: Record,
+ hourlyData: Record,
+ variable: string
+): string {
+ for (const model of Object.keys(hourlyData)) {
+ if (model === 'time') continue;
+ if (model.startsWith(variable) && hourlyUnits[model]) {
+ return hourlyUnits[model];
+ }
+ }
+ return '';
+}
diff --git a/src/lib/charts/index.ts b/src/lib/charts/index.ts
new file mode 100644
index 0000000..a81689c
--- /dev/null
+++ b/src/lib/charts/index.ts
@@ -0,0 +1,15 @@
+/**
+ * Canvas Charts — Barrel Export
+ *
+ * Usage:
+ * import { CanvasChart, buildDaylightBands, SERIES_COLORS } from '$lib/charts';
+ */
+
+export { default as CanvasChart } from './CanvasChart.svelte';
+export type { ChartSeries } from './CanvasChart.svelte';
+
+export { buildDaylightBands } from './bands';
+export type { DaylightBand } from './bands';
+
+export { CHART_COLORS, SERIES_COLORS, calculateAverage, findUnit, isColumnUnit } from './data';
+export type { AverageResult } from './data';
diff --git a/src/lib/components/charts/ChartContainer.svelte b/src/lib/components/charts/ChartContainer.svelte
index 83403f0..4cfab83 100644
--- a/src/lib/components/charts/ChartContainer.svelte
+++ b/src/lib/components/charts/ChartContainer.svelte
@@ -11,7 +11,7 @@
Usage:
{#each charts as chart}
-
+
{/each}
-->
diff --git a/src/lib/components/charts/ChartToolbar.svelte b/src/lib/components/charts/ChartToolbar.svelte
index ea7a079..1024e2f 100644
--- a/src/lib/components/charts/ChartToolbar.svelte
+++ b/src/lib/components/charts/ChartToolbar.svelte
@@ -3,7 +3,6 @@
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
@@ -11,7 +10,7 @@
Usage:
{#snippet controls()}
@@ -19,22 +18,23 @@
{/snippet}
-->
-
- import type { ExportFormat } from '$lib/utils/echarts/download';
- import type * as echarts from 'echarts';
+
-
-
-
-
diff --git a/src/lib/components/charts/echarts.css b/src/lib/components/charts/echarts.css
deleted file mode 100644
index be91b40..0000000
--- a/src/lib/components/charts/echarts.css
+++ /dev/null
@@ -1,164 +0,0 @@
-/* ═══════════════════════════════════════════════════════════════════════════════
- ECharts Global Styles — Open-Meteo Weather
-
- Shared CSS for all ECharts chart instances across the application.
- Provides consistent theming, tooltip styling, and responsive behavior
- that integrates with the application's design system (Tailwind + shadcn).
- ═══════════════════════════════════════════════════════════════════════════════ */
-
-/* ─── Chart Wrapper ────────────────────────────────────────────────────────── */
-
-.echart-wrapper {
- width: 100%;
- position: relative;
- overflow: hidden;
-}
-
-/* ─── Chart Container (legacy class support) ───────────────────────────────── */
-
-.echarts-container {
- width: 100%;
- height: 100%;
- min-height: 300px;
-}
-
-/* Ensure canvas background is always transparent so our page bg shows through */
-.echart-wrapper canvas,
-.echarts-container canvas {
- background: transparent !important;
-}
-
-/* ─── Tooltip Styling ──────────────────────────────────────────────────────── */
-
-/* Override ECharts' default tooltip to match the application's popover design */
-.echarts-tooltip {
- background: hsl(var(--popover)) !important;
- border: 1px solid hsl(var(--border)) !important;
- border-radius: var(--radius, 0.5rem) !important;
- box-shadow:
- 0 4px 6px -1px rgb(0 0 0 / 0.1),
- 0 2px 4px -2px rgb(0 0 0 / 0.05) !important;
- padding: 0.625rem 0.75rem !important;
- font-size: 0.8125rem !important;
- line-height: 1.4 !important;
- max-width: min(90vw, 480px) !important;
- pointer-events: none;
-}
-
-.echarts-tooltip-content {
- color: hsl(var(--popover-foreground)) !important;
-}
-
-/* Tooltip marker dots — make them slightly larger and rounded */
-.echarts-tooltip .echarts-tooltip-marker,
-.echarts-tooltip span[style*='border-radius'] {
- display: inline-block;
- vertical-align: middle;
- margin-right: 0.25rem;
-}
-
-/* ─── Loading Mask ─────────────────────────────────────────────────────────── */
-
-.echarts-loading-mask {
- background: hsl(var(--background) / 0.8) !important;
-}
-
-/* ─── Light Mode Adjustments ───────────────────────────────────────────────── */
-
-[data-theme='light'] .echart-wrapper,
-[data-theme='light'] .echarts-container,
-:root:not(.dark):not([data-theme='dark']) .echart-wrapper,
-:root:not(.dark):not([data-theme='dark']) .echarts-container {
- color: hsl(var(--foreground));
-}
-
-[data-theme='light'] .echarts-tooltip,
-:root:not(.dark):not([data-theme='dark']) .echarts-tooltip {
- color: hsl(var(--popover-foreground)) !important;
-}
-
-/* ─── Dark Mode Adjustments ────────────────────────────────────────────────── */
-
-.dark .echart-wrapper,
-[data-theme='dark'] .echart-wrapper,
-.dark .echarts-container,
-[data-theme='dark'] .echarts-container {
- color: hsl(var(--foreground));
-}
-
-.dark .echarts-tooltip,
-[data-theme='dark'] .echarts-tooltip {
- color: hsl(var(--popover-foreground)) !important;
- box-shadow:
- 0 4px 6px -1px rgb(0 0 0 / 0.3),
- 0 2px 4px -2px rgb(0 0 0 / 0.15) !important;
-}
-
-/* ─── Toolbox Icon Overrides ───────────────────────────────────────────────── */
-
-/* Make sure the toolbox icons are visually subtle until hovered */
-.echart-wrapper [class*='toolbox'],
-.echarts-container [class*='toolbox'] {
- opacity: 0.6;
- transition: opacity 150ms ease;
-}
-
-.echart-wrapper:hover [class*='toolbox'],
-.echarts-container:hover [class*='toolbox'] {
- opacity: 1;
-}
-
-/* ─── Chart Spacing ────────────────────────────────────────────────────────── */
-
-/* Add consistent vertical spacing between stacked chart instances */
-.chart-content .echart-wrapper + .echart-wrapper {
- margin-top: 0.5rem;
-}
-
-/* ─── Responsive Sizing ────────────────────────────────────────────────────── */
-
-@media (max-width: 640px) {
- .echarts-container {
- min-height: 240px;
- }
-
- /* Slightly smaller tooltips on mobile */
- .echarts-tooltip {
- font-size: 0.75rem !important;
- padding: 0.5rem 0.625rem !important;
- }
-}
-
-@media (min-width: 641px) and (max-width: 1024px) {
- .echarts-container {
- min-height: 280px;
- }
-}
-
-/* ─── Print Styles ─────────────────────────────────────────────────────────── */
-
-@media print {
- .echart-wrapper,
- .echarts-container {
- break-inside: avoid;
- page-break-inside: avoid;
- }
-
- /* Hide interactive elements when printing */
- .echart-wrapper [class*='toolbox'],
- .echarts-container [class*='toolbox'],
- .chart-toolbar {
- display: none !important;
- }
-}
-
-/* ─── Accessibility ────────────────────────────────────────────────────────── */
-
-/* Respect reduced motion preferences */
-@media (prefers-reduced-motion: reduce) {
- .echart-wrapper *,
- .echarts-container * {
- animation-duration: 0.01ms !important;
- transition-duration: 0.01ms !important;
- }
-}
diff --git a/src/lib/components/charts/echarts.ts b/src/lib/components/charts/echarts.ts
deleted file mode 100644
index 6df7931..0000000
--- a/src/lib/components/charts/echarts.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-// Tree-shaken ECharts build: only the pieces the app actually renders (line
-// and bar series with grid/tooltip/legend/dataZoom/mark/graphic features) are
-// registered, instead of the ~1 MB full bundle.
-import { BarChart, LineChart } from 'echarts/charts';
-import {
- DataZoomInsideComponent,
- DataZoomSliderComponent,
- GraphicComponent,
- GridComponent,
- LegendComponent,
- MarkAreaComponent,
- MarkLineComponent,
- TitleComponent,
- TooltipComponent
-} from 'echarts/components';
-import * as echarts from 'echarts/core';
-import { CanvasRenderer } from 'echarts/renderers';
-
-echarts.use([
- LineChart,
- BarChart,
- GridComponent,
- TooltipComponent,
- LegendComponent,
- TitleComponent,
- DataZoomInsideComponent,
- DataZoomSliderComponent,
- MarkLineComponent,
- MarkAreaComponent,
- GraphicComponent,
- CanvasRenderer
-]);
-
-export { echarts };
diff --git a/src/lib/components/charts/index.ts b/src/lib/components/charts/index.ts
index c67aa73..a892500 100644
--- a/src/lib/components/charts/index.ts
+++ b/src/lib/components/charts/index.ts
@@ -4,9 +4,8 @@
* Re-exports all chart-related Svelte components from a single entry point.
*
* Usage:
- * import { EChart, ChartContainer, ChartToolbar } from '$lib/components/charts';
+ * import { ChartContainer, ChartToolbar } from '$lib/components/charts';
*/
-export { default as EChart } from './EChart.svelte';
export { default as ChartContainer } from './ChartContainer.svelte';
export { default as ChartToolbar } from './ChartToolbar.svelte';
diff --git a/src/lib/components/navigation/weather-nav.svelte b/src/lib/components/navigation/weather-nav.svelte
index 872c818..51fabbf 100644
--- a/src/lib/components/navigation/weather-nav.svelte
+++ b/src/lib/components/navigation/weather-nav.svelte
@@ -52,41 +52,19 @@
class:w-55={!collapsed}
class:w-14={collapsed}
>
-
-
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index 33e9f0e..f2bb703 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -1,3 +1,3 @@
- Open-Meteo Weather
+ OMbrella
diff --git a/src/routes/page.svelte.spec.ts b/src/routes/page.svelte.spec.ts
index 08ddddc..1fa08f6 100644
--- a/src/routes/page.svelte.spec.ts
+++ b/src/routes/page.svelte.spec.ts
@@ -8,6 +8,6 @@ describe('/+page.svelte', () => {
render(Page);
const title = document.querySelector('title');
- expect(title?.textContent).toBe('Open-Meteo Weather');
+ expect(title?.textContent).toBe('OMbrella');
});
});
diff --git a/src/routes/weather/14-day/[location]/+page.svelte b/src/routes/weather/14-day/[location]/+page.svelte
index 9f26483..64752b1 100644
--- a/src/routes/weather/14-day/[location]/+page.svelte
+++ b/src/routes/weather/14-day/[location]/+page.svelte
@@ -1,32 +1,24 @@
@@ -221,20 +194,25 @@
{/if}
-
- {#each chartOptions as option, i (i)}
-
- {/each}
+
+ {#if fetchedData}
+ {#each chartDefs as def, i (i)}
+
+ {/each}
+ {/if}
diff --git a/src/routes/weather/compare/[location]/+page.svelte b/src/routes/weather/compare/[location]/+page.svelte
index 7a29abe..93b612d 100644
--- a/src/routes/weather/compare/[location]/+page.svelte
+++ b/src/routes/weather/compare/[location]/+page.svelte
@@ -1,28 +1,25 @@
@@ -254,16 +230,25 @@
{/if}
-
- {#each chartOptions as option, i (i)}
-
- {/each}
+
+ {#if fetchedData}
+ {#each chartDefs as def, i (i)}
+
+ {/each}
+ {/if}
{#if fetchedData && !loading}
diff --git a/src/routes/weather/week/[location]/+page.svelte b/src/routes/weather/week/[location]/+page.svelte
index 9ea855f..7dfecce 100644
--- a/src/routes/weather/week/[location]/+page.svelte
+++ b/src/routes/weather/week/[location]/+page.svelte
@@ -82,7 +82,7 @@
timezone: result.timezone,
timestamps: result.hourlyTimestamps,
hourlyDates: result.hourlyDates,
- markAreas: result.markAreas
+ daylightBands: result.daylightBands
};
fetchedDaily = {
@@ -102,8 +102,8 @@
- Weather | Open-Meteo.com
-
+ OMbrella | Weather
+
diff --git a/src/routes/weather/week/[location]/DailyCards.svelte b/src/routes/weather/week/[location]/DailyCards.svelte
index 671f9bd..9742154 100644
--- a/src/routes/weather/week/[location]/DailyCards.svelte
+++ b/src/routes/weather/week/[location]/DailyCards.svelte
@@ -40,7 +40,7 @@
-
+
{#if daily}
{#each daily.dailyDates as time, index (index)}
{@const selected = isSameDayInZone(time, selectedDay, daily.timezone)}
@@ -57,100 +57,90 @@
{@const windDir = daily.daily.winddirection_10m_dominant[index]}
{@const unit = String(units.temperature_unit)}
{@const maxStyle = getTempStyle(tempMax, unit)}
- {@const minStyle = getTempStyle(tempMin, unit)}
{#if tempMax != null && !isNaN(tempMax)}