Skip to content

Commit e553e64

Browse files
committed
feat chart and dashboard providers inside library, bumbed version, updated example app and sample code on landing page
1 parent 55bcdf3 commit e553e64

13 files changed

Lines changed: 1193 additions & 20 deletions

packages/components/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "graph-italia-components",
33
"description": "A simple React component library.",
4-
"version": "0.3.23",
4+
"version": "0.3.24",
55
"keywords": [
66
"react",
77
"typescript",
@@ -64,6 +64,7 @@
6464
"react": "^19.1.0",
6565
"react-dom": "^19.1.0",
6666
"react-error-boundary": "^6.0.0",
67+
"react-grid-layout": "^2.2.2",
6768
"react-markdown": "^10.1.0",
6869
"remark-gfm": "^4.0.1"
6970
}
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import React, { useMemo } from "react";
2+
import { Responsive, WidthProvider } from "react-grid-layout/legacy";
3+
import type { FieldDataType } from "../types";
4+
import RenderChart from "./RenderChart";
5+
6+
// ── Types ─────────────────────────────────────────────────────────────────────
7+
8+
export interface DashboardSlot {
9+
settings: {
10+
i: string;
11+
x: number;
12+
y: number;
13+
/** Span bucket: 1 = narrow (~1/3), 2 = medium (~2/3), 3 = full width */
14+
w: number;
15+
/** Row height multiplier */
16+
h: number;
17+
};
18+
chart: FieldDataType;
19+
}
20+
21+
export interface DashboardData {
22+
id?: string;
23+
name?: string;
24+
description?: string;
25+
publish?: boolean;
26+
slots: DashboardSlot[];
27+
}
28+
29+
export interface RenderDashboardProps {
30+
data: DashboardData;
31+
/** Base row height in px. Defaults to 380. */
32+
rowHeight?: number;
33+
/** Gap between slots in px. Defaults to 16. */
34+
margin?: number;
35+
/** Show dashboard name and description heading. Defaults to true. */
36+
showHeading?: boolean;
37+
}
38+
39+
// ── Grid config ───────────────────────────────────────────────────────────────
40+
41+
const TOTAL_COLS = 12;
42+
const ALL_COLS = { lg: 12, md: 12, sm: 12, xs: 12, xxs: 12 };
43+
const BREAKPOINTS = { lg: 1200, md: 768, sm: 480, xs: 320, xxs: 0 };
44+
45+
const SPAN_UNITS: Record<string, number[]> = {
46+
lg: [4, 8, 12],
47+
md: [6, 12, 12],
48+
sm: [12, 12, 12],
49+
xs: [12, 12, 12],
50+
xxs: [12, 12, 12],
51+
};
52+
53+
function toGridW(span: number, bp: string): number {
54+
return (SPAN_UNITS[bp] ?? SPAN_UNITS.sm)[Math.min(span, 3) - 1] ?? TOTAL_COLS;
55+
}
56+
57+
type TLayoutItem = { i: string; x: number; y: number; w: number; h: number };
58+
59+
function buildLayouts(items: TLayoutItem[]) {
60+
const lgRgl = items.map((item) => ({
61+
i: item.i,
62+
x: item.x,
63+
y: item.y,
64+
w: toGridW(item.w, "lg"),
65+
h: item.h,
66+
}));
67+
68+
function packBp(bp: string) {
69+
const sorted = [...items].sort((a, b) =>
70+
a.y !== b.y ? a.y - b.y : a.x - b.x
71+
);
72+
let x = 0, y = 0, rowMaxH = 1;
73+
return sorted.map((item) => {
74+
const w = toGridW(item.w, bp);
75+
if (x + w > TOTAL_COLS) {
76+
x = 0;
77+
y += rowMaxH;
78+
rowMaxH = item.h;
79+
} else {
80+
rowMaxH = Math.max(rowMaxH, item.h);
81+
}
82+
const out = { i: item.i, x, y, w, h: item.h };
83+
x += w;
84+
return out;
85+
});
86+
}
87+
88+
return {
89+
lg: lgRgl,
90+
md: packBp("md"),
91+
sm: packBp("sm"),
92+
xs: packBp("xs"),
93+
xxs: packBp("xxs"),
94+
};
95+
}
96+
97+
const ResponsiveGrid = WidthProvider(Responsive);
98+
99+
// ── Component ─────────────────────────────────────────────────────────────────
100+
101+
export function RenderDashboard({
102+
data,
103+
rowHeight = 380,
104+
margin = 16,
105+
showHeading = true,
106+
}: RenderDashboardProps) {
107+
const { layouts, items, chartMap } = useMemo(() => {
108+
const items: TLayoutItem[] = data.slots.map(({ settings }) => ({
109+
i: settings.i,
110+
x: settings.x,
111+
y: settings.y,
112+
w: settings.w,
113+
h: settings.h,
114+
}));
115+
const chartMap = data.slots.reduce<Record<string, FieldDataType>>(
116+
(acc, { settings, chart }) => { acc[settings.i] = chart; return acc; },
117+
{}
118+
);
119+
return { layouts: buildLayouts(items), items, chartMap };
120+
}, [data.slots]);
121+
122+
return (
123+
<div>
124+
{showHeading && data.name && (
125+
<h2
126+
style={{
127+
margin: `0 0 ${margin / 2}px`,
128+
fontSize: "1.5rem",
129+
fontWeight: 700,
130+
lineHeight: 1.3,
131+
}}
132+
>
133+
{data.name}
134+
</h2>
135+
)}
136+
{showHeading && data.description && (
137+
<p
138+
style={{
139+
margin: `0 0 ${margin}px`,
140+
fontSize: "1rem",
141+
opacity: 0.7,
142+
}}
143+
>
144+
{data.description}
145+
</p>
146+
)}
147+
148+
<ResponsiveGrid
149+
layouts={layouts}
150+
breakpoints={BREAKPOINTS}
151+
cols={ALL_COLS}
152+
rowHeight={rowHeight}
153+
margin={[margin, margin]}
154+
isDraggable={false}
155+
isResizable={false}
156+
>
157+
{items.map((item) => {
158+
const chart = chartMap[item.i] as FieldDataType;
159+
const chartHeight = rowHeight * item.h + margin * (item.h - 1);
160+
161+
return (
162+
<div
163+
key={item.i}
164+
style={{
165+
overflow: "hidden",
166+
borderRadius: 8,
167+
border: "1px solid rgba(128,128,128,0.15)",
168+
boxShadow: "0 1px 3px rgba(0,0,0,0.06)",
169+
}}
170+
>
171+
{chart && (
172+
<RenderChart
173+
{...chart}
174+
rowHeight={chartHeight}
175+
hFactor={1}
176+
/>
177+
)}
178+
</div>
179+
);
180+
})}
181+
</ResponsiveGrid>
182+
</div>
183+
);
184+
}
185+
186+
export default RenderDashboard;
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import React, { useEffect, useRef, useState } from "react";
2+
import type { FieldDataType } from "../types";
3+
import RenderChart from "./RenderChart";
4+
5+
// ── Types ─────────────────────────────────────────────────────────────────────
6+
7+
export interface DashboardSlot {
8+
settings: {
9+
i: string;
10+
x: number;
11+
y: number;
12+
/** Span bucket: 1 = narrow (~1/3), 2 = medium (~2/3), 3 = full width */
13+
w: number;
14+
/** Row height multiplier */
15+
h: number;
16+
};
17+
chart: FieldDataType;
18+
}
19+
20+
export interface DashboardData {
21+
id?: string;
22+
name?: string;
23+
description?: string;
24+
publish?: boolean;
25+
slots: DashboardSlot[];
26+
}
27+
28+
export interface RenderDashboardProps {
29+
data: DashboardData;
30+
/** Base row height in px. Defaults to 380. */
31+
rowHeight?: number;
32+
/** Gap between slots in px. Defaults to 16. */
33+
margin?: number;
34+
/** Show dashboard name and description heading. Defaults to true. */
35+
showHeading?: boolean;
36+
}
37+
38+
// ── Responsive breakpoints ────────────────────────────────────────────────────
39+
40+
// Mirrors EmbedDashboardPage SPAN_UNITS: w=1 → narrow, w=2 → medium, w=3 → full
41+
const SPAN_UNITS: Record<string, number[]> = {
42+
lg: [4, 8, 12], // >= 1200
43+
md: [6, 12, 12], // >= 768
44+
sm: [12, 12, 12], // < 768
45+
};
46+
47+
function getBreakpoint(width: number): string {
48+
if (width >= 1200) return "lg";
49+
if (width >= 768) return "md";
50+
return "sm";
51+
}
52+
53+
function toColSpan(w: number, bp: string): number {
54+
return (SPAN_UNITS[bp] ?? SPAN_UNITS.sm)[Math.min(w, 3) - 1] ?? 12;
55+
}
56+
57+
// ── Component ─────────────────────────────────────────────────────────────────
58+
59+
/**
60+
* Renders a dashboard from pre-fetched data.
61+
* Slots are arranged in a 12-column responsive grid that mirrors
62+
* the EmbedDashboardPage layout logic.
63+
*/
64+
export function RenderGridDashboard({
65+
data,
66+
rowHeight = 380,
67+
margin = 16,
68+
showHeading = true,
69+
}: RenderDashboardProps) {
70+
const containerRef = useRef<HTMLDivElement>(null);
71+
const [containerWidth, setContainerWidth] = useState<number>(1200);
72+
73+
useEffect(() => {
74+
const el = containerRef.current;
75+
if (!el) return;
76+
setContainerWidth(el.clientWidth || 1200);
77+
const ro = new ResizeObserver(([entry]) => {
78+
setContainerWidth(entry.contentRect.width);
79+
});
80+
ro.observe(el);
81+
return () => ro.disconnect();
82+
}, []);
83+
84+
const bp = getBreakpoint(containerWidth);
85+
86+
// Sort slots by reading order (top → bottom, left → right)
87+
const sorted = [...data.slots].sort((a, b) =>
88+
a.settings.y !== b.settings.y
89+
? a.settings.y - b.settings.y
90+
: a.settings.x - b.settings.x
91+
);
92+
93+
return (
94+
<div ref={containerRef}>
95+
{showHeading && data.name && (
96+
<h2
97+
style={{
98+
margin: `0 0 ${margin / 2}px`,
99+
fontSize: "1.5rem",
100+
fontWeight: 700,
101+
lineHeight: 1.3,
102+
}}
103+
>
104+
{data.name}
105+
</h2>
106+
)}
107+
{showHeading && data.description && (
108+
<p
109+
style={{
110+
margin: `0 0 ${margin}px`,
111+
fontSize: "1rem",
112+
opacity: 0.7,
113+
}}
114+
>
115+
{data.description}
116+
</p>
117+
)}
118+
119+
<div
120+
style={{
121+
display: "grid",
122+
gridTemplateColumns: "repeat(12, 1fr)",
123+
gap: margin,
124+
}}
125+
>
126+
{sorted.map(({ settings, chart }) => {
127+
const colSpan = toColSpan(settings.w, bp);
128+
const slotHeight =
129+
rowHeight * settings.h + margin * (settings.h - 1);
130+
131+
return (
132+
<div
133+
key={settings.i}
134+
style={{
135+
gridColumn: `span ${colSpan}`,
136+
overflow: "hidden",
137+
borderRadius: 8,
138+
border: "1px solid rgba(128,128,128,0.15)",
139+
boxShadow: "0 1px 3px rgba(0,0,0,0.06)",
140+
height: slotHeight,
141+
}}
142+
>
143+
<RenderChart
144+
{...chart}
145+
rowHeight={slotHeight}
146+
hFactor={1}
147+
/>
148+
</div>
149+
);
150+
})}
151+
</div>
152+
</div>
153+
);
154+
}
155+
156+
export default RenderGridDashboard;

0 commit comments

Comments
 (0)