a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+export {
+ Empty,
+ EmptyHeader,
+ EmptyTitle,
+ EmptyDescription,
+ EmptyContent,
+ EmptyMedia,
+}
diff --git a/src/components/widgets/group-card-empty.tsx b/src/components/widgets/group-card-empty.tsx
new file mode 100644
index 0000000..17e5e9d
--- /dev/null
+++ b/src/components/widgets/group-card-empty.tsx
@@ -0,0 +1,26 @@
+import {
+ Empty,
+ EmptyDescription,
+ EmptyHeader,
+ EmptyTitle,
+} from "@/components/ui/empty";
+
+/**
+ * Empty body for a dashboard group card. `min-h` is the footprint floor so an
+ * empty card holds a populated card's height even when its grid row has no
+ * taller sibling to stretch against. It fills and centers within the card's
+ * content region (the parent `CardContent` carries `flex-1`); no padding of
+ * its own, so nothing biases the centered message downward.
+ */
+export function GroupCardEmpty() {
+ return (
+
+
+ No data
+
+ No metrics with data for this period.
+
+
+
+ );
+}
diff --git a/src/components/widgets/metric-views/chart-empty.tsx b/src/components/widgets/metric-views/chart-empty.tsx
new file mode 100644
index 0000000..a139b0e
--- /dev/null
+++ b/src/components/widgets/metric-views/chart-empty.tsx
@@ -0,0 +1,24 @@
+import { Empty, EmptyDescription, EmptyHeader } from "@/components/ui/empty";
+import { cn } from "@/lib/utils";
+
+/**
+ * Placeholder for a chart with no data — centers the message in the plot area
+ * so an empty chart keeps its neighbours' footprint instead of collapsing to
+ * a line of text. No frame of its own: the enclosing card is the border.
+ * `className` carries the plot height so it matches the chart it stands in for.
+ */
+export function ChartEmpty({
+ message,
+ className,
+}: {
+ message: string;
+ className?: string;
+}) {
+ return (
+
+
+ {message}
+
+
+ );
+}
diff --git a/src/components/widgets/metric-views/collection-drilldown.test.tsx b/src/components/widgets/metric-views/collection-drilldown.test.tsx
index a489521..d0b26cb 100644
--- a/src/components/widgets/metric-views/collection-drilldown.test.tsx
+++ b/src/components/widgets/metric-views/collection-drilldown.test.tsx
@@ -114,9 +114,9 @@ describe("CollectionDrilldown", () => {
entityId="alice@example.com"
/>,
);
- // Histograms live in a single labeled "Distributions" card, shaded vs the
- // peer median (the subtitle proves the diverging render, not a bare chart).
- expect(screen.getByText("Distributions")).toBeInTheDocument();
+ // Each histogram is its own card in the grid (no wrapping "Distributions"
+ // card), shaded vs the peer median (the subtitle proves the diverging
+ // render, not a bare chart).
expect(screen.getByText(/vs peer median/)).toBeInTheDocument();
});
diff --git a/src/components/widgets/metric-views/collection-drilldown.tsx b/src/components/widgets/metric-views/collection-drilldown.tsx
index 2613f27..43f3842 100644
--- a/src/components/widgets/metric-views/collection-drilldown.tsx
+++ b/src/components/widgets/metric-views/collection-drilldown.tsx
@@ -1,10 +1,4 @@
import { ComingSoon } from "@/components/widgets/coming-soon";
-import {
- Card,
- CardContent,
- CardHeader,
- CardTitle,
-} from "@/components/ui/card";
import { Spinner } from "@/components/ui/spinner";
import { MetricBreakdown } from "@/components/widgets/metric-views/metric-breakdown";
import { MetricHistogram } from "@/components/widgets/metric-views/metric-histogram";
@@ -12,7 +6,7 @@ import { MetricSummaryCard } from "@/components/widgets/metric-views/metric-summ
import { MetricTrend } from "@/components/widgets/metric-views/metric-trend";
import { PeerStory } from "@/components/widgets/metric-views/peer-story";
import type { DrilldownBlock, MetricGroup } from "@/lib/insight/groups";
-import type { NormalizedMetricResult } from "@/lib/metrics/collection";
+import { forEntity, type NormalizedMetricResult } from "@/lib/metrics/collection";
import { buildPeerStoryEntries } from "@/lib/metrics/peer-story";
import type { PeerCohortLabel } from "@/lib/peers";
import type { MetricCollectionResult } from "@/queries/metric-results";
@@ -124,9 +118,18 @@ export function CollectionDrilldown({
!isSummaryBlock(block) &&
blockMetrics(block, data.byKey).length > 0,
);
- const distributionMetrics = def.drilldown
+ // Populated distributions lead; those with no events for this entity in the
+ // period sort to the end so the section doesn't open on an empty placeholder.
+ // Stable partition keeps declared order within each group.
+ const declaredDistributions = def.drilldown
.filter((block) => block.view === "histogram")
.flatMap((block) => blockMetrics(block, data.byKey));
+ const hasDistribution = (metric: NormalizedMetricResult) =>
+ (forEntity(metric, entityId).histogram[0]?.bins?.length ?? 0) > 0;
+ const distributionMetrics = [
+ ...declaredDistributions.filter(hasDistribution),
+ ...declaredDistributions.filter((metric) => !hasDistribution(metric)),
+ ];
return (
{distributionMetrics.length > 0 ? (
- // One labeled card holds every distribution; the charts inside are
- // un-carded so there are no nested borders and "distribution" isn't
- // repeated per chart.
-
-
-
- Distributions
-
-
- 1 && "lg:grid-cols-2",
- )}
- >
- {distributionMetrics.map((metric) => (
-
- ))}
-
-
+ // Each histogram is its own card, dropped straight into the grid like
+ // the chart blocks above — no wrapping "Distributions" card.
+
1 && "lg:grid-cols-2",
+ )}
+ >
+ {distributionMetrics.map((metric) => (
+
+ ))}
+
) : null}
);
diff --git a/src/components/widgets/metric-views/metric-breakdown.test.tsx b/src/components/widgets/metric-views/metric-breakdown.test.tsx
index 717e2ec..d7fa2d2 100644
--- a/src/components/widgets/metric-views/metric-breakdown.test.tsx
+++ b/src/components/widgets/metric-views/metric-breakdown.test.tsx
@@ -61,6 +61,6 @@ describe("MetricBreakdown", () => {
entityId="me@x.com"
/>,
);
- expect(screen.getByText("No composition data yet.")).toBeInTheDocument();
+ expect(screen.getByText("No composition data yet")).toBeInTheDocument();
});
});
diff --git a/src/components/widgets/metric-views/metric-breakdown.tsx b/src/components/widgets/metric-views/metric-breakdown.tsx
index 6778aa4..04d6f6f 100644
--- a/src/components/widgets/metric-views/metric-breakdown.tsx
+++ b/src/components/widgets/metric-views/metric-breakdown.tsx
@@ -5,6 +5,7 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
+import { ChartEmpty } from "@/components/widgets/metric-views/chart-empty";
import {
dimensionColorSeed,
dimensionLabel,
@@ -38,11 +39,13 @@ export function MetricBreakdown({ metric, entityId }: MetricBreakdownProps) {
if (rows.length === 0) {
return (
-