Add DashboardCard component and redesign peak traffic chart
- Create reusable DashboardCard component with title and content slot - Redesign PeakTrafficChart to match Figma design: - Vertical bar chart showing peak hours by day of week - Y-axis: hours (0:00 to 21:00) - X-axis: days (Mon-Sun) - Two data series: "This month" (solid) and "Last month" (hatched) - Diagonal stripe pattern for last month bars using SVG - Legend at bottom 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from "svelte";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
children: Snippet;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
let { title, children, class: className }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={cn(
|
||||
"overflow-hidden rounded-lg border border-border bg-card shadow-card",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div class="border-b border-border px-5 py-3">
|
||||
<h3 class="text-sm font-semibold text-foreground">{title}</h3>
|
||||
</div>
|
||||
|
||||
<div class="p-5">
|
||||
{@render children()}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,3 +1,4 @@
|
||||
export { default as DashboardCard } from "./dashboard-card.svelte";
|
||||
export { default as FrequentFilters } from "./frequent-filters.svelte";
|
||||
export { default as MetricCard } from "./metric-card.svelte";
|
||||
export { default as PeakTrafficChart } from "./peak-traffic-chart.svelte";
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils.js";
|
||||
import DashboardCard from "./dashboard-card.svelte";
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
@@ -7,80 +8,102 @@ interface Props {
|
||||
|
||||
let { class: className }: Props = $props();
|
||||
|
||||
const hours = [
|
||||
"0:00",
|
||||
"3:00",
|
||||
"6:00",
|
||||
"9:00",
|
||||
"12:00",
|
||||
"15:00",
|
||||
"18:00",
|
||||
"21:00",
|
||||
];
|
||||
const hours = ["0:00", "6:00", "12:00", "15:00", "18:00", "21:00"];
|
||||
const days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
|
||||
|
||||
// Traffic intensity data (0-100)
|
||||
const heatmapData = [
|
||||
[20, 35, 45, 60, 75, 55, 30],
|
||||
[15, 25, 40, 55, 65, 45, 25],
|
||||
[25, 40, 55, 70, 80, 60, 35],
|
||||
[35, 50, 65, 80, 90, 70, 45],
|
||||
[40, 55, 70, 85, 95, 75, 50],
|
||||
[45, 60, 75, 90, 85, 70, 45],
|
||||
[50, 65, 80, 75, 70, 55, 40],
|
||||
[35, 50, 60, 55, 50, 40, 30],
|
||||
// Traffic data: [startHour, endHour] for each day (0-24 scale)
|
||||
// This month data (solid bars)
|
||||
const thisMonthData = [
|
||||
{ start: 6, end: 18 },
|
||||
{ start: 9, end: 15 },
|
||||
{ start: 12, end: 18 },
|
||||
{ start: 15, end: 21 },
|
||||
{ start: 12, end: 18 },
|
||||
{ start: 3, end: 12 },
|
||||
{ start: 9, end: 18 },
|
||||
];
|
||||
|
||||
function getCellOpacity(value: number): number {
|
||||
return 0.15 + (value / 100) * 0.85;
|
||||
// Last month data (hatched bars)
|
||||
const lastMonthData = [
|
||||
{ start: 3, end: 9 },
|
||||
{ start: 12, end: 18 },
|
||||
{ start: 6, end: 12 },
|
||||
{ start: 6, end: 12 },
|
||||
{ start: 6, end: 12 },
|
||||
{ start: 9, end: 15 },
|
||||
{ start: 15, end: 21 },
|
||||
];
|
||||
|
||||
// Convert hour to percentage position (0:00 = 0%, 24:00 = 100%)
|
||||
function hourToPercent(hour: number): number {
|
||||
return (hour / 24) * 100;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={cn(
|
||||
"overflow-hidden rounded-lg border border-border bg-card shadow-card",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div class="border-b border-border px-5 py-3">
|
||||
<h3 class="text-sm font-semibold text-foreground">Peak Traffic Hours</h3>
|
||||
</div>
|
||||
|
||||
<div class="p-5">
|
||||
<div class="flex gap-3">
|
||||
<DashboardCard title="Peak Traffic Hours" class={className}>
|
||||
<div class="relative">
|
||||
<!-- Chart container -->
|
||||
<div class="flex">
|
||||
<!-- Y-axis labels -->
|
||||
<div class="flex flex-col justify-between pr-1">
|
||||
<div class="flex w-10 flex-col justify-between pr-2" style="height: 210px">
|
||||
{#each hours as hour}
|
||||
<div class="flex h-6 items-center">
|
||||
<span class="text-[11px] tabular-nums text-muted-foreground">{hour}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Grid -->
|
||||
<div class="flex-1">
|
||||
<div class="grid grid-cols-7 gap-1">
|
||||
{#each hours as _, hourIndex}
|
||||
{#each days as _, dayIndex}
|
||||
{@const value = heatmapData[hourIndex][dayIndex]}
|
||||
<div
|
||||
class="group relative h-6 rounded transition-all duration-150 hover:ring-2 hover:ring-foreground/20"
|
||||
style="background-color: oklch(0.205 0.006 285 / {getCellOpacity(value)})"
|
||||
title="{days[dayIndex]} {hours[hourIndex]}: {value}% traffic"
|
||||
>
|
||||
<!-- Tooltip -->
|
||||
<div
|
||||
class="pointer-events-none absolute -top-8 left-1/2 z-10 -translate-x-1/2 whitespace-nowrap rounded bg-foreground px-2 py-1 text-[10px] font-medium text-background opacity-0 shadow-lg transition-opacity group-hover:opacity-100"
|
||||
>
|
||||
{value}%
|
||||
</div>
|
||||
</div>
|
||||
<!-- Chart area -->
|
||||
<div class="relative flex-1">
|
||||
<!-- Grid lines -->
|
||||
<div class="absolute inset-0 flex flex-col justify-between" style="height: 210px">
|
||||
{#each hours as _}
|
||||
<div class="h-px w-full bg-border"></div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Bars container -->
|
||||
<div class="relative grid grid-cols-7 gap-4 px-2" style="height: 210px">
|
||||
{#each days as _, dayIndex}
|
||||
{@const thisMonth = thisMonthData[dayIndex]}
|
||||
{@const lastMonth = lastMonthData[dayIndex]}
|
||||
<div class="relative">
|
||||
<!-- This month bar (solid) -->
|
||||
<div
|
||||
class="absolute left-0 w-3 rounded-sm bg-muted-foreground/60"
|
||||
style="
|
||||
top: {hourToPercent(thisMonth.start)}%;
|
||||
height: {hourToPercent(thisMonth.end - thisMonth.start)}%;
|
||||
"
|
||||
></div>
|
||||
|
||||
<!-- Last month bar (hatched) -->
|
||||
<div
|
||||
class="absolute left-4 w-3 overflow-hidden rounded-sm"
|
||||
style="
|
||||
top: {hourToPercent(lastMonth.start)}%;
|
||||
height: {hourToPercent(lastMonth.end - lastMonth.start)}%;
|
||||
"
|
||||
>
|
||||
<svg class="h-full w-full" preserveAspectRatio="none">
|
||||
<defs>
|
||||
<pattern
|
||||
id="diagonal-{dayIndex}"
|
||||
patternUnits="userSpaceOnUse"
|
||||
width="4"
|
||||
height="4"
|
||||
patternTransform="rotate(45)"
|
||||
>
|
||||
<line x1="0" y1="0" x2="0" y2="4" stroke="currentColor" stroke-width="1.5" class="text-muted-foreground/50" />
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="url(#diagonal-{dayIndex})" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- X-axis labels -->
|
||||
<div class="mt-2 grid grid-cols-7 gap-1">
|
||||
<div class="mt-2 grid grid-cols-7 gap-4 px-2">
|
||||
{#each days as day}
|
||||
<div class="text-center text-[11px] text-muted-foreground">{day}</div>
|
||||
{/each}
|
||||
@@ -89,18 +112,30 @@ function getCellOpacity(value: number): number {
|
||||
</div>
|
||||
|
||||
<!-- Legend -->
|
||||
<div class="mt-5 flex items-center justify-center gap-4">
|
||||
<div class="mt-4 flex items-center justify-center gap-6">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex items-center gap-0.5">
|
||||
{#each [0.2, 0.4, 0.6, 0.8, 1] as opacity}
|
||||
<div
|
||||
class="h-2 w-4 first:rounded-l last:rounded-r"
|
||||
style="background-color: oklch(0.205 0.006 285 / {opacity})"
|
||||
></div>
|
||||
{/each}
|
||||
<div class="h-3 w-3 rounded-sm bg-muted-foreground/60"></div>
|
||||
<span class="text-[11px] text-muted-foreground">This month</span>
|
||||
</div>
|
||||
<span class="text-[11px] text-muted-foreground">Traffic intensity</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="relative h-3 w-3 overflow-hidden rounded-sm">
|
||||
<svg class="h-full w-full" preserveAspectRatio="none">
|
||||
<defs>
|
||||
<pattern
|
||||
id="diagonal-legend"
|
||||
patternUnits="userSpaceOnUse"
|
||||
width="4"
|
||||
height="4"
|
||||
patternTransform="rotate(45)"
|
||||
>
|
||||
<line x1="0" y1="0" x2="0" y2="4" stroke="currentColor" stroke-width="1.5" class="text-muted-foreground/50" />
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="url(#diagonal-legend)" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-[11px] text-muted-foreground">Last month</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DashboardCard>
|
||||
|
||||
Reference in New Issue
Block a user