feat: canvas to echarts #5
@@ -31,8 +31,8 @@
|
|||||||
|
|
||||||
const CHART_GROUP = 'week-meteogram';
|
const CHART_GROUP = 'week-meteogram';
|
||||||
const MS_PER_DAY = 24 * 3600 * 1000;
|
const MS_PER_DAY = 24 * 3600 * 1000;
|
||||||
const CELL_W = 38;
|
const CURVE_H = 140;
|
||||||
const CURVE_H = 120;
|
const SVG_UNIT = 100;
|
||||||
|
|
||||||
let params = $state({
|
let params = $state({
|
||||||
latitude: [$storedLocation.latitude],
|
latitude: [$storedLocation.latitude],
|
||||||
@@ -57,7 +57,6 @@
|
|||||||
const selectedDay = new SvelteDate();
|
const selectedDay = new SvelteDate();
|
||||||
let selectedDayIndex = $state(1);
|
let selectedDayIndex = $state(1);
|
||||||
|
|
||||||
let tableScrollDiv: HTMLElement | undefined = $state();
|
|
||||||
let hourlyInterval = $state<1 | 3>(3);
|
let hourlyInterval = $state<1 | 3>(3);
|
||||||
let showDetailedCharts = $state(false);
|
let showDetailedCharts = $state(false);
|
||||||
|
|
||||||
@@ -104,22 +103,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function scrollTableToDay(day: Date): void {
|
|
||||||
if (!tableScrollDiv) return;
|
|
||||||
const cells = tableScrollDiv.querySelectorAll<HTMLElement>('td.hour-cell[data-date]');
|
|
||||||
for (const cell of cells) {
|
|
||||||
if (Number(cell.dataset['date']) === day.getDate()) {
|
|
||||||
tableScrollDiv.scrollTo({ left: cell.offsetLeft - 120, behavior: 'smooth' });
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const switchDay = (date: Date, index: number) => {
|
const switchDay = (date: Date, index: number) => {
|
||||||
selectedDay.setTime(date.getTime());
|
selectedDay.setTime(date.getTime());
|
||||||
selectedDayIndex = index;
|
selectedDayIndex = index;
|
||||||
scrollChartsToDay(date);
|
scrollChartsToDay(date);
|
||||||
requestAnimationFrame(() => scrollTableToDay(date));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function resetZoom(): void {
|
function resetZoom(): void {
|
||||||
@@ -172,7 +159,6 @@
|
|||||||
echarts.connect(CHART_GROUP);
|
echarts.connect(CHART_GROUP);
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
scrollChartsToDay(selectedDay);
|
scrollChartsToDay(selectedDay);
|
||||||
scrollTableToDay(selectedDay);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -247,14 +233,20 @@
|
|||||||
return `${date.getMonth() + 1}-${date.getDate()}`;
|
return `${date.getMonth() + 1}-${date.getDate()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFilteredIndices(dates: Date[]): number[] {
|
function getDayIndices(dates: Date[], day: Date): number[] {
|
||||||
if (hourlyInterval === 1) {
|
const dayDate = day.getDate();
|
||||||
return dates.map((_, i) => i);
|
const dayMonth = day.getMonth();
|
||||||
|
const dayYear = day.getFullYear();
|
||||||
|
const indices: number[] = [];
|
||||||
|
for (let i = 0; i < dates.length; i++) {
|
||||||
|
const d = dates[i];
|
||||||
|
if (d.getDate() === dayDate && d.getMonth() === dayMonth && d.getFullYear() === dayYear) {
|
||||||
|
if (hourlyInterval === 1 || d.getHours() % 3 === 0) {
|
||||||
|
indices.push(i);
|
||||||
}
|
}
|
||||||
return dates.reduce<number[]>((acc, date, i) => {
|
}
|
||||||
if (date.getHours() % 3 === 0) acc.push(i);
|
}
|
||||||
return acc;
|
return indices;
|
||||||
}, []);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPrecipBarHeight(val: number, maxVal: number): number {
|
function getPrecipBarHeight(val: number, maxVal: number): number {
|
||||||
@@ -266,9 +258,9 @@
|
|||||||
if (indices.length < 2) return '';
|
if (indices.length < 2) return '';
|
||||||
const rangeT = maxT - minT || 1;
|
const rangeT = maxT - minT || 1;
|
||||||
const points: [number, number][] = indices.map((idx, i) => {
|
const points: [number, number][] = indices.map((idx, i) => {
|
||||||
const x = i * CELL_W + CELL_W / 2;
|
const x = i * SVG_UNIT + SVG_UNIT / 2;
|
||||||
const t = temps[idx] ?? minT;
|
const t = temps[idx] ?? minT;
|
||||||
const y = CURVE_H - 20 - ((t - minT) / rangeT) * (CURVE_H - 40);
|
const y = CURVE_H - 24 - ((t - minT) / rangeT) * (CURVE_H - 48);
|
||||||
return [x, y];
|
return [x, y];
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -286,8 +278,9 @@
|
|||||||
function buildAreaPath(temps: number[], indices: number[], minT: number, maxT: number): string {
|
function buildAreaPath(temps: number[], indices: number[], minT: number, maxT: number): string {
|
||||||
const curvePath = buildCurvePath(temps, indices, minT, maxT);
|
const curvePath = buildCurvePath(temps, indices, minT, maxT);
|
||||||
if (!curvePath) return '';
|
if (!curvePath) return '';
|
||||||
const lastX = (indices.length - 1) * CELL_W + CELL_W / 2;
|
const n = indices.length;
|
||||||
const firstX = CELL_W / 2;
|
const lastX = (n - 1) * SVG_UNIT + SVG_UNIT / 2;
|
||||||
|
const firstX = SVG_UNIT / 2;
|
||||||
return `${curvePath} L ${lastX},${CURVE_H} L ${firstX},${CURVE_H} Z`;
|
return `${curvePath} L ${lastX},${CURVE_H} L ${firstX},${CURVE_H} Z`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,6 +288,10 @@
|
|||||||
return Math.min(0.7, (cover ?? 0) / 120);
|
return Math.min(0.7, (cover ?? 0) / 120);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getIconSize(interval: 1 | 3): number {
|
||||||
|
return interval === 3 ? 40 : 26;
|
||||||
|
}
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const loc = location;
|
const loc = location;
|
||||||
const modelList = params.models;
|
const modelList = params.models;
|
||||||
@@ -870,15 +867,19 @@
|
|||||||
{#if fetchedHourly}
|
{#if fetchedHourly}
|
||||||
{@const hourly = fetchedHourly.hourly}
|
{@const hourly = fetchedHourly.hourly}
|
||||||
{@const dates = fetchedHourly.hourlyDates}
|
{@const dates = fetchedHourly.hourlyDates}
|
||||||
{@const filteredIdx = getFilteredIndices(dates)}
|
{@const dayIdx = getDayIndices(dates, selectedDay)}
|
||||||
|
{@const numCols = dayIdx.length}
|
||||||
|
{@const svgW = numCols * SVG_UNIT}
|
||||||
{@const maxPrecip = Math.max(
|
{@const maxPrecip = Math.max(
|
||||||
...hourly.precipitation.filter((v) => v != null && !isNaN(v)),
|
...dayIdx.map((i) => hourly.precipitation[i]).filter((v) => v != null && !isNaN(v)),
|
||||||
0.1
|
0.1
|
||||||
)}
|
)}
|
||||||
{@const allTemps = hourly.temperature_2m.filter((t) => t != null && !isNaN(t))}
|
{@const dayTemps = dayIdx
|
||||||
{@const minTemp = Math.min(...allTemps)}
|
.map((i) => hourly.temperature_2m[i])
|
||||||
{@const maxTemp = Math.max(...allTemps)}
|
.filter((t) => t != null && !isNaN(t))}
|
||||||
{@const totalWidth = filteredIdx.length * CELL_W}
|
{@const minTemp = dayTemps.length ? Math.min(...dayTemps) : 0}
|
||||||
|
{@const maxTemp = dayTemps.length ? Math.max(...dayTemps) : 10}
|
||||||
|
{@const iconPx = getIconSize(hourlyInterval)}
|
||||||
|
|
||||||
<div class="hourly-header">
|
<div class="hourly-header">
|
||||||
<h3 class="hourly-title">
|
<h3 class="hourly-title">
|
||||||
@@ -897,26 +898,77 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Combined Hourly Meteogram + Table -->
|
{#if numCols > 0}
|
||||||
<div bind:this={tableScrollDiv} class="hourly-scroll">
|
<!-- Single-day hourly table (no horizontal scroll) -->
|
||||||
<div class="hourly-inner" style="min-width: {totalWidth + 96}px">
|
<div class="hourly-container">
|
||||||
<!-- Temperature Curve + Icons Area -->
|
<table class="hourly-table {hourlyInterval === 3 ? 'interval-3h' : 'interval-1h'}">
|
||||||
<div class="curve-area">
|
<caption class="sr-only">Hourly weather details for {location.name}</caption>
|
||||||
<div class="curve-row-header"></div>
|
<colgroup>
|
||||||
<div class="curve-container" style="width: {totalWidth}px; height: {CURVE_H}px">
|
<col class="col-header" />
|
||||||
<!-- Cloud cover background -->
|
{#each dayIdx as _ (_.toString())}
|
||||||
|
<col />
|
||||||
|
{/each}
|
||||||
|
</colgroup>
|
||||||
|
<tbody>
|
||||||
|
<!-- Time row -->
|
||||||
|
<tr>
|
||||||
|
<th class="row-header" scope="row"></th>
|
||||||
|
{#each dayIdx as idx (idx)}
|
||||||
|
{@const date = dates[idx]}
|
||||||
|
{@const isNow = isCurrentHour(date)}
|
||||||
|
<td class="hour-cell time-cell {isNow ? 'now' : ''}" data-date={date.getDate()}>
|
||||||
|
{pad(date.getHours())}<sup class="time-sup">00</sup>
|
||||||
|
</td>
|
||||||
|
{/each}
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Weather icons row (prominent) -->
|
||||||
|
<tr class="icon-row">
|
||||||
|
<th class="row-header" scope="row">
|
||||||
|
<svg class="fill-foreground inline-block" width="16px" height="16px">
|
||||||
|
<use xlink:href="/images/weather-icons/wi-day-cloudy.svg#Layer_1"></use>
|
||||||
|
</svg>
|
||||||
|
</th>
|
||||||
|
{#each dayIdx as idx (idx)}
|
||||||
|
{@const wCode = hourly.weather_code[idx]}
|
||||||
|
{@const date = dates[idx]}
|
||||||
|
{@const daytime = isDaytimeHour(date.getHours())}
|
||||||
|
{@const isNow = isCurrentHour(date)}
|
||||||
|
<td class="hour-cell weather-icon-cell {isNow ? 'now' : ''}">
|
||||||
|
<svg class="fill-foreground" width="{iconPx}px" height="{iconPx}px">
|
||||||
|
<use
|
||||||
|
xlink:href="/images/weather-icons/wi-{daytime
|
||||||
|
? 'day'
|
||||||
|
: 'night'}-{weatherCodes[wCode as keyof typeof weatherCodes] ??
|
||||||
|
'clear'}.svg#Layer_1"
|
||||||
|
></use>
|
||||||
|
</svg>
|
||||||
|
</td>
|
||||||
|
{/each}
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Temperature curve row (colspan) -->
|
||||||
|
<tr class="curve-row">
|
||||||
|
<th class="row-header curve-header" scope="row">
|
||||||
|
<span class="row-label"
|
||||||
|
>{params.temperature_unit === 'celsius' ? '°C' : '°F'}</span
|
||||||
|
>
|
||||||
|
</th>
|
||||||
|
<td colspan={numCols} class="curve-cell">
|
||||||
|
<div class="curve-container" style="height: {CURVE_H}px">
|
||||||
<svg
|
<svg
|
||||||
class="curve-svg"
|
class="curve-svg"
|
||||||
width={totalWidth}
|
width="100%"
|
||||||
height={CURVE_H}
|
height={CURVE_H}
|
||||||
viewBox="0 0 {totalWidth} {CURVE_H}"
|
viewBox="0 0 {svgW} {CURVE_H}"
|
||||||
|
preserveAspectRatio="none"
|
||||||
>
|
>
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="tempGrad" x1="0" y1="0" x2="1" y2="0">
|
<linearGradient id="tempGrad" x1="0" y1="0" x2="1" y2="0">
|
||||||
{#each filteredIdx as idx, i (idx)}
|
{#each dayIdx as idx, i (idx)}
|
||||||
{@const t = hourly.temperature_2m[idx]}
|
{@const t = hourly.temperature_2m[idx]}
|
||||||
<stop
|
<stop
|
||||||
offset="{(i / Math.max(filteredIdx.length - 1, 1)) * 100}%"
|
offset="{(i / Math.max(numCols - 1, 1)) * 100}%"
|
||||||
stop-color={getColor(t, String(params.temperature_unit))}
|
stop-color={getColor(t, String(params.temperature_unit))}
|
||||||
stop-opacity="0.45"
|
stop-opacity="0.45"
|
||||||
/>
|
/>
|
||||||
@@ -924,13 +976,13 @@
|
|||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
|
|
||||||
<!-- Cloud cover bands per cell -->
|
<!-- Cloud cover bands -->
|
||||||
{#each filteredIdx as idx, i (idx)}
|
{#each dayIdx as idx, i (idx)}
|
||||||
{@const cloud = hourly.cloud_cover[idx]}
|
{@const cloud = hourly.cloud_cover[idx]}
|
||||||
<rect
|
<rect
|
||||||
x={i * CELL_W}
|
x={i * SVG_UNIT}
|
||||||
y={0}
|
y={0}
|
||||||
width={CELL_W}
|
width={SVG_UNIT}
|
||||||
height={CURVE_H}
|
height={CURVE_H}
|
||||||
fill="rgb(160,175,190)"
|
fill="rgb(160,175,190)"
|
||||||
opacity={getCloudOpacity(cloud)}
|
opacity={getCloudOpacity(cloud)}
|
||||||
@@ -939,79 +991,39 @@
|
|||||||
|
|
||||||
<!-- Temperature area fill -->
|
<!-- Temperature area fill -->
|
||||||
<path
|
<path
|
||||||
d={buildAreaPath(hourly.temperature_2m, filteredIdx, minTemp, maxTemp)}
|
d={buildAreaPath(hourly.temperature_2m, dayIdx, minTemp, maxTemp)}
|
||||||
fill="url(#tempGrad)"
|
fill="url(#tempGrad)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Temperature curve line -->
|
<!-- Temperature curve line -->
|
||||||
<path
|
<path
|
||||||
d={buildCurvePath(hourly.temperature_2m, filteredIdx, minTemp, maxTemp)}
|
d={buildCurvePath(hourly.temperature_2m, dayIdx, minTemp, maxTemp)}
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="#d84315"
|
stroke="#d84315"
|
||||||
stroke-width="2.5"
|
stroke-width="2.5"
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
|
vector-effect="non-scaling-stroke"
|
||||||
/>
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
<!-- Temperature value labels -->
|
<!-- Temperature labels (positioned with % so they align with columns) -->
|
||||||
{#each filteredIdx as idx, i (idx)}
|
<div class="curve-labels">
|
||||||
|
{#each dayIdx as idx, i (idx)}
|
||||||
{@const t = hourly.temperature_2m[idx]}
|
{@const t = hourly.temperature_2m[idx]}
|
||||||
{@const rangeT = maxTemp - minTemp || 1}
|
{@const rangeT = maxTemp - minTemp || 1}
|
||||||
{@const y = CURVE_H - 20 - ((t - minTemp) / rangeT) * (CURVE_H - 40)}
|
{@const yPct =
|
||||||
<text
|
((1 - ((t ?? minTemp) - minTemp) / rangeT) * 0.7 + 0.05) * 100}
|
||||||
x={i * CELL_W + CELL_W / 2}
|
<span
|
||||||
y={y - 8}
|
class="curve-label"
|
||||||
text-anchor="middle"
|
style="left: {((i + 0.5) / numCols) * 100}%; top: calc({yPct}% - 18px)"
|
||||||
class="curve-temp-label"
|
|
||||||
fill="currentColor"
|
|
||||||
>
|
>
|
||||||
{t?.toFixed(0) ?? '-'}°
|
{t?.toFixed(0) ?? '-'}°
|
||||||
</text>
|
</span>
|
||||||
{/each}
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<!-- Weather icons overlaid on top -->
|
|
||||||
<div class="curve-icons" style="width: {totalWidth}px">
|
|
||||||
{#each filteredIdx as idx, i (idx)}
|
|
||||||
{@const wCode = hourly.weather_code[idx]}
|
|
||||||
{@const daytime = isDaytimeHour(dates[idx].getHours())}
|
|
||||||
<div class="curve-icon-cell" style="width: {CELL_W}px; left: {i * CELL_W}px">
|
|
||||||
<svg class="fill-foreground" width="20px" height="20px">
|
|
||||||
<use
|
|
||||||
xlink:href="/images/weather-icons/wi-{daytime
|
|
||||||
? 'day'
|
|
||||||
: 'night'}-{weatherCodes[wCode as keyof typeof weatherCodes] ??
|
|
||||||
'clear'}.svg#Layer_1"
|
|
||||||
></use>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Hourly Data Table -->
|
|
||||||
<table class="hourly-table">
|
|
||||||
<caption class="sr-only">Hourly weather details for {location.name}</caption>
|
|
||||||
<tbody>
|
|
||||||
<!-- Time row -->
|
|
||||||
<tr>
|
|
||||||
<th class="row-header" scope="row">
|
|
||||||
<svg class="fill-foreground inline-block" width="14px" height="14px">
|
|
||||||
<use xlink:href="/images/weather-icons/wi-time-4.svg#Layer_1"></use>
|
|
||||||
</svg>
|
|
||||||
</th>
|
|
||||||
{#each filteredIdx as idx (idx)}
|
|
||||||
{@const date = dates[idx]}
|
|
||||||
{@const isNow = isCurrentHour(date)}
|
|
||||||
{@const isMidnight = date.getHours() === 0}
|
|
||||||
<td
|
|
||||||
class="hour-cell time-cell {isNow ? 'now' : ''} {isMidnight ? 'midnight' : ''}"
|
|
||||||
data-date={date.getDate()}
|
|
||||||
>
|
|
||||||
{pad(date.getHours())}<sup class="time-sup">00</sup>
|
|
||||||
</td>
|
</td>
|
||||||
{/each}
|
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<!-- Precipitation bars row -->
|
<!-- Precipitation bars row -->
|
||||||
@@ -1022,16 +1034,11 @@
|
|||||||
<span class="row-unit">{params.precipitation_unit === 'mm' ? 'mm' : 'in'}</span>
|
<span class="row-unit">{params.precipitation_unit === 'mm' ? 'mm' : 'in'}</span>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
{#each filteredIdx as idx (idx)}
|
{#each dayIdx as idx (idx)}
|
||||||
{@const val = hourly.precipitation[idx]}
|
{@const val = hourly.precipitation[idx]}
|
||||||
{@const date = dates[idx]}
|
{@const date = dates[idx]}
|
||||||
{@const isNow = isCurrentHour(date)}
|
{@const isNow = isCurrentHour(date)}
|
||||||
{@const isMidnight = date.getHours() === 0}
|
<td class="hour-cell precip-cell {isNow ? 'now' : ''}">
|
||||||
<td
|
|
||||||
class="hour-cell precip-cell {isNow ? 'now' : ''} {isMidnight
|
|
||||||
? 'midnight'
|
|
||||||
: ''}"
|
|
||||||
>
|
|
||||||
<div class="precip-bar-container">
|
<div class="precip-bar-container">
|
||||||
{#if val > 0}
|
{#if val > 0}
|
||||||
<div
|
<div
|
||||||
@@ -1050,13 +1057,12 @@
|
|||||||
<th class="row-header" scope="row">
|
<th class="row-header" scope="row">
|
||||||
<span class="row-unit">%</span>
|
<span class="row-unit">%</span>
|
||||||
</th>
|
</th>
|
||||||
{#each filteredIdx as idx (idx)}
|
{#each dayIdx as idx (idx)}
|
||||||
{@const prob = hourly.precipitation_probability[idx]}
|
{@const prob = hourly.precipitation_probability[idx]}
|
||||||
{@const date = dates[idx]}
|
{@const date = dates[idx]}
|
||||||
{@const isNow = isCurrentHour(date)}
|
{@const isNow = isCurrentHour(date)}
|
||||||
{@const isMidnight = date.getHours() === 0}
|
|
||||||
<td
|
<td
|
||||||
class="hour-cell {isNow ? 'now' : ''} {isMidnight ? 'midnight' : ''}"
|
class="hour-cell {isNow ? 'now' : ''}"
|
||||||
style="background: {getPrecipProbBg(prob ?? 0)}; color: {getPrecipProbColor(
|
style="background: {getPrecipProbBg(prob ?? 0)}; color: {getPrecipProbColor(
|
||||||
prob ?? 0
|
prob ?? 0
|
||||||
)}"
|
)}"
|
||||||
@@ -1069,23 +1075,20 @@
|
|||||||
<!-- Precipitation type icons -->
|
<!-- Precipitation type icons -->
|
||||||
<tr>
|
<tr>
|
||||||
<th class="row-header" scope="row">
|
<th class="row-header" scope="row">
|
||||||
<svg class="fill-foreground inline-block" width="14px" height="14px">
|
<svg class="fill-foreground inline-block" width="16px" height="16px">
|
||||||
<use xlink:href="/images/weather-icons/wi-raindrop.svg#Layer_1"></use>
|
<use xlink:href="/images/weather-icons/wi-raindrop.svg#Layer_1"></use>
|
||||||
</svg>
|
</svg>
|
||||||
</th>
|
</th>
|
||||||
{#each filteredIdx as idx (idx)}
|
{#each dayIdx as idx (idx)}
|
||||||
{@const val = hourly.precipitation[idx]}
|
{@const val = hourly.precipitation[idx]}
|
||||||
{@const date = dates[idx]}
|
{@const date = dates[idx]}
|
||||||
{@const isNow = isCurrentHour(date)}
|
{@const isNow = isCurrentHour(date)}
|
||||||
{@const isMidnight = date.getHours() === 0}
|
<td class="hour-cell icon-cell {isNow ? 'now' : ''}">
|
||||||
<td
|
|
||||||
class="hour-cell icon-cell {isNow ? 'now' : ''} {isMidnight ? 'midnight' : ''}"
|
|
||||||
>
|
|
||||||
{#if val > 0}
|
{#if val > 0}
|
||||||
<svg
|
<svg
|
||||||
class="fill-foreground"
|
class="fill-foreground"
|
||||||
width="16px"
|
width="18px"
|
||||||
height="16px"
|
height="18px"
|
||||||
style="opacity: {Math.min(1, val / 3)}"
|
style="opacity: {Math.min(1, val / 3)}"
|
||||||
>
|
>
|
||||||
<use xlink:href="/images/weather-icons/wi-raindrop.svg#Layer_1"></use>
|
<use xlink:href="/images/weather-icons/wi-raindrop.svg#Layer_1"></use>
|
||||||
@@ -1095,25 +1098,21 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<!-- Wind Speed + Direction -->
|
<!-- Wind Direction Arrows -->
|
||||||
<tr>
|
<tr>
|
||||||
<th class="row-header" scope="row">
|
<th class="row-header" scope="row">
|
||||||
<svg class="fill-foreground inline-block" width="14px" height="14px">
|
<svg class="fill-foreground inline-block" width="16px" height="16px">
|
||||||
<use xlink:href="/images/weather-icons/wi-strong-wind.svg#Layer_1"></use>
|
<use xlink:href="/images/weather-icons/wi-strong-wind.svg#Layer_1"></use>
|
||||||
</svg>
|
</svg>
|
||||||
</th>
|
</th>
|
||||||
{#each filteredIdx as idx (idx)}
|
{#each dayIdx as idx (idx)}
|
||||||
{@const _wind = hourly.windspeed_10m[idx]}
|
|
||||||
{@const windDir = hourly.winddirection_10m[idx]}
|
{@const windDir = hourly.winddirection_10m[idx]}
|
||||||
{@const date = dates[idx]}
|
{@const date = dates[idx]}
|
||||||
{@const isNow = isCurrentHour(date)}
|
{@const isNow = isCurrentHour(date)}
|
||||||
{@const isMidnight = date.getHours() === 0}
|
<td class="hour-cell icon-cell {isNow ? 'now' : ''}">
|
||||||
<td
|
|
||||||
class="hour-cell wind-cell {isNow ? 'now' : ''} {isMidnight ? 'midnight' : ''}"
|
|
||||||
>
|
|
||||||
{#if windDir != null && !isNaN(windDir)}
|
{#if windDir != null && !isNaN(windDir)}
|
||||||
<div class="wind-arrow" style="transform: {getWindArrowRotation(windDir)}">
|
<div class="wind-arrow" style="transform: {getWindArrowRotation(windDir)}">
|
||||||
<svg class="fill-foreground" width="16px" height="16px">
|
<svg class="fill-foreground" width="20px" height="20px">
|
||||||
<use xlink:href="/images/weather-icons/wi-direction-down.svg#Layer_1"
|
<use xlink:href="/images/weather-icons/wi-direction-down.svg#Layer_1"
|
||||||
></use>
|
></use>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -1126,16 +1125,15 @@
|
|||||||
<!-- Apparent Temperature (Feels Like) -->
|
<!-- Apparent Temperature (Feels Like) -->
|
||||||
<tr>
|
<tr>
|
||||||
<th class="row-header" scope="row">
|
<th class="row-header" scope="row">
|
||||||
<svg class="fill-foreground inline-block" width="14px" height="14px">
|
<svg class="fill-foreground inline-block" width="16px" height="16px">
|
||||||
<use xlink:href="/images/weather-icons/wi-thermometer.svg#Layer_1"></use>
|
<use xlink:href="/images/weather-icons/wi-thermometer.svg#Layer_1"></use>
|
||||||
</svg>
|
</svg>
|
||||||
</th>
|
</th>
|
||||||
{#each filteredIdx as idx (idx)}
|
{#each dayIdx as idx (idx)}
|
||||||
{@const temp = hourly.apparent_temperature[idx]}
|
{@const temp = hourly.apparent_temperature[idx]}
|
||||||
{@const date = dates[idx]}
|
{@const date = dates[idx]}
|
||||||
{@const isNow = isCurrentHour(date)}
|
{@const isNow = isCurrentHour(date)}
|
||||||
{@const isMidnight = date.getHours() === 0}
|
<td class="hour-cell {isNow ? 'now' : ''}">
|
||||||
<td class="hour-cell {isNow ? 'now' : ''} {isMidnight ? 'midnight' : ''}">
|
|
||||||
{temp != null ? temp.toFixed(0) + '°' : '-'}
|
{temp != null ? temp.toFixed(0) + '°' : '-'}
|
||||||
</td>
|
</td>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -1144,15 +1142,16 @@
|
|||||||
<!-- Humidity -->
|
<!-- Humidity -->
|
||||||
<tr>
|
<tr>
|
||||||
<th class="row-header" scope="row">
|
<th class="row-header" scope="row">
|
||||||
<span class="row-unit">%</span>
|
<svg class="fill-foreground inline-block" width="16px" height="16px">
|
||||||
|
<use xlink:href="/images/weather-icons/wi-humidity.svg#Layer_1"></use>
|
||||||
|
</svg>
|
||||||
</th>
|
</th>
|
||||||
{#each filteredIdx as idx (idx)}
|
{#each dayIdx as idx (idx)}
|
||||||
{@const hum = hourly.relative_humidity_2m[idx]}
|
{@const hum = hourly.relative_humidity_2m[idx]}
|
||||||
{@const date = dates[idx]}
|
{@const date = dates[idx]}
|
||||||
{@const isNow = isCurrentHour(date)}
|
{@const isNow = isCurrentHour(date)}
|
||||||
{@const isMidnight = date.getHours() === 0}
|
|
||||||
<td
|
<td
|
||||||
class="hour-cell {isNow ? 'now' : ''} {isMidnight ? 'midnight' : ''}"
|
class="hour-cell {isNow ? 'now' : ''}"
|
||||||
style="background: {getHumidityBg(hum ?? 0)}"
|
style="background: {getHumidityBg(hum ?? 0)}"
|
||||||
>
|
>
|
||||||
{hum != null ? hum.toFixed(0) + '%' : '-'}
|
{hum != null ? hum.toFixed(0) + '%' : '-'}
|
||||||
@@ -1160,50 +1159,23 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<!-- Wind Direction Labels -->
|
|
||||||
<tr>
|
|
||||||
<th class="row-header" scope="row">
|
|
||||||
<svg class="fill-foreground inline-block" width="14px" height="14px">
|
|
||||||
<use xlink:href="/images/weather-icons/wi-wind-deg.svg#Layer_1"></use>
|
|
||||||
</svg>
|
|
||||||
</th>
|
|
||||||
{#each filteredIdx as idx (idx)}
|
|
||||||
{@const windDir = hourly.winddirection_10m[idx]}
|
|
||||||
{@const date = dates[idx]}
|
|
||||||
{@const isNow = isCurrentHour(date)}
|
|
||||||
{@const isMidnight = date.getHours() === 0}
|
|
||||||
<td
|
|
||||||
class="hour-cell icon-cell {isNow ? 'now' : ''} {isMidnight ? 'midnight' : ''}"
|
|
||||||
>
|
|
||||||
{#if windDir != null && !isNaN(windDir)}
|
|
||||||
<div style="transform: rotate({windDir}deg); display: inline-block">
|
|
||||||
<svg class="fill-foreground" width="18px" height="18px">
|
|
||||||
<use xlink:href="/images/weather-icons/wi-direction-down.svg#Layer_1"
|
|
||||||
></use>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
-
|
|
||||||
{/if}
|
|
||||||
</td>
|
|
||||||
{/each}
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<!-- Wind Speed values -->
|
<!-- Wind Speed values -->
|
||||||
<tr>
|
<tr>
|
||||||
<th class="row-header" scope="row">
|
<th class="row-header" scope="row">
|
||||||
<div class="row-header-stack">
|
<div class="row-header-stack">
|
||||||
<svg class="fill-foreground inline-block" width="14px" height="14px">
|
<svg class="fill-foreground inline-block" width="16px" height="16px">
|
||||||
<use xlink:href="/images/weather-icons/wi-strong-wind.svg#Layer_1"></use>
|
<use xlink:href="/images/weather-icons/wi-strong-wind.svg#Layer_1"></use>
|
||||||
</svg>
|
</svg>
|
||||||
|
<span class="row-unit"
|
||||||
|
>{params.wind_speed_unit === 'kmh' ? 'km/h' : params.wind_speed_unit}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
{#each filteredIdx as idx (idx)}
|
{#each dayIdx as idx (idx)}
|
||||||
{@const wind = hourly.windspeed_10m[idx]}
|
{@const wind = hourly.windspeed_10m[idx]}
|
||||||
{@const date = dates[idx]}
|
{@const date = dates[idx]}
|
||||||
{@const isNow = isCurrentHour(date)}
|
{@const isNow = isCurrentHour(date)}
|
||||||
{@const isMidnight = date.getHours() === 0}
|
<td class="hour-cell {isNow ? 'now' : ''}">
|
||||||
<td class="hour-cell {isNow ? 'now' : ''} {isMidnight ? 'midnight' : ''}">
|
|
||||||
{wind?.toFixed(0) ?? '-'}
|
{wind?.toFixed(0) ?? '-'}
|
||||||
</td>
|
</td>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -1212,16 +1184,15 @@
|
|||||||
<!-- Dew Point -->
|
<!-- Dew Point -->
|
||||||
<tr>
|
<tr>
|
||||||
<th class="row-header" scope="row">
|
<th class="row-header" scope="row">
|
||||||
<svg class="fill-foreground inline-block" width="14px" height="14px">
|
<svg class="fill-foreground inline-block" width="16px" height="16px">
|
||||||
<use xlink:href="/images/weather-icons/wi-raindrops.svg#Layer_1"></use>
|
<use xlink:href="/images/weather-icons/wi-raindrops.svg#Layer_1"></use>
|
||||||
</svg>
|
</svg>
|
||||||
</th>
|
</th>
|
||||||
{#each filteredIdx as idx (idx)}
|
{#each dayIdx as idx (idx)}
|
||||||
{@const dp = hourly.dew_point_2m[idx]}
|
{@const dp = hourly.dew_point_2m[idx]}
|
||||||
{@const date = dates[idx]}
|
{@const date = dates[idx]}
|
||||||
{@const isNow = isCurrentHour(date)}
|
{@const isNow = isCurrentHour(date)}
|
||||||
{@const isMidnight = date.getHours() === 0}
|
<td class="hour-cell {isNow ? 'now' : ''}">
|
||||||
<td class="hour-cell {isNow ? 'now' : ''} {isMidnight ? 'midnight' : ''}">
|
|
||||||
{dp != null ? dp.toFixed(0) + '°' : '-'}
|
{dp != null ? dp.toFixed(0) + '°' : '-'}
|
||||||
</td>
|
</td>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -1229,7 +1200,7 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Sunrise / Sunset Info -->
|
<!-- Sunrise / Sunset Info -->
|
||||||
@@ -1520,86 +1491,25 @@
|
|||||||
left: 22px;
|
left: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ═══ Horizontal Scroll Container ═══════════════════════════════════════════ */
|
/* ═══ Hourly Table Container ═══════════════════════════════════════════════ */
|
||||||
|
|
||||||
.hourly-scroll {
|
.hourly-container {
|
||||||
overflow-x: auto;
|
|
||||||
overflow-y: hidden;
|
|
||||||
scrollbar-width: thin;
|
|
||||||
margin-left: -1.25rem;
|
|
||||||
margin-right: -1.25rem;
|
|
||||||
border: 1px solid hsl(var(--border));
|
border: 1px solid hsl(var(--border));
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.hourly-scroll {
|
|
||||||
margin-left: 0;
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.hourly-inner {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ═══ Temperature Curve Area ═══════════════════════════════════════════════ */
|
|
||||||
|
|
||||||
.curve-area {
|
|
||||||
display: flex;
|
|
||||||
border-bottom: 2px solid hsl(var(--border));
|
|
||||||
background: hsl(var(--card) / 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.curve-row-header {
|
|
||||||
position: sticky;
|
|
||||||
left: 0;
|
|
||||||
z-index: 10;
|
|
||||||
min-width: 90px;
|
|
||||||
max-width: 90px;
|
|
||||||
background: hsl(var(--background));
|
|
||||||
border-right: 2px solid hsl(var(--border));
|
|
||||||
}
|
|
||||||
|
|
||||||
.curve-container {
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.curve-svg {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.curve-temp-label {
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.curve-icons {
|
|
||||||
position: absolute;
|
|
||||||
top: 2px;
|
|
||||||
left: 0;
|
|
||||||
display: flex;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.curve-icon-cell {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ═══ Hourly Data Table ═══════════════════════════════════════════════════ */
|
/* ═══ Hourly Data Table ═══════════════════════════════════════════════════ */
|
||||||
|
|
||||||
.hourly-table {
|
.hourly-table {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
font-size: 11px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
table-layout: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hourly-table .col-header {
|
||||||
|
width: 64px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hourly-table tr {
|
.hourly-table tr {
|
||||||
@@ -1611,18 +1521,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.row-header {
|
.row-header {
|
||||||
position: sticky;
|
width: 64px;
|
||||||
left: 0;
|
padding: 4px 4px;
|
||||||
z-index: 10;
|
|
||||||
min-width: 90px;
|
|
||||||
max-width: 90px;
|
|
||||||
padding: 3px 6px;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
background: hsl(var(--background));
|
background: hsl(var(--background));
|
||||||
border-right: 2px solid hsl(var(--border));
|
border-right: 2px solid hsl(var(--border));
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.row-header-stack {
|
.row-header-stack {
|
||||||
@@ -1630,7 +1537,13 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
line-height: 1.1;
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-label {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
}
|
}
|
||||||
|
|
||||||
.row-unit {
|
.row-unit {
|
||||||
@@ -1639,17 +1552,24 @@
|
|||||||
color: hsl(var(--muted-foreground));
|
color: hsl(var(--muted-foreground));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ═══ Hour Cells ═══════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
.hour-cell {
|
.hour-cell {
|
||||||
min-width: 38px;
|
padding: 6px 2px;
|
||||||
max-width: 38px;
|
|
||||||
padding: 3px 1px;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 11px;
|
font-size: 13px;
|
||||||
border-right: 1px solid hsl(var(--border) / 0.4);
|
font-weight: 500;
|
||||||
|
border-right: 1px solid hsl(var(--border) / 0.3);
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hour-cell.midnight {
|
.interval-3h .hour-cell {
|
||||||
border-left: 2px solid hsl(var(--primary) / 0.5);
|
font-size: 15px;
|
||||||
|
padding: 8px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hour-cell:last-child {
|
||||||
|
border-right: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hour-cell.now {
|
.hour-cell.now {
|
||||||
@@ -1659,32 +1579,111 @@
|
|||||||
|
|
||||||
.time-cell {
|
.time-cell {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: 10px;
|
|
||||||
background: hsl(var(--muted) / 0.3);
|
background: hsl(var(--muted) / 0.3);
|
||||||
|
padding: 5px 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.interval-3h .time-cell {
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 6px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.interval-1h .time-cell {
|
||||||
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-sup {
|
.time-sup {
|
||||||
font-size: 7px;
|
font-size: 8px;
|
||||||
vertical-align: super;
|
vertical-align: super;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ═══ Weather Icon Row ═══════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
.icon-row {
|
||||||
|
background: hsl(var(--muted) / 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-icon-cell {
|
||||||
|
padding: 6px 2px;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
line-height: 0;
|
||||||
|
border-right: 1px solid hsl(var(--border) / 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-icon-cell:last-child {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.interval-3h .weather-icon-cell {
|
||||||
|
padding: 10px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══ Temperature Curve ═══════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
.curve-row {
|
||||||
|
border-top: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.curve-header {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.curve-cell {
|
||||||
|
padding: 0 !important;
|
||||||
|
border-right: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.curve-container {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.curve-svg {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.curve-labels {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.curve-label {
|
||||||
|
position: absolute;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.interval-3h .curve-label {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
/* ═══ Precipitation Bar ═══════════════════════════════════════════════════ */
|
/* ═══ Precipitation Bar ═══════════════════════════════════════════════════ */
|
||||||
|
|
||||||
.precip-cell {
|
.precip-cell {
|
||||||
vertical-align: bottom;
|
vertical-align: bottom;
|
||||||
padding: 1px;
|
padding: 2px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.precip-bar-container {
|
.precip-bar-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 22px;
|
height: 28px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.interval-3h .precip-bar-container {
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
.precip-bar {
|
.precip-bar {
|
||||||
width: 60%;
|
width: 55%;
|
||||||
min-height: 2px;
|
min-height: 2px;
|
||||||
background: linear-gradient(to top, rgba(30, 136, 229, 0.5), rgba(30, 136, 229, 0.9));
|
background: linear-gradient(to top, rgba(30, 136, 229, 0.5), rgba(30, 136, 229, 0.9));
|
||||||
border-radius: 2px 2px 0 0;
|
border-radius: 2px 2px 0 0;
|
||||||
@@ -1692,16 +1691,22 @@
|
|||||||
|
|
||||||
.precip-val {
|
.precip-val {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 9px;
|
font-size: 10px;
|
||||||
line-height: 1;
|
line-height: 1.2;
|
||||||
color: hsl(var(--muted-foreground));
|
color: hsl(var(--muted-foreground));
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
margin-top: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.interval-3h .precip-val {
|
||||||
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-cell {
|
.icon-cell {
|
||||||
padding: 2px;
|
padding: 4px 2px;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
line-height: 0;
|
line-height: 0;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-cell > div,
|
.icon-cell > div,
|
||||||
@@ -1709,12 +1714,7 @@
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ═══ Wind Cell ═══════════════════════════════════════════════════════════ */
|
/* ═══ Wind Arrow ═══════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
.wind-cell {
|
|
||||||
vertical-align: middle;
|
|
||||||
line-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wind-arrow {
|
.wind-arrow {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@@ -1817,5 +1817,25 @@
|
|||||||
.card-temp-min {
|
.card-temp-min {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hourly-table .col-header {
|
||||||
|
width: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-header {
|
||||||
|
width: 48px;
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 3px 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hour-cell {
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 4px 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.interval-3h .hour-cell {
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 6px 2px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user