feat: New templates added & improvements
This commit is contained in:
parent
8f0b3b9e85
commit
a485342f3c
30 changed files with 2404 additions and 1055 deletions
|
|
@ -6,7 +6,7 @@ export const slideLayoutDescription =
|
|||
"A centered opening slide with company name, roadmap title, and supporting subtitle.";
|
||||
|
||||
export const Schema = z.object({
|
||||
companyName: z.string().min(2).max(28).default("COMPANY NAME").meta({
|
||||
companyName: z.string().min(2).max(18).default("COMPANY NAME").meta({
|
||||
description: "Organization name shown above the slide title.",
|
||||
}),
|
||||
title: z.string().min(8).max(28).default("Development Roadmap").meta({
|
||||
|
|
@ -15,7 +15,7 @@ export const Schema = z.object({
|
|||
subtitle: z
|
||||
.string()
|
||||
.min(24)
|
||||
.max(92)
|
||||
.max(40)
|
||||
.default(
|
||||
"We transform ideas into market-ready solutions through systematic development processes."
|
||||
)
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ export function UserAuth() {
|
|||
.min(40)
|
||||
.max(360)
|
||||
.default(
|
||||
"This component manages credentials as local state and submits them through an async handler. The login utility abstracts network details while the handler keeps the UI flow predictable. Keep validation and side effects isolated so changes remain safe when authentication requirements evolve."
|
||||
"This component manages credentials as local state and submits them through an async handler. The login utility abstracts network details while the handler keeps the UI flow predictable. Keep validation and side effects isolated so changes remain safe when authentication requirements evolve. "
|
||||
)
|
||||
.meta({
|
||||
description: "Explanation paragraph shown in the right column.",
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ export const Schema = z.object({
|
|||
description: "Endpoint path text.",
|
||||
}),
|
||||
headers: z
|
||||
.array(z.string().min(12).max(44))
|
||||
.array(z.string().max(10))
|
||||
.min(2)
|
||||
.max(2)
|
||||
.default(["Content-Type: application/json", "Authorization: Bearer <token>"])
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
import * as z from "zod";
|
||||
|
||||
const FeatureCardSchema = z.object({
|
||||
title: z.string().min(3).max(20).meta({
|
||||
title: z.string().min(3).max(17).meta({
|
||||
description: "Feature title shown on each card.",
|
||||
}),
|
||||
description: z.string().min(18).max(82).meta({
|
||||
description: z.string().min(18).max(80).meta({
|
||||
description: "Supporting feature description.",
|
||||
}),
|
||||
icon: z.object({
|
||||
__icon_url__: z.string().min(10).max(180).meta({
|
||||
__icon_url__: z.string().meta({
|
||||
description: "URL to icon",
|
||||
}),
|
||||
__icon_query__: z.string().min(3).max(28).meta({
|
||||
__icon_query__: z.string().meta({
|
||||
description: "Query used to search the icon",
|
||||
}),
|
||||
}).default({
|
||||
|
|
@ -45,7 +45,7 @@ export const Schema = z.object({
|
|||
},
|
||||
},
|
||||
{
|
||||
title: "Component Library",
|
||||
title: "Component Library ",
|
||||
description: "Reusable UI components with consistent design patterns.",
|
||||
icon: {
|
||||
__icon_url__: "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
import * as z from "zod";
|
||||
|
||||
const ComparisonRowSchema = z.object({
|
||||
feature: z.string().min(4).max(20).meta({
|
||||
feature: z.string().min(4).max(17).meta({
|
||||
description: "Feature label shown in the first column.",
|
||||
}),
|
||||
react: z.string().min(1).max(12).meta({
|
||||
description: "React cell value.",
|
||||
column1: z.string().max(10).meta({
|
||||
description: "Column 1 cell value.",
|
||||
}),
|
||||
vue: z.string().min(1).max(12).meta({
|
||||
description: "Vue cell value.",
|
||||
column2: z.string().max(10).meta({
|
||||
description: "Column 2 cell value.",
|
||||
}),
|
||||
angular: z.string().min(1).max(12).meta({
|
||||
description: "Angular cell value.",
|
||||
column3: z.string().max(10).meta({
|
||||
description: "Column 3 cell value.",
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
@ -24,17 +24,20 @@ export const Schema = z.object({
|
|||
title: z.string().min(6).max(18).default("Comparison").meta({
|
||||
description: "Slide title shown above the table.",
|
||||
}),
|
||||
tableColumns: z.array(z.string().max(4)).meta({
|
||||
description: "Table columns shown in the first row.",
|
||||
}).default(["Feature", "Column 1", "Column 2", "Column 3"]),
|
||||
rows: z
|
||||
.array(ComparisonRowSchema)
|
||||
.min(6)
|
||||
.max(6)
|
||||
.default([
|
||||
{ feature: "Component-based", react: "check", vue: "check", angular: "check" },
|
||||
{ feature: "TypeScript Support", react: "check", vue: "check", angular: "check" },
|
||||
{ feature: "Learning Curve", react: "Medium", vue: "Easy", angular: "Steep" },
|
||||
{ feature: "Bundle Size", react: "40KB", vue: "34KB", angular: "167KB" },
|
||||
{ feature: "Performance", react: "Excellent", vue: "Excellent", angular: "Good" },
|
||||
{ feature: "Community Size", react: "Huge", vue: "Large", angular: "Large" },
|
||||
{ feature: "Component-based", column1: "check", column2: "check", column3: "check" },
|
||||
{ feature: "TypeScript Support", column1: "check", column2: "check", column3: "check" },
|
||||
{ feature: "Learning Curve", column1: "Medium", column2: "Easy", column3: "Steep" },
|
||||
{ feature: "Bundle Size", column1: "40KB", column2: "34KB", column3: "167KB" },
|
||||
{ feature: "Performance", column1: "Excellent", column2: "Excellent", column3: "Good" },
|
||||
{ feature: "Community Size", column1: "Huge", column2: "Large", column3: "Large" },
|
||||
])
|
||||
.meta({
|
||||
description: "Six comparison rows shown in the table.",
|
||||
|
|
@ -62,23 +65,30 @@ const CodeSlide05ComparisonTable = ({ data }: { data: Partial<SchemaType> }) =>
|
|||
<h2 className="text-[64px] font-medium text-[#ffffff]">{data.title}</h2>
|
||||
|
||||
<div className="mt-[22px] min-h-0 flex-1 rounded-[16px] bg-[#0F172BCC] border border-[#1D293D80]">
|
||||
<div className="grid grid-cols-[0.4fr_0.20fr_0.20fr_0.20fr] items-center text-[#8ea1da]">
|
||||
<p className="px-[32px] py-[16px] text-[18px] text-center border-b border-r border-[#1D293D80]">Feature</p>
|
||||
<p className="px-[32px] py-[16px] text-[18px] text-center text-[#ffffff] border-b border-r border-[#1D293D80]">React</p>
|
||||
<p className="px-[32px] py-[16px] text-[18px] text-center text-[#ffffff] border-b border-r border-[#1D293D80]">Vue</p>
|
||||
<p className="px-[32px] py-[16px] text-[18px] text-center text-[#ffffff] border-b border-r border-[#1D293D80]">Angular</p>
|
||||
<div className="grid grid-cols-[0.4fr_0.20fr_0.20fr_0.20fr] items-center text-[#8ea1da]"
|
||||
style={{
|
||||
gridTemplateColumns: `repeat(${data?.tableColumns?.length || 1}, 1fr)`,
|
||||
}}
|
||||
>
|
||||
|
||||
{data?.tableColumns?.map((column) => (
|
||||
<p className="px-[32px] py-[16px] text-[18px] text-center text-[#ffffff] border-b border-r border-[#1D293D80]">{column}</p>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="">
|
||||
{data?.rows?.map((row) => (
|
||||
<div
|
||||
key={row.feature}
|
||||
className="grid grid-cols-[0.4fr_0.20fr_0.20fr_0.20fr] "
|
||||
className="grid grid-cols-[0.4fr_0.20fr_0.20fr_0.20fr]"
|
||||
style={{
|
||||
gridTemplateColumns: `repeat(${data?.tableColumns?.length || 1}, 1fr)`,
|
||||
}}
|
||||
>
|
||||
<p className="px-[32px] py-[20px] text-center text-[18px] text-[#d5dcff] border-b border-r border-[#1D293D80]">{row.feature}</p>
|
||||
<div className="flex justify-center items-center text-[18px] border-b border-r border-[#1D293D80] ">{renderCell(row.react)}</div>
|
||||
<div className="flex justify-center items-center text-[18px] border-b border-r border-[#1D293D80] ">{renderCell(row.vue)}</div>
|
||||
<div className="flex justify-center items-center text-[18px] border-b border-r border-[#1D293D80] ">{renderCell(row.angular)}</div>
|
||||
<div className="flex justify-center items-center text-[18px] border-b border-r border-[#1D293D80] ">{renderCell(row.column1)}</div>
|
||||
<div className="flex justify-center items-center text-[18px] border-b border-r border-[#1D293D80] ">{renderCell(row.column2)}</div>
|
||||
<div className="flex justify-center items-center text-[18px] border-b border-r border-[#1D293D80] ">{renderCell(row.column3)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,12 +5,16 @@ const WorkflowStepSchema = z.object({
|
|||
title: z.string().min(3).max(12).meta({
|
||||
description: "Step title shown in each workflow card.",
|
||||
}),
|
||||
description: z.string().min(18).max(58).meta({
|
||||
description: z.string().min(18).max(50).meta({
|
||||
description: "Short step description text.",
|
||||
}),
|
||||
icon: z.object({
|
||||
__icon_url__: z.string().min(10).max(180),
|
||||
__icon_query__: z.string().min(3).max(28),
|
||||
__icon_url__: z.string().meta({
|
||||
description: "URL to icon",
|
||||
}),
|
||||
__icon_query__: z.string().meta({
|
||||
description: "Query used to search the icon",
|
||||
}),
|
||||
}).default({
|
||||
__icon_url__: "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
|
||||
__icon_query__: "check icon",
|
||||
|
|
|
|||
|
|
@ -6,11 +6,11 @@ export const slideLayoutDescription =
|
|||
"A two-column numbered use-case list with eight compact items.";
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().min(6).max(16).default("Usecase").meta({
|
||||
title: z.string().min(6).max(30).default("Usecase").meta({
|
||||
description: "Slide title shown above the numbered list.",
|
||||
}),
|
||||
items: z
|
||||
.array(z.string().min(16).max(58))
|
||||
.array(z.string().min(4).max(8))
|
||||
.min(4)
|
||||
.max(8)
|
||||
.default([
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ export const slideLayoutDescription =
|
|||
"A text-only explanation slide with generous whitespace for narrative documentation.";
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().min(8).max(24).default("Code + Explanation").meta({
|
||||
title: z.string().min(8).max(30).default("Code + Explanation").meta({
|
||||
description: "Main slide title shown at the top-left.",
|
||||
}),
|
||||
explanationTitle: z.string().min(4).max(20).default("Explanation").meta({
|
||||
|
|
@ -14,7 +14,7 @@ export const Schema = z.object({
|
|||
}),
|
||||
explanation: z
|
||||
.string()
|
||||
.min(60)
|
||||
|
||||
.max(360)
|
||||
.default(
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import * as z from "zod";
|
||||
|
||||
const MetricSchema = z.object({
|
||||
value: z.string().min(2).max(8).meta({
|
||||
value: z.string().min(2).max(6).meta({
|
||||
description: "Primary metric value.",
|
||||
}),
|
||||
label: z.string().min(3).max(14).meta({
|
||||
|
|
@ -26,7 +26,6 @@ export const Schema = z.object({
|
|||
}),
|
||||
explanation: z
|
||||
.string()
|
||||
.min(60)
|
||||
.max(320)
|
||||
.default(
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import * as z from "zod";
|
||||
|
||||
const MetricSchema = z.object({
|
||||
value: z.string().min(2).max(8).meta({
|
||||
value: z.string().min(2).max(6).meta({
|
||||
description: "Primary metric value.",
|
||||
}),
|
||||
label: z.string().min(3).max(14).meta({
|
||||
|
|
|
|||
|
|
@ -0,0 +1,511 @@
|
|||
"use client";
|
||||
|
||||
import {
|
||||
Area,
|
||||
AreaChart,
|
||||
Bar,
|
||||
BarChart,
|
||||
CartesianGrid,
|
||||
Cell,
|
||||
LabelList,
|
||||
Legend,
|
||||
Line,
|
||||
LineChart,
|
||||
Pie,
|
||||
PieChart,
|
||||
ReferenceLine,
|
||||
ResponsiveContainer,
|
||||
Scatter,
|
||||
ScatterChart,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
|
||||
export type EducationChartType =
|
||||
| "bar"
|
||||
| "bar-horizontal"
|
||||
| "bar-grouped-vertical"
|
||||
| "bar-grouped-horizontal"
|
||||
| "bar-stacked-vertical"
|
||||
| "bar-stacked-horizontal"
|
||||
| "bar-clustered"
|
||||
| "bar-diverging"
|
||||
| "line"
|
||||
| "area"
|
||||
| "area-stacked"
|
||||
| "pie"
|
||||
| "donut"
|
||||
| "scatter";
|
||||
|
||||
export type SimpleDatum = {
|
||||
name: string;
|
||||
value: number;
|
||||
};
|
||||
|
||||
export type MultiSeriesDatum = {
|
||||
name: string;
|
||||
values: Record<string, number>;
|
||||
};
|
||||
|
||||
export type DivergingDatum = {
|
||||
name: string;
|
||||
positive: number;
|
||||
negative: number;
|
||||
};
|
||||
|
||||
export type ScatterDatum = {
|
||||
x: number;
|
||||
y: number;
|
||||
name?: string;
|
||||
};
|
||||
|
||||
export type EducationChartDatum = SimpleDatum | MultiSeriesDatum | DivergingDatum | ScatterDatum;
|
||||
|
||||
type TooltipPayloadItem = {
|
||||
color?: string;
|
||||
dataKey?: string | number;
|
||||
name?: string;
|
||||
value?: string | number;
|
||||
};
|
||||
|
||||
type TooltipProps = {
|
||||
active?: boolean;
|
||||
label?: string | number;
|
||||
payload?: TooltipPayloadItem[];
|
||||
};
|
||||
|
||||
type PieLabelProps = {
|
||||
name?: string;
|
||||
percent?: number;
|
||||
textAnchor?: "start" | "middle" | "end";
|
||||
x?: number;
|
||||
y?: number;
|
||||
};
|
||||
|
||||
const DEFAULT_COLORS = [
|
||||
"#4A15A8",
|
||||
"#5B45AD",
|
||||
"#7E6CC0",
|
||||
"#9F94CD",
|
||||
"#6A31B8",
|
||||
"#4D2A97",
|
||||
];
|
||||
|
||||
const AXIS = "#7C7A83";
|
||||
const GRID = "#CFCBD8";
|
||||
const PRIMARY_TEXT = "#3E3C45";
|
||||
|
||||
function formatComma(value: number | string) {
|
||||
if (typeof value === "number") {
|
||||
return value.toLocaleString("en-US");
|
||||
}
|
||||
|
||||
const parsed = Number(value);
|
||||
if (Number.isFinite(parsed)) {
|
||||
return parsed.toLocaleString("en-US");
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function isSimpleDatum(item: EducationChartDatum): item is SimpleDatum {
|
||||
return typeof (item as SimpleDatum).name === "string" && typeof (item as SimpleDatum).value === "number";
|
||||
}
|
||||
|
||||
function isMultiSeriesDatum(item: EducationChartDatum): item is MultiSeriesDatum {
|
||||
return (
|
||||
typeof (item as MultiSeriesDatum).name === "string" &&
|
||||
typeof (item as MultiSeriesDatum).values === "object" &&
|
||||
(item as MultiSeriesDatum).values !== null
|
||||
);
|
||||
}
|
||||
|
||||
function isDivergingDatum(item: EducationChartDatum): item is DivergingDatum {
|
||||
return (
|
||||
typeof (item as DivergingDatum).name === "string" &&
|
||||
typeof (item as DivergingDatum).positive === "number" &&
|
||||
typeof (item as DivergingDatum).negative === "number"
|
||||
);
|
||||
}
|
||||
|
||||
function isScatterDatum(item: EducationChartDatum): item is ScatterDatum {
|
||||
return typeof (item as ScatterDatum).x === "number" && typeof (item as ScatterDatum).y === "number";
|
||||
}
|
||||
|
||||
function toSimpleData(data: EducationChartDatum[]) {
|
||||
return data
|
||||
.filter(isSimpleDatum)
|
||||
.map((item) => ({
|
||||
name: item.name,
|
||||
value: item.value,
|
||||
}));
|
||||
}
|
||||
|
||||
function toScatterData(data: EducationChartDatum[]) {
|
||||
const scatterData = data.filter(isScatterDatum);
|
||||
|
||||
if (scatterData.length > 0) {
|
||||
return scatterData.map((item, index) => ({
|
||||
x: item.x,
|
||||
y: item.y,
|
||||
name: item.name ?? String(index + 1),
|
||||
}));
|
||||
}
|
||||
|
||||
return data
|
||||
.filter(isSimpleDatum)
|
||||
.map((item, index) => ({
|
||||
x: index + 1,
|
||||
y: item.value,
|
||||
name: item.name,
|
||||
}));
|
||||
}
|
||||
|
||||
const renderPieInsideLabel = (props: any) => {
|
||||
const {
|
||||
cx = 0,
|
||||
cy = 0,
|
||||
midAngle = 0,
|
||||
innerRadius: ir = 0,
|
||||
outerRadius: or = 0,
|
||||
percent = 0,
|
||||
name,
|
||||
} = props;
|
||||
if (percent < 0.08) return null;
|
||||
const toNum = (v: unknown) => {
|
||||
if (typeof v === "number" && Number.isFinite(v)) return v;
|
||||
if (typeof v === "string" && v.trim().endsWith("%")) return NaN;
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) ? n : NaN;
|
||||
};
|
||||
let inner = toNum(ir);
|
||||
let outer = toNum(or);
|
||||
if (!Number.isFinite(outer)) {
|
||||
outer = 140;
|
||||
inner = Number.isFinite(inner) ? inner : 0;
|
||||
}
|
||||
if (!Number.isFinite(inner)) inner = 0;
|
||||
const midR = inner + (outer - inner) * 0.5;
|
||||
const rad = (-midAngle * Math.PI) / 180;
|
||||
const x = cx + midR * Math.cos(rad);
|
||||
const y = cy + midR * Math.sin(rad);
|
||||
const nm = String(name ?? "");
|
||||
const short = nm.length <= 10;
|
||||
const pct = `${(percent * 100).toFixed(0)}%`;
|
||||
const fontSize = short ? 10 : 9;
|
||||
const labelText = short ? `${name} ${pct}` : pct;
|
||||
return (
|
||||
<text
|
||||
x={x}
|
||||
y={y}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="central"
|
||||
fill="#ffffff"
|
||||
fontSize={fontSize}
|
||||
fontWeight={600}
|
||||
style={{
|
||||
paintOrder: "stroke fill",
|
||||
stroke: "rgba(0,0,0,0.28)",
|
||||
strokeWidth: 2,
|
||||
}}
|
||||
>
|
||||
{labelText}
|
||||
</text>
|
||||
);
|
||||
};
|
||||
|
||||
function transformMultiSeriesData(data: EducationChartDatum[], series: string[]) {
|
||||
return data
|
||||
.filter(isMultiSeriesDatum)
|
||||
.map((item) => {
|
||||
const transformed: Record<string, string | number> = {
|
||||
name: item.name,
|
||||
};
|
||||
|
||||
series.forEach((serie) => {
|
||||
transformed[serie] = item.values?.[serie] ?? 0;
|
||||
});
|
||||
|
||||
return transformed;
|
||||
});
|
||||
}
|
||||
|
||||
function transformDivergingData(data: EducationChartDatum[]) {
|
||||
return data
|
||||
.filter(isDivergingDatum)
|
||||
.map((item) => ({
|
||||
name: item.name,
|
||||
positive: item.positive,
|
||||
negative: -Math.abs(item.negative),
|
||||
}));
|
||||
}
|
||||
|
||||
function CustomTooltip({ active, payload, label }: TooltipProps) {
|
||||
if (!active || !payload || payload.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-[8px] border border-[#ddd9e8] bg-[#f7f6fa] px-[10px] py-[8px]">
|
||||
<p className="text-[11px] font-semibold text-[#3f3d47]">{label}</p>
|
||||
{payload.map((entry, index) => (
|
||||
<p key={`${entry.name ?? "value"}-${index}`} className="text-[10px] text-[#5d5b67]">
|
||||
{entry.name ?? entry.dataKey}: {String(entry.value)}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getChartColor(index: number) {
|
||||
return DEFAULT_COLORS[index % DEFAULT_COLORS.length];
|
||||
}
|
||||
|
||||
function renderPieLabel({ name, percent = 0, x = 0, y = 0, textAnchor = "middle" }: PieLabelProps) {
|
||||
if (percent < 0.08) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<text x={x} y={y} textAnchor={textAnchor} fill={AXIS} fontSize={10} fontFamily="serif">
|
||||
{`${name ?? ""} ${(percent * 100).toFixed(0)}%`}
|
||||
</text>
|
||||
);
|
||||
}
|
||||
|
||||
function ChartLegend({ showLegend }: { showLegend: boolean }) {
|
||||
if (!showLegend) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Legend wrapperStyle={{ fontSize: "12px", color: AXIS, paddingTop: "8px" }} iconType="circle" />;
|
||||
}
|
||||
|
||||
export default function EducationChartPrimitives({
|
||||
chartType,
|
||||
chartData,
|
||||
series,
|
||||
showLegend,
|
||||
|
||||
divergingLabels,
|
||||
}: {
|
||||
chartType: EducationChartType;
|
||||
chartData: EducationChartDatum[];
|
||||
series: string[];
|
||||
showLegend: boolean;
|
||||
showTooltip: boolean;
|
||||
divergingLabels: [string, string];
|
||||
}) {
|
||||
const axisProps = {
|
||||
tick: { fill: AXIS, fontSize: 12, fontFamily: "serif" },
|
||||
axisLine: { stroke: GRID },
|
||||
tickLine: { stroke: GRID },
|
||||
} as const;
|
||||
|
||||
const gridProps = {
|
||||
strokeDasharray: "0",
|
||||
stroke: GRID,
|
||||
opacity: 1,
|
||||
} as const;
|
||||
|
||||
const commonMargin = { top: 10, right: 12, left: 6, bottom: 8 };
|
||||
|
||||
const simpleData = toSimpleData(chartData);
|
||||
|
||||
const chart = (() => {
|
||||
switch (chartType) {
|
||||
case "bar":
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={simpleData} margin={commonMargin}>
|
||||
<XAxis dataKey="name" {...axisProps} axisLine={false} tickLine={false} />
|
||||
<ChartLegend showLegend={showLegend} />
|
||||
<Bar dataKey="value" radius={[18, 18, 18, 18]} barSize={30} isAnimationActive={false}>
|
||||
{simpleData.map((_, index) => (
|
||||
<Cell key={`bar-cell-${index}`} fill={getChartColor(index)} />
|
||||
))}
|
||||
<LabelList
|
||||
dataKey="value"
|
||||
position="top"
|
||||
fill={AXIS}
|
||||
fontSize={12}
|
||||
offset={10}
|
||||
/>
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
|
||||
);
|
||||
|
||||
case "bar-horizontal":
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={simpleData} layout="vertical" margin={commonMargin}>
|
||||
<CartesianGrid {...gridProps} horizontal={false} />
|
||||
<XAxis type="number" {...axisProps} tickFormatter={formatComma} />
|
||||
<YAxis type="category" dataKey="name" {...axisProps} width={74} />
|
||||
|
||||
<ChartLegend showLegend={showLegend} />
|
||||
<Bar dataKey="value" radius={[0, 10, 10, 0]} isAnimationActive={false}>
|
||||
{simpleData.map((_, index) => (
|
||||
<Cell key={`barh-cell-${index}`} fill={getChartColor(index)} />
|
||||
))}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
case "line":
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={simpleData} margin={commonMargin}>
|
||||
<CartesianGrid {...gridProps} horizontal={false} />
|
||||
<XAxis dataKey="name" {...axisProps} axisLine={false} tickLine={false} />
|
||||
<YAxis {...axisProps} tickFormatter={formatComma} axisLine={false} tickLine={false} />
|
||||
|
||||
<ChartLegend showLegend={showLegend} />
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="value"
|
||||
stroke={getChartColor(0)}
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
|
||||
case "area":
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={simpleData} margin={commonMargin}>
|
||||
<CartesianGrid {...gridProps} vertical={false} />
|
||||
<XAxis dataKey="name" {...axisProps} axisLine={false} tickLine={false} />
|
||||
<YAxis {...axisProps} tickFormatter={formatComma} axisLine={false} tickLine={false} />
|
||||
|
||||
<ChartLegend showLegend={showLegend} />
|
||||
<defs>
|
||||
<linearGradient id="education-area-fill" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor={getChartColor(0)} stopOpacity={0.35} />
|
||||
<stop offset="95%" stopColor={getChartColor(0)} stopOpacity={0.05} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="value"
|
||||
stroke={getChartColor(0)}
|
||||
strokeWidth={2}
|
||||
fill="url(#education-area-fill)"
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
|
||||
|
||||
|
||||
case "pie":
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
|
||||
<PieChart margin={commonMargin}>
|
||||
<Pie
|
||||
data={simpleData}
|
||||
dataKey="value"
|
||||
nameKey="name"
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
|
||||
label={renderPieInsideLabel}
|
||||
labelLine={false}
|
||||
>
|
||||
{simpleData.map((_, index) => (
|
||||
<Cell key={`pie-cell-${index}`} fill={getChartColor(index)} />
|
||||
))}
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
|
||||
case "donut":
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
|
||||
<PieChart margin={commonMargin}>
|
||||
<Pie
|
||||
data={simpleData}
|
||||
dataKey="value"
|
||||
nameKey="name"
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius={90}
|
||||
label={renderPieInsideLabel}
|
||||
paddingAngle={2}
|
||||
labelLine={false}
|
||||
>
|
||||
{simpleData.map((_, index) => (
|
||||
<Cell key={`donut-cell-${index}`} fill={getChartColor(index)} />
|
||||
))}
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
|
||||
case "scatter": {
|
||||
const scatterData = toScatterData(chartData);
|
||||
const labelMap = new Map<number, string>();
|
||||
const xTicks = Array.from(new Set(scatterData.map((item) => item.x))).sort((a, b) => a - b);
|
||||
const minTick = xTicks[0] ?? 0;
|
||||
const maxTick = xTicks[xTicks.length - 1] ?? 1;
|
||||
|
||||
scatterData.forEach((item) => {
|
||||
labelMap.set(item.x, item.name);
|
||||
});
|
||||
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<ScatterChart margin={commonMargin}>
|
||||
<CartesianGrid {...gridProps} vertical={false} />
|
||||
<XAxis
|
||||
type="number"
|
||||
dataKey="x"
|
||||
{...axisProps}
|
||||
ticks={xTicks}
|
||||
domain={[minTick - 0.5, maxTick + 0.5]}
|
||||
tickFormatter={(value) => labelMap.get(Number(value)) ?? String(value)}
|
||||
/>
|
||||
<YAxis type="number" dataKey="y" {...axisProps} tickFormatter={formatComma} />
|
||||
|
||||
<ChartLegend showLegend={showLegend} />
|
||||
<Scatter data={scatterData} fill={getChartColor(0)} isAnimationActive={false}>
|
||||
{scatterData.map((_, index) => (
|
||||
<Cell key={`scatter-cell-${index}`} fill={getChartColor(index)} />
|
||||
))}
|
||||
</Scatter>
|
||||
</ScatterChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
}
|
||||
|
||||
default:
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center text-[14px]" style={{ color: PRIMARY_TEXT }}>
|
||||
Unsupported chart type
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})();
|
||||
|
||||
return <ResponsiveContainer width="100%" height="100%">{chart}</ResponsiveContainer>;
|
||||
}
|
||||
|
|
@ -0,0 +1,180 @@
|
|||
import * as z from "zod";
|
||||
|
||||
import EducationChartPrimitives, {
|
||||
type EducationChartDatum,
|
||||
type EducationChartType,
|
||||
} from "./EducationChartPrimitives";
|
||||
|
||||
export const slideLayoutId = "education-report-chart-slide";
|
||||
export const slideLayoutName = "Education Report Chart Slide";
|
||||
export const slideLayoutDescription =
|
||||
"A split education report slide with one unified schema that supports multiple Recharts chart types in the right panel.";
|
||||
|
||||
const ChartTypeSchema = z.enum([
|
||||
"bar",
|
||||
|
||||
"line",
|
||||
"area",
|
||||
|
||||
"pie",
|
||||
"donut",
|
||||
"scatter",
|
||||
]);
|
||||
|
||||
const SimpleDataSchema = z.object({
|
||||
name: z.string().min(1).max(20).meta({
|
||||
description: "Simple chart category label.",
|
||||
}),
|
||||
value: z.number().min(-100000).max(100000).meta({
|
||||
description: "Simple chart numeric value.",
|
||||
}),
|
||||
});
|
||||
|
||||
const MultiSeriesDataSchema = z.object({
|
||||
name: z.string().min(1).max(20).meta({
|
||||
description: "Grouped/stacked category label.",
|
||||
}),
|
||||
values: z.record(z.string(), z.number()).meta({
|
||||
description: "Series-to-value map for grouped or stacked charts.",
|
||||
}),
|
||||
});
|
||||
|
||||
const DivergingDataSchema = z.object({
|
||||
name: z.string().min(1).max(20).meta({
|
||||
description: "Diverging chart category label.",
|
||||
}),
|
||||
positive: z.number().min(0).max(100000).meta({
|
||||
description: "Positive side value.",
|
||||
}),
|
||||
negative: z.number().min(0).max(100000).meta({
|
||||
description: "Negative side value.",
|
||||
}),
|
||||
});
|
||||
|
||||
const ScatterDataSchema = z.object({
|
||||
x: z.number().min(-100000).max(100000).meta({
|
||||
description: "Scatter X coordinate.",
|
||||
}),
|
||||
y: z.number().min(-100000).max(100000).meta({
|
||||
description: "Scatter Y coordinate.",
|
||||
}),
|
||||
name: z.string().min(1).max(20).optional().meta({
|
||||
description: "Optional scatter tick label.",
|
||||
}),
|
||||
});
|
||||
|
||||
const UnifiedChartDataSchema = z.union([
|
||||
z.array(SimpleDataSchema),
|
||||
z.array(MultiSeriesDataSchema),
|
||||
z.array(DivergingDataSchema),
|
||||
z.array(ScatterDataSchema),
|
||||
]);
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().min(3).max(20).default("Report").meta({
|
||||
description: "Left-side report title.",
|
||||
}),
|
||||
body: z.string().min(80).max(260).default(
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
|
||||
).meta({
|
||||
description: "Left-side report body paragraph.",
|
||||
}),
|
||||
footnote: z.string().min(20).max(150).default(
|
||||
"(Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.)"
|
||||
).meta({
|
||||
description: "Left-side footnote line.",
|
||||
}),
|
||||
chartTitle: z.string().min(8).max(42).default("Students by Grade Level").meta({
|
||||
description: "Right-panel chart heading.",
|
||||
}),
|
||||
dateRange: z.string().min(8).max(22).default("Apr 10 - Apr 17").meta({
|
||||
description: "Right-panel date range label.",
|
||||
}),
|
||||
chartType: ChartTypeSchema.default("area").meta({
|
||||
description:
|
||||
"Chart type selector. Supports bar, grouped, stacked, clustered, diverging, line, area, pie/donut, and scatter.",
|
||||
}),
|
||||
chartData: UnifiedChartDataSchema.default([
|
||||
{ name: "Option A", value: 17.07 },
|
||||
{ name: "Option B", value: 45.23 },
|
||||
{ name: "Option C", value: 21.61 },
|
||||
{ name: "Option D", value: 16.36 },
|
||||
]).meta({
|
||||
description: "Unified chart data payload. Shape depends on chartType.",
|
||||
}),
|
||||
series: z.array(z.string().min(1).max(20)).max(6).default(["Series A", "Series B"]).meta({
|
||||
description: "Series names for grouped/stacked/clustered/area-stacked charts.",
|
||||
}),
|
||||
divergingLabels: z.tuple([z.string().min(1).max(24), z.string().min(1).max(24)]).default([
|
||||
"Positive",
|
||||
"Negative",
|
||||
]).meta({
|
||||
description: "Legend labels for bar-diverging charts.",
|
||||
}),
|
||||
showLegend: z.boolean().default(true).meta({
|
||||
description: "Show or hide chart legend.",
|
||||
}),
|
||||
|
||||
showStatusMessage: z.boolean().default(false).meta({
|
||||
description: "Show callout message under chart (useful for weekly/performance styles).",
|
||||
}),
|
||||
statusMessageTitle: z.string().min(8).max(40).default("You are doing good!").meta({
|
||||
description: "Callout headline under chart.",
|
||||
}),
|
||||
statusMessageBody: z.string().min(10).max(80).default("You almost reached your goal").meta({
|
||||
description: "Callout subtitle under chart.",
|
||||
}),
|
||||
});
|
||||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
|
||||
|
||||
const EducationReportChartSlide = ({ data }: { data: Partial<SchemaType> }) => {
|
||||
const slideData = Schema.parse(data);
|
||||
|
||||
const chartHeightClass = slideData.showStatusMessage ? "h-[372px]" : "h-[486px]";
|
||||
|
||||
return (
|
||||
<div className="relative h-[720px] w-[1280px] overflow-hidden bg-[#efeff1]">
|
||||
<div className="grid h-full grid-cols-[1fr_560px] items-center ">
|
||||
<div className="px-[52px] pb-[46px] mt-[111px] ">
|
||||
<div className="text-start">
|
||||
<h2 className=" text-[64px] font-medium leading-[98%] text-[#101C3D] ">
|
||||
{slideData.title}
|
||||
</h2>
|
||||
<p className=" mt-[38px] max-w-[610px] text-[22px] leading-[1.22] text-[#3E3F4A] ">
|
||||
{slideData.body}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p className="max-w-[610px] mt-[96px] text-[18px] leading-[1.22] text-[#4E4F57] ">
|
||||
{slideData.footnote}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-[#eceaf0] px-[42px] h-full flex flex-col justify-center ">
|
||||
<h3 className="text-center text-[24px] font-semibold leading-none text-[#33313A] ">
|
||||
{slideData.chartTitle}
|
||||
</h3>
|
||||
<p className="mt-1 text-center pb-6 text-[18px] leading-none text-[#4D4B55] ">
|
||||
{slideData.dateRange}
|
||||
</p>
|
||||
|
||||
<div className={` ${chartHeightClass} h-[372px]`}>
|
||||
<EducationChartPrimitives
|
||||
chartType={slideData.chartType as EducationChartType}
|
||||
chartData={slideData.chartData as EducationChartDatum[]}
|
||||
series={slideData.series}
|
||||
showLegend={slideData.showLegend}
|
||||
divergingLabels={slideData.divergingLabels}
|
||||
showTooltip={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EducationReportChartSlide;
|
||||
|
|
@ -1,130 +0,0 @@
|
|||
import * as z from "zod";
|
||||
|
||||
|
||||
export const slideLayoutId = "education-report-donut-slide";
|
||||
export const slideLayoutName = "Education Report Donut Slide";
|
||||
export const slideLayoutDescription =
|
||||
"A report slide with left-side title/content and a right-side donut chart with legend values.";
|
||||
|
||||
const SegmentSchema = z.object({
|
||||
label: z.string().min(3).max(12).meta({
|
||||
description: "Legend label for one donut chart segment.",
|
||||
}),
|
||||
value: z.number().min(1).max(100).meta({
|
||||
description: "Percentage value for one chart segment.",
|
||||
}),
|
||||
color: z.string().min(4).max(20).meta({
|
||||
description: "Hex color value for one chart segment.",
|
||||
}),
|
||||
});
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().min(3).max(14).default("Report").meta({
|
||||
description: "Main heading in the left content area.",
|
||||
}),
|
||||
body: z.string().min(80).max(220).default(
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris."
|
||||
).meta({
|
||||
description: "Main report paragraph on the left.",
|
||||
}),
|
||||
footnote: z.string().min(20).max(110).default(
|
||||
"(Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt.)"
|
||||
).meta({
|
||||
description: "Footnote text shown at the bottom of the left area.",
|
||||
}),
|
||||
chartTitle: z.string().min(8).max(26).default("Students by Grade Level").meta({
|
||||
description: "Heading shown above the donut chart.",
|
||||
}),
|
||||
dateRange: z.string().min(8).max(22).default("Apr 10 - Apr 17").meta({
|
||||
description: "Date range label under the chart heading.",
|
||||
}),
|
||||
segments: z
|
||||
.array(SegmentSchema)
|
||||
.min(4)
|
||||
.max(4)
|
||||
.default([
|
||||
{ label: "Option A", value: 17.07, color: "#4A15A8" },
|
||||
{ label: "Option B", value: 45.23, color: "#5B45AD" },
|
||||
{ label: "Option C", value: 21.61, color: "#876FC1" },
|
||||
{ label: "Option D", value: 16.36, color: "#A89ACF" },
|
||||
])
|
||||
.meta({
|
||||
description: "Four donut segments with labels and percentages.",
|
||||
}),
|
||||
|
||||
});
|
||||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
const EducationReportDonutSlide = ({ data }: { data: Partial<SchemaType> }) => {
|
||||
const { title, body, footnote, chartTitle, dateRange, segments } = data;
|
||||
|
||||
const total = segments?.reduce((sum, item) => sum + item.value, 0) || 0;
|
||||
let cursor = 0;
|
||||
const conicStops = segments
|
||||
?.map((segment) => {
|
||||
const start = cursor;
|
||||
const span = total > 0 ? (segment.value / total) * 100 : 0;
|
||||
cursor += span;
|
||||
return `${segment.color} ${start}% ${cursor}%`;
|
||||
})
|
||||
.join(", ");
|
||||
|
||||
return (
|
||||
<div className="relative h-[720px] w-[1280px] overflow-hidden bg-[#efeff1]">
|
||||
<div className="h-[147px] w-[147px] mx-[40px] bg-[#2C3592] p-[20px]">
|
||||
</div>
|
||||
<div className="grid h-full grid-cols-[1fr_600px]">
|
||||
|
||||
|
||||
<div className="px-[53px] flex flex-col justify-between">
|
||||
|
||||
<div>
|
||||
<h2 className="mt-[131px] font-serif text-[64px] leading-[98%] tracking-[-0.02em] text-[#1a1752]">
|
||||
{title}
|
||||
</h2>
|
||||
|
||||
<p className="mt-[30px] max-w-[610px] text-[22px] font-medium leading-[1.24] text-[#34394C]">
|
||||
{body}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p className=" max-w-[620px] text-[18px] leading-[1.28] text-[#46474C]">
|
||||
{footnote}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="px-[56px] pt-[106px]">
|
||||
<h3 className="text-center text-[24px] font-medium leading-none text-[#34394C]">
|
||||
{chartTitle}
|
||||
</h3>
|
||||
<p className="mt-[12px] text-center text-[14px] leading-none text-[#454962]">{dateRange}</p>
|
||||
|
||||
<div className="mt-[28px] flex justify-center">
|
||||
<div
|
||||
className="relative h-[300px] w-[300px] rounded-full"
|
||||
style={{ background: `conic-gradient(${conicStops})` }}
|
||||
>
|
||||
<div className="absolute left-1/2 top-1/2 h-[222px] w-[222px] -translate-x-1/2 -translate-y-1/2 rounded-full bg-[#efeff1]" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-[52px] grid grid-cols-2 gap-x-[34px] gap-y-[26px]">
|
||||
{segments?.map((segment, index) => (
|
||||
<div key={`${segment.label}-${index}`} className="flex items-center gap-[14px]">
|
||||
<span className="h-[18px] w-[18px] rounded-full" style={{ backgroundColor: segment.color }} />
|
||||
<span className="text-[20px] leading-none text-[#46474C]">{segment.label}</span>
|
||||
<span className="text-[20px] font-medium leading-none text-[#34394C]">
|
||||
{segment.value.toFixed(2)}%
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EducationReportDonutSlide;
|
||||
|
|
@ -1,14 +1,81 @@
|
|||
import * as z from "zod";
|
||||
|
||||
import {
|
||||
ResponsiveContainer,
|
||||
BarChart,
|
||||
Bar,
|
||||
CartesianGrid,
|
||||
XAxis,
|
||||
YAxis,
|
||||
PieChart,
|
||||
Pie,
|
||||
Cell,
|
||||
LineChart,
|
||||
Line,
|
||||
LabelList,
|
||||
} from "recharts";
|
||||
|
||||
export const slideLayoutId = "product-overview-report-snapshot-slide";
|
||||
export const slideLayoutName = "Product Overview Report Snapshot Slide";
|
||||
export const slideLayoutDescription =
|
||||
"A report summary slide with a left-edge photo strip, title and intro copy, a compact bar chart card, and a KPI callout card on the right.";
|
||||
"A report summary slide with a left-edge photo strip, title and intro copy, a chart card that supports four visual styles, and KPI callout cards on the right.";
|
||||
|
||||
const BarSchema = z.object({
|
||||
const LegacyBarSchema = z.object({
|
||||
value: z.number().min(10).max(100).meta({
|
||||
description: "Relative bar value used in the spending mini chart.",
|
||||
description: "Relative bar value used by legacy data.",
|
||||
}),
|
||||
});
|
||||
|
||||
const MiniBarPointSchema = z.object({
|
||||
label: z.string().min(1).max(10).meta({
|
||||
description: "Category label for the mini bar chart.",
|
||||
}),
|
||||
primary: z.number().min(0).max(1000).meta({
|
||||
description: "Primary series value.",
|
||||
}),
|
||||
secondary: z.number().min(0).max(1000).meta({
|
||||
description: "Secondary series value.",
|
||||
}),
|
||||
});
|
||||
|
||||
const DonutPointSchema = z.object({
|
||||
name: z.string().min(1).max(20).meta({
|
||||
description: "Donut segment label.",
|
||||
}),
|
||||
value: z.number().min(1).max(100).meta({
|
||||
description: "Donut segment value.",
|
||||
}),
|
||||
});
|
||||
|
||||
const GroupedBarPointSchema = z.object({
|
||||
label: z.string().min(1).max(12).meta({
|
||||
description: "X-axis label for grouped bars.",
|
||||
}),
|
||||
optionA: z.number().min(0).max(220).meta({
|
||||
description: "Option A value.",
|
||||
}),
|
||||
optionB: z.number().min(0).max(220).meta({
|
||||
description: "Option B value.",
|
||||
}),
|
||||
});
|
||||
|
||||
const TrendPointSchema = z.object({
|
||||
label: z.string().min(1).max(12).meta({
|
||||
description: "X-axis label for trend lines.",
|
||||
}),
|
||||
optionA: z.number().min(0).max(100).meta({
|
||||
description: "Option A trend value.",
|
||||
}),
|
||||
optionB: z.number().min(0).max(100).meta({
|
||||
description: "Option B trend value.",
|
||||
}),
|
||||
});
|
||||
|
||||
const MetricCardSchema = z.object({
|
||||
value: z.string().min(1).max(8).meta({
|
||||
description: "KPI value text.",
|
||||
}),
|
||||
body: z.string().min(10).max(40).meta({
|
||||
description: "KPI supporting sentence.",
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
@ -19,8 +86,8 @@ export const Schema = z.object({
|
|||
taglineLabel: z.string().min(3).max(10).default("TAGLINE").meta({
|
||||
description: "Small label above intro paragraph.",
|
||||
}),
|
||||
taglineBody: z.string().min(40).max(100).default(
|
||||
"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea."
|
||||
taglineBody: z.string().min(40).max(120).default(
|
||||
"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
|
||||
).meta({
|
||||
description: "Intro paragraph shown beneath the heading.",
|
||||
}),
|
||||
|
|
@ -34,13 +101,90 @@ export const Schema = z.object({
|
|||
}).meta({
|
||||
description: "Left-side vertical image strip.",
|
||||
}),
|
||||
chartStyle: z.enum(["mini-bars", "donut", "grouped-bars", "dual-line"]).default("donut").meta({
|
||||
description: "Chart style variant matching Image #1 to Image #4.",
|
||||
}),
|
||||
chartTitle: z.string().min(3).max(20).default("Sandro Tavares").meta({
|
||||
description: "Name displayed in the chart card.",
|
||||
}),
|
||||
bars: z
|
||||
.array(BarSchema)
|
||||
miniBars: z
|
||||
.array(MiniBarPointSchema)
|
||||
.min(8)
|
||||
.max(8)
|
||||
.max(9)
|
||||
.default([
|
||||
{ label: "1", primary: 320, secondary: 560 },
|
||||
{ label: "2", primary: 140, secondary: 840 },
|
||||
{ label: "3", primary: 230, secondary: 520 },
|
||||
{ label: "4", primary: 320, secondary: 660 },
|
||||
{ label: "5", primary: 150, secondary: 460 },
|
||||
{ label: "6", primary: 160, secondary: 850 },
|
||||
{ label: "7", primary: 640, secondary: 320 },
|
||||
{ label: "8", primary: 320, secondary: 440 },
|
||||
{ label: "9", primary: 420, secondary: 620 },
|
||||
])
|
||||
.meta({
|
||||
description: "Data for Image #1 mini dual-series bar chart.",
|
||||
}),
|
||||
donutData: z
|
||||
.array(DonutPointSchema)
|
||||
.min(3)
|
||||
.max(3)
|
||||
.default([
|
||||
{ name: "Option A", value: 60 },
|
||||
{ name: "Option B", value: 20 },
|
||||
{ name: "Option C", value: 20 },
|
||||
])
|
||||
.meta({
|
||||
description: "Data for Image #2 donut chart.",
|
||||
}),
|
||||
groupedBars: z
|
||||
.array(GroupedBarPointSchema)
|
||||
.min(4)
|
||||
.max(4)
|
||||
.default([
|
||||
{ label: "label", optionA: 120, optionB: 200 },
|
||||
{ label: "label", optionA: 150, optionB: 80 },
|
||||
{ label: "label", optionA: 70, optionB: 110 },
|
||||
{ label: "label", optionA: 130, optionB: 130 },
|
||||
])
|
||||
.meta({
|
||||
description: "Data for Image #3 grouped bar chart.",
|
||||
}),
|
||||
trendLines: z
|
||||
.array(TrendPointSchema)
|
||||
.min(6)
|
||||
.max(7)
|
||||
.default([
|
||||
{ label: "label", optionA: 8, optionB: 2 },
|
||||
{ label: "label", optionA: 45, optionB: 65 },
|
||||
{ label: "label", optionA: 35, optionB: 40 },
|
||||
{ label: "label", optionA: 95, optionB: 100 },
|
||||
{ label: "label", optionA: 50, optionB: 35 },
|
||||
{ label: "label", optionA: 5, optionB: 75 },
|
||||
{ label: "label", optionA: 55, optionB: 50 },
|
||||
])
|
||||
.meta({
|
||||
description: "Data for Image #4 dual-line chart.",
|
||||
}),
|
||||
legendLabels: z.array(z.string().min(1).max(18)).min(2).max(3).default(["Option A", "Option B", "Option C"]).meta({
|
||||
description: "Legend labels used by donut/grouped/line variants.",
|
||||
}),
|
||||
xAxisName: z.string().min(3).max(16).default("X axis name").meta({
|
||||
description: "X axis title used in the dual-line variant.",
|
||||
}),
|
||||
yAxisName: z.string().min(3).max(16).default("Y axis name").meta({
|
||||
description: "Y axis title used in the dual-line variant.",
|
||||
}),
|
||||
footerLabel: z.string().min(10).max(60).default("Current margin: April Spendings").meta({
|
||||
description: "Footer label under mini bar chart.",
|
||||
}),
|
||||
footerValue: z.string().min(6).max(24).default("$350.00 / $640.00").meta({
|
||||
description: "Footer value under mini bar chart.",
|
||||
}),
|
||||
bars: z
|
||||
.array(LegacyBarSchema)
|
||||
.min(8)
|
||||
.max(9)
|
||||
.default([
|
||||
{ value: 52 },
|
||||
{ value: 24 },
|
||||
|
|
@ -52,14 +196,25 @@ export const Schema = z.object({
|
|||
{ value: 55 },
|
||||
])
|
||||
.meta({
|
||||
description: "Eight bars used in the spending card chart.",
|
||||
description: "Legacy fallback bar values used when miniBars is not supplied.",
|
||||
}),
|
||||
metricValue: z.string().min(1).max(8).default("X 5").meta({
|
||||
description: "KPI value in the callout card.",
|
||||
description: "Legacy single KPI value.",
|
||||
}),
|
||||
metricBody: z.string().min(10).max(18).default("Lorem ipsum.").meta({
|
||||
description: "KPI short text in the callout card.",
|
||||
metricBody: z.string().min(10).max(40).default("Lorem ipsum dolor sit.").meta({
|
||||
description: "Legacy single KPI body.",
|
||||
}),
|
||||
metricCards: z
|
||||
.array(MetricCardSchema)
|
||||
.min(1)
|
||||
.max(2)
|
||||
.default([
|
||||
{ value: "X 5", body: "Lorem ipsum dolor sit." },
|
||||
{ value: "X 5", body: "Lorem ipsum dolor sit." },
|
||||
])
|
||||
.meta({
|
||||
description: "One or two KPI cards shown on the right.",
|
||||
}),
|
||||
metricIcon: z.object({
|
||||
__icon_url__: z.string().default("https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg"),
|
||||
__icon_query__: z.string().min(3).max(30).default("pulse icon"),
|
||||
|
|
@ -74,86 +229,357 @@ export const Schema = z.object({
|
|||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
const MINI_BAR_DARK = "#0B4B40";
|
||||
const MINI_BAR_LIGHT = "#CED3D1";
|
||||
const DONUT_COLORS = ["#0B4B40", "#4B6B61", "#7B938C"];
|
||||
const KPI_ICON_BG = "#063C73";
|
||||
|
||||
const PulseIcon = () => (
|
||||
<svg viewBox="0 0 24 24" className="h-[22px] w-[22px]" aria-hidden="true">
|
||||
<path
|
||||
d="M2.5 12h4.6l1.7-4.4 3.1 9 2.7-6.2h6.9"
|
||||
fill="none"
|
||||
stroke="#ffffff"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const renderDonutPercentLabel = ({ cx, cy, midAngle, outerRadius, percent }: any) => {
|
||||
const radius = (outerRadius ?? 0) + 18;
|
||||
const x = (cx ?? 0) + radius * Math.cos((-(midAngle ?? 0) * Math.PI) / 180);
|
||||
const y = (cy ?? 0) + radius * Math.sin((-(midAngle ?? 0) * Math.PI) / 180);
|
||||
|
||||
return (
|
||||
<g>
|
||||
<circle cx={x} cy={y} r={16} fill="#ECEAF8" />
|
||||
<text x={x} y={y} style={{ padding: '4px' }} textAnchor="middle" fill="#2C2B39" fontSize={10} fontWeight={600}>
|
||||
{`${Math.round((percent ?? 0) * 100)}%`}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
const ReportSnapshotSlide = ({ data }: { data: Partial<SchemaType> }) => {
|
||||
const {
|
||||
title,
|
||||
taglineLabel,
|
||||
taglineBody,
|
||||
sideImage,
|
||||
chartStyle,
|
||||
chartTitle,
|
||||
miniBars,
|
||||
donutData,
|
||||
groupedBars,
|
||||
trendLines,
|
||||
legendLabels,
|
||||
xAxisName,
|
||||
yAxisName,
|
||||
footerLabel,
|
||||
footerValue,
|
||||
bars,
|
||||
metricValue,
|
||||
metricBody,
|
||||
metricCards,
|
||||
metricIcon,
|
||||
} = data;
|
||||
|
||||
const resolvedMiniBars =
|
||||
miniBars && miniBars.length >= 8
|
||||
? miniBars
|
||||
: (bars ?? []).map((bar, index) => {
|
||||
const scaledPrimary = Math.round(bar.value * 8 + 80);
|
||||
const scaledSecondary = Math.min(1000, scaledPrimary + 220);
|
||||
|
||||
return {
|
||||
label: `${index + 1}`,
|
||||
primary: scaledPrimary,
|
||||
secondary: scaledSecondary,
|
||||
};
|
||||
});
|
||||
|
||||
const fallbackMetric = {
|
||||
value: metricValue ?? "X 5",
|
||||
body: metricBody ?? "Lorem ipsum dolor sit.",
|
||||
};
|
||||
|
||||
const resolvedMetricCards =
|
||||
metricCards && metricCards.length > 0
|
||||
? metricCards
|
||||
: [fallbackMetric, fallbackMetric];
|
||||
|
||||
const visibleMetricCards =
|
||||
(chartStyle ?? "mini-bars") === "mini-bars"
|
||||
? resolvedMetricCards.slice(0, 1)
|
||||
: resolvedMetricCards.slice(0, 2);
|
||||
|
||||
const usePulseFallback = !metricIcon?.__icon_url__ || metricIcon.__icon_url__.includes("placeholder.svg");
|
||||
const activeChartStyle = chartStyle ?? "mini-bars";
|
||||
const donutTotal = (donutData ?? []).reduce((sum, item) => sum + item.value, 0) || 1;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative h-[720px] w-[1280px] overflow-hidden rounded-[24px] flex gap-[44px]"
|
||||
style={{ backgroundColor: "#DAE1DE" }}
|
||||
className="relative h-[720px] w-[1280px] overflow-hidden rounded-[24px]"
|
||||
style={{ backgroundColor: "#D7DEDB" }}
|
||||
>
|
||||
{sideImage?.__image_url__ && (
|
||||
<img
|
||||
src={sideImage?.__image_url__}
|
||||
alt={sideImage?.__image_prompt__}
|
||||
className=" h-full w-[232px] object-cover"
|
||||
src={sideImage.__image_url__}
|
||||
alt={sideImage.__image_prompt__}
|
||||
className="absolute left-0 top-0 h-full w-[232px] object-cover"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className=" pt-[74px]">
|
||||
<h2
|
||||
className="text-[80px] font-semibold leading-[108.4%] tracking-[-2.419px] text-[#15342D]"
|
||||
style={{ color: "#15342D" }}
|
||||
>
|
||||
<div className="absolute left-[268px] top-[74px] text-[#083F37]">
|
||||
<h2 className="text-[80px] font-semibold leading-[108.4%] tracking-[-2.419px]">
|
||||
{title}
|
||||
</h2>
|
||||
|
||||
<div className="mt-[17px] w-[560px]">
|
||||
<p
|
||||
className="text-[20px] font-semibold tracking-[2.074px] text-[#15342D]"
|
||||
style={{ color: "#15342D" }}
|
||||
>
|
||||
<div className="mt-[14px] w-[560px]">
|
||||
<p className="text-[20px] font-semibold tracking-[2.074px] text-[#083F37]">
|
||||
{taglineLabel}
|
||||
</p>
|
||||
<p className="mt-[13px] text-[24px] font-normal text-[#15342DCC]">{taglineBody}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-[56px] left-[268px] w-[574px] bg-[#ececee] px-[24px] py-[20px]">
|
||||
<p className="text-[20px] text-[#6a6a6a]">Spendings</p>
|
||||
<p className="mt-[14px] text-[28px] font-normal text-[#15342DCC]" style={{ color: "#15342D" }}>
|
||||
{chartTitle}
|
||||
</p>
|
||||
|
||||
<div className="mt-[24px] flex h-[124px] items-end gap-[22px] border-b border-[#d8dcdb] pb-[10px]">
|
||||
{bars?.map((bar, index) => (
|
||||
<div key={index} className="w-[24px] rounded-[4px] bg-[#0b4b40]" style={{ height: `${bar.value}%` }} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-[10px] flex justify-between text-[22px] text-[#6a6a6a]">
|
||||
<p>Current margin: April Spendings</p>
|
||||
<p style={{ color: "#15342D" }}>$350.00 / $640.00</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute right-[42px] top-[380px] h-[148px] w-[360px] bg-[#ececee] px-[34px] py-[22px]">
|
||||
<div className="flex items-center gap-[14px]">
|
||||
<div className="flex h-[48px] w-[48px] items-center justify-center rounded-full bg-[#0a3f73]">
|
||||
<img
|
||||
src={metricIcon?.__icon_url__}
|
||||
alt={metricIcon?.__icon_query__}
|
||||
className="h-[22px] w-[22px] object-contain"
|
||||
style={{ filter: "brightness(0) invert(1)" }}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-[28px] font-normal text-[#15342DCC]" style={{ color: "#15342D" }}>
|
||||
{metricValue}
|
||||
<p className="mt-[12px] text-[24px] leading-[1.11] text-[#083F37]/75">
|
||||
{taglineBody}
|
||||
</p>
|
||||
</div>
|
||||
<p className="mt-[16px] text-[28px] font-normal text-[#15342DCC]" style={{ color: "#15342D" }}>
|
||||
{metricBody}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`absolute bottom-[50px] left-[266px] w-[580px] bg-[#F3F3F3] px-[28px] pb-[18px] pt-[20px] ${activeChartStyle === "mini-bars" ? "h-[308px]" : "h-[350px]"
|
||||
}`}
|
||||
>
|
||||
<p className="mt-[14px] text-[32px] font-normal leading-[1.1] text-[#15342D]">{chartTitle}</p>
|
||||
|
||||
{activeChartStyle === "mini-bars" && (
|
||||
<>
|
||||
<div className="mt-[18px] h-[166px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart
|
||||
data={resolvedMiniBars}
|
||||
margin={{ top: 0, right: 8, left: -6, bottom: 0 }}
|
||||
barCategoryGap={16}
|
||||
>
|
||||
<CartesianGrid vertical={false} stroke="#D7DCDA" strokeDasharray="3 3" />
|
||||
<XAxis dataKey="label" tick={false} axisLine={false} tickLine={false} />
|
||||
<YAxis
|
||||
width={42}
|
||||
|
||||
|
||||
tickFormatter={(value) => `$${value}`}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fill: "#6C7271", fontSize: 10 }}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="secondary"
|
||||
fill={MINI_BAR_LIGHT}
|
||||
radius={[5, 5, 0, 0]}
|
||||
isAnimationActive={false}
|
||||
maxBarSize={26}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="primary"
|
||||
fill={MINI_BAR_DARK}
|
||||
radius={[5, 5, 0, 0]}
|
||||
isAnimationActive={false}
|
||||
maxBarSize={26}
|
||||
/>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
<div className="mt-[14px] flex items-center justify-between ">
|
||||
<p className="text-[#6D7371] text-[18px]">{footerLabel}</p>
|
||||
<p className="font-medium text-[#15342D] text-[18px]">{footerValue}</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{activeChartStyle === "donut" && (
|
||||
<div className="mt-[6px] flex h-[250px] items-center">
|
||||
<div className="h-[220px] w-[250px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={donutData ?? []}
|
||||
dataKey="value"
|
||||
innerRadius={48}
|
||||
outerRadius={82}
|
||||
stroke="none"
|
||||
labelLine={false}
|
||||
label={renderDonutPercentLabel}
|
||||
isAnimationActive={false}
|
||||
>
|
||||
{(donutData ?? []).map((entry, index) => (
|
||||
<Cell key={`${entry.name}-${index}`} fill={DONUT_COLORS[index % DONUT_COLORS.length]} />
|
||||
))}
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
<div className="ml-[8px] flex-1 space-y-[16px] pr-[8px]">
|
||||
{(donutData ?? []).map((entry, index) => {
|
||||
const percent = Math.round((entry.value / donutTotal) * 100);
|
||||
return (
|
||||
<div key={`${entry.name}-legend-${index}`} className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-[10px]">
|
||||
<span
|
||||
className="h-[14px] w-[14px] rounded-full"
|
||||
style={{ backgroundColor: DONUT_COLORS[index % DONUT_COLORS.length] }}
|
||||
/>
|
||||
<p className="text-[18px] font-bold text-[#767676]">
|
||||
{legendLabels?.[index] ?? entry.name}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-[18px] font-bold text-[#404040]">{percent}%</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeChartStyle === "grouped-bars" && (
|
||||
<div className="mt-[12px] flex h-[236px] items-center justify-between">
|
||||
<div className="h-[210px] w-[362px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart
|
||||
data={groupedBars ?? []}
|
||||
margin={{ top: 12, right: 6, left: -12, bottom: 0 }}
|
||||
barCategoryGap={20}
|
||||
>
|
||||
<CartesianGrid vertical={false} stroke="#D7DCDA" />
|
||||
<XAxis
|
||||
dataKey="label"
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fill: "#42484A", fontSize: 10 }}
|
||||
/>
|
||||
<YAxis
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
|
||||
tick={{ fill: "#566061", fontSize: 10 }}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="optionA"
|
||||
fill={MINI_BAR_DARK}
|
||||
radius={[4, 4, 0, 0]}
|
||||
maxBarSize={20}
|
||||
isAnimationActive={false}
|
||||
>
|
||||
<LabelList dataKey="optionA" position="top" fill="#5B6463" fontSize={9} />
|
||||
</Bar>
|
||||
<Bar
|
||||
dataKey="optionB"
|
||||
fill="#8A9A96"
|
||||
radius={[4, 4, 0, 0]}
|
||||
maxBarSize={20}
|
||||
isAnimationActive={false}
|
||||
>
|
||||
<LabelList dataKey="optionB" position="top" fill="#5B6463" fontSize={9} />
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
<div className="ml-[24px] space-y-[24px]">
|
||||
<div className="flex items-center gap-[10px]">
|
||||
<span className="h-[14px] w-[14px] rounded-full bg-[#0B4B40]" />
|
||||
<p className="text-[18px] font-medium leading-[1] text-[#6A6B6E]">{legendLabels?.[0] ?? "Option A"}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-[10px]">
|
||||
<span className="h-[14px] w-[14px] rounded-full bg-[#8A9A96]" />
|
||||
<p className="text-[18px] font-medium leading-[1] text-[#6A6B6E]">{legendLabels?.[1] ?? "Option B"}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeChartStyle === "dual-line" && (
|
||||
<div className="mt-[12px] flex h-[236px] items-center justify-between">
|
||||
<div className="h-[210px] w-[362px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={trendLines ?? []} margin={{ top: 12, right: 6, left: -6, bottom: 16 }}>
|
||||
<CartesianGrid vertical={false} stroke="#D7DCDA" />
|
||||
<XAxis
|
||||
dataKey="label"
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fill: "#42484A", fontSize: 10 }}
|
||||
label={{ value: xAxisName, position: "insideBottom", offset: -6, fill: "#535B5C", fontSize: 10 }}
|
||||
/>
|
||||
<YAxis
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
|
||||
tick={{ fill: "#566061", fontSize: 10 }}
|
||||
label={{ value: yAxisName, angle: -90, position: "insideLeft", fill: "#535B5C", fontSize: 10, dx: -8 }}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="optionA"
|
||||
stroke={MINI_BAR_DARK}
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="optionB"
|
||||
stroke="#8A9A96"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
<div className="ml-[18px] space-y-[24px]">
|
||||
<div className="flex items-center gap-[10px]">
|
||||
<span className="h-[14px] w-[14px] rounded-full bg-[#0B4B40]" />
|
||||
<p className="text-[18px] font-medium leading-[1] text-[#6A6B6E]">{legendLabels?.[0] ?? "Option A"}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-[10px]">
|
||||
<span className="h-[14px] w-[14px] rounded-full bg-[#8A9A96]" />
|
||||
<p className="text-[18px] font-medium leading-[1] text-[#6A6B6E]">{legendLabels?.[1] ?? "Option B"}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`absolute right-[38px] w-[362px] ${activeChartStyle === "mini-bars" ? "top-[382px]" : "top-[320px]"
|
||||
}`}
|
||||
>
|
||||
<div className="flex flex-col gap-[24px]">
|
||||
{visibleMetricCards.map((metric, index) => (
|
||||
<div key={`${metric.value}-${index}`} className="bg-[#F3F3F3] px-[33px] py-[24px]">
|
||||
<div className="flex items-center gap-[14px]">
|
||||
<div className="flex h-[56px] w-[56px] items-center justify-center rounded-full" style={{ backgroundColor: "#15342D" }}>
|
||||
{usePulseFallback ? (
|
||||
<PulseIcon />
|
||||
) : (
|
||||
<img
|
||||
src={metricIcon?.__icon_url__}
|
||||
alt={metricIcon?.__icon_query__}
|
||||
className="h-[24px] w-[24px] object-contain"
|
||||
style={{ filter: "invert(1)" }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-[48px] font-semibold leading-[1] text-[#113F37]">{metric.value}</p>
|
||||
</div>
|
||||
<p className="mt-[18px] text-[28px] leading-[1.08] text-[#113F37]">{metric.body}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
"use client";
|
||||
|
||||
import * as z from "zod";
|
||||
|
||||
import { ResponsiveContainer } from "recharts";
|
||||
|
||||
import { WorkflowBarChart } from "./chartPrimitives";
|
||||
import { FlexibleReportChart, flexibleChartDataSchema } from "./flexibleReportChart";
|
||||
|
||||
const InsightItemSchema = z.object({
|
||||
title: z.string().min(3).max(18).meta({
|
||||
title: z.string().min(3).max(12).meta({
|
||||
description: "Short insight title shown next to the icon.",
|
||||
}),
|
||||
description: z.string().min(20).max(84).meta({
|
||||
|
|
@ -12,15 +15,6 @@ const InsightItemSchema = z.object({
|
|||
}),
|
||||
});
|
||||
|
||||
const ChartPointSchema = z.object({
|
||||
label: z.string().min(1).max(12).meta({
|
||||
description: "Chart axis label.",
|
||||
}),
|
||||
value: z.number().min(0).max(1000).meta({
|
||||
description: "Bar chart value.",
|
||||
}),
|
||||
});
|
||||
|
||||
export const slideLayoutId = "data-analysis-bar-slide";
|
||||
export const slideLayoutName = "Data Analysis Bar Slide";
|
||||
export const slideLayoutDescription =
|
||||
|
|
@ -30,16 +24,20 @@ export const Schema = z.object({
|
|||
title: z.string().min(3).max(28).default("Data Analysis").meta({
|
||||
description: "Slide title shown at the top-left.",
|
||||
}),
|
||||
itemIcon: z.object({
|
||||
__icon_url__: z.string().default("https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg"),
|
||||
__icon_query__: z.string().default("pulse icon"),
|
||||
}).default({
|
||||
__icon_url__:
|
||||
"https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
|
||||
__icon_query__: "pulse icon",
|
||||
}).meta({
|
||||
description: "Icon shown in each analysis item badge.",
|
||||
}),
|
||||
itemIcon: z
|
||||
.object({
|
||||
__icon_url__: z
|
||||
.string()
|
||||
.default("https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg"),
|
||||
__icon_query__: z.string().default("pulse icon"),
|
||||
})
|
||||
.default({
|
||||
__icon_url__: "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
|
||||
__icon_query__: "pulse icon",
|
||||
})
|
||||
.meta({
|
||||
description: "Icon shown in each analysis item badge.",
|
||||
}),
|
||||
items: z
|
||||
.array(InsightItemSchema)
|
||||
.min(3)
|
||||
|
|
@ -52,22 +50,19 @@ export const Schema = z.object({
|
|||
.meta({
|
||||
description: "Three analysis points shown in the left column.",
|
||||
}),
|
||||
chartData: z
|
||||
.array(ChartPointSchema)
|
||||
.min(7)
|
||||
.max(7)
|
||||
.default([
|
||||
{ label: "Mon", value: 120 },
|
||||
{ label: "Tue", value: 200 },
|
||||
{ label: "Wed", value: 150 },
|
||||
{ label: "Thu", value: 80 },
|
||||
{ label: "Fri", value: 70 },
|
||||
{ label: "Sat", value: 110 },
|
||||
{ label: "Sun", value: 130 },
|
||||
])
|
||||
.meta({
|
||||
description: "Weekly values shown in the bar chart.",
|
||||
}),
|
||||
chartData: flexibleChartDataSchema.default({
|
||||
type: "pie",
|
||||
data: [
|
||||
{ name: "Mon", value: 120 },
|
||||
{ name: "Tue", value: 200 },
|
||||
{ name: "Wed", value: 150 },
|
||||
{ name: "Thu", value: 80 },
|
||||
{ name: "Fri", value: 70 },
|
||||
{ name: "Sat", value: 110 },
|
||||
{ name: "Sun", value: 130 },
|
||||
],
|
||||
|
||||
}),
|
||||
legendLabel: z.string().min(3).max(32).default("Traditional Workflow").meta({
|
||||
description: "Legend label shown below the chart.",
|
||||
}),
|
||||
|
|
@ -76,20 +71,17 @@ export const Schema = z.object({
|
|||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
const DataAnalysisBarSlide = ({ data }: { data: Partial<SchemaType> }) => {
|
||||
|
||||
const { title, itemIcon, items, chartData, legendLabel } = data;
|
||||
const rows = chartData?.data ?? [];
|
||||
const chartType = chartData?.type ?? "bar";
|
||||
const series = chartData?.series ?? [];
|
||||
|
||||
return (
|
||||
<div className="relative h-[720px] w-[1280px] overflow-hidden rounded-[24px] bg-[#f9f8f8]">
|
||||
<div
|
||||
className="absolute left-0 top-0 w-[42px] rounded-b-[22px] bg-[#157CFF]"
|
||||
style={{ height: 185 }}
|
||||
/>
|
||||
<div className="absolute left-0 top-0 w-[42px] rounded-b-[22px] bg-[#4d4ef3]" style={{ height: 185 }} />
|
||||
|
||||
<div className="px-[64px] pt-[48px]">
|
||||
<h2 className="text-[80px] font-bold leading-[108.4%] tracking-[-2.419px] text-[#232223]">
|
||||
{title}
|
||||
</h2>
|
||||
<h2 className="text-[80px] font-bold leading-[108.4%] tracking-[-2.419px] text-[#232223]">{title}</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between px-[85px] pt-[44px]">
|
||||
|
|
@ -105,20 +97,18 @@ const DataAnalysisBarSlide = ({ data }: { data: Partial<SchemaType> }) => {
|
|||
style={{ filter: "brightness(0) invert(1)" }}
|
||||
/>
|
||||
</div>
|
||||
<h3 className="text-[20px] font-medium tracking-[2.074px] text-[#232223]">
|
||||
{item.title}
|
||||
</h3>
|
||||
<h3 className="text-[20px] font-medium tracking-[2.074px] text-[#232223]">{item.title}</h3>
|
||||
</div>
|
||||
<p className="mt-[20px] text-[24px] leading-[26.667px] text-[#232223]">
|
||||
{item.description}
|
||||
</p>
|
||||
<p className="mt-[20px] text-[24px] leading-[26.667px] text-[#232223]">{item.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="ml-[44px] flex flex-col items-center">
|
||||
<div className="h-[346px] w-[560px]">
|
||||
<WorkflowBarChart data={chartData ?? []} />
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<FlexibleReportChart chartType={chartType} data={rows} series={series} colorFallback="#4d4ef3" />
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
<div className="mt-[12px] flex items-center gap-[10px] text-[24px] tracking-[-0.03em] text-[#4d4ef3]">
|
||||
<span className="h-[12px] w-[12px] rounded-full bg-[#4d4ef3]" />
|
||||
|
|
|
|||
|
|
@ -1,13 +1,20 @@
|
|||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import * as z from "zod";
|
||||
|
||||
import { ResponsiveContainer } from "recharts";
|
||||
|
||||
import {
|
||||
AreaTrendChart,
|
||||
CompactBarChart,
|
||||
CompactPieChart,
|
||||
SemiDonutChart,
|
||||
TrendLineChart,
|
||||
} from "./chartPrimitives";
|
||||
DivergingDataPointSchema,
|
||||
FlexibleReportChart,
|
||||
MultiSeriesDataPointSchema,
|
||||
ScatterDataPointSchema,
|
||||
SimpleDataPointSchema,
|
||||
flexibleChartDataSchema,
|
||||
flexibleChartTypeSchema,
|
||||
type FlexibleChartData,
|
||||
} from "./flexibleReportChart";
|
||||
|
||||
const SummaryCardSchema = z.object({
|
||||
value: z.string().min(1).max(8).meta({
|
||||
|
|
@ -16,162 +23,111 @@ const SummaryCardSchema = z.object({
|
|||
label: z.string().min(3).max(20).meta({
|
||||
description: "Short summary card label.",
|
||||
}),
|
||||
});
|
||||
|
||||
const ChartPointSchema = z.object({
|
||||
label: z.string().min(1).max(12).meta({
|
||||
description: "Chart axis label.",
|
||||
}),
|
||||
value: z.number().min(0).max(1000).meta({
|
||||
description: "Single-series chart value.",
|
||||
icon: z.object({
|
||||
__icon_url__: z.string().default("https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg"),
|
||||
__icon_query__: z.string().default("pulse icon"),
|
||||
}).optional().meta({
|
||||
description: "Icon shown in each compact summary card.",
|
||||
}).default({
|
||||
__icon_url__: "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
|
||||
__icon_query__: "pulse icon",
|
||||
}),
|
||||
});
|
||||
|
||||
const DualChartPointSchema = z.object({
|
||||
label: z.string().min(1).max(12).meta({
|
||||
description: "Chart axis label.",
|
||||
}),
|
||||
valueA: z.number().min(0).max(1000).meta({
|
||||
description: "First series value.",
|
||||
}),
|
||||
valueB: z.number().min(0).max(1000).meta({
|
||||
description: "Second series value.",
|
||||
}),
|
||||
});
|
||||
|
||||
const PieSegmentSchema = z.object({
|
||||
name: z.string().min(1).max(18).meta({
|
||||
description: "Category name shown in chart legends.",
|
||||
}),
|
||||
value: z.number().min(1).max(1000).meta({
|
||||
description: "Category value used in the chart.",
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const slideLayoutId = "data-analysis-dashboard-slide";
|
||||
export const slideLayoutName = "Data Analysis Dashboard Slide";
|
||||
export const slideLayoutDescription =
|
||||
"A dashboard-style slide with a title at the top, a row of compact summary cards underneath, and two stacked dashboard panels below. Each panel is split into three chart cells, creating a six-chart overview made of bar, donut, line, area, pie, and comparison charts.";
|
||||
"A dashboard-style slide with a title, summary cards, and a responsive grid of chart panels (1–9). Each panel uses the same flexible chart types as other report slides; labels and margins are compact for small cells.";
|
||||
|
||||
const ChartItemSchema = z.object({
|
||||
|
||||
type: flexibleChartTypeSchema.default('bar'),
|
||||
data: z.union([
|
||||
z.array(SimpleDataPointSchema),
|
||||
z.array(MultiSeriesDataPointSchema),
|
||||
z.array(DivergingDataPointSchema),
|
||||
z.array(ScatterDataPointSchema),
|
||||
]).default([
|
||||
{ name: 'Q1', value: 45 },
|
||||
{ name: 'Q2', value: 72 },
|
||||
{ name: 'Q3', value: 58 },
|
||||
{ name: 'Q4', value: 89 },
|
||||
]),
|
||||
series: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().min(3).max(28).default("Data Analysis").meta({
|
||||
title: z.string().min(3).max(12).default("Data Analysis").meta({
|
||||
description: "Slide title shown at the top-left.",
|
||||
}),
|
||||
summaryIcon: z.object({
|
||||
__icon_url__: z.string().default("https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg"),
|
||||
__icon_query__: z.string().default("pulse icon"),
|
||||
}).default({
|
||||
__icon_url__:
|
||||
"https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
|
||||
__icon_query__: "pulse icon",
|
||||
}).meta({
|
||||
description: "Icon shown in each compact summary card.",
|
||||
}),
|
||||
|
||||
summaryCards: z
|
||||
.array(SummaryCardSchema)
|
||||
.min(4)
|
||||
.min(2)
|
||||
.max(4)
|
||||
.optional()
|
||||
.default([
|
||||
{ value: "5", label: "Text 1" },
|
||||
{ value: "52", label: "Text 2" },
|
||||
{ value: "4", label: "Text 3" },
|
||||
{ value: "80%", label: "Text 4" },
|
||||
{
|
||||
value: "5", label: "Text 1", icon: {
|
||||
__icon_url__: "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
|
||||
__icon_query__: "placeholder icon",
|
||||
}
|
||||
},
|
||||
{
|
||||
value: "52", label: "Text 2", icon: {
|
||||
__icon_url__: "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
|
||||
__icon_query__: "placeholder icon",
|
||||
}
|
||||
},
|
||||
{
|
||||
value: "4", label: "Text 3", icon: {
|
||||
__icon_url__: "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
|
||||
__icon_query__: "placeholder icon",
|
||||
}
|
||||
},
|
||||
{
|
||||
value: "80%", label: "Text 4", icon: {
|
||||
__icon_url__: "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
|
||||
__icon_query__: "placeholder icon",
|
||||
}
|
||||
},
|
||||
])
|
||||
.meta({
|
||||
description: "Four compact summary cards displayed above the dashboard panels.",
|
||||
}),
|
||||
workflowBars: z
|
||||
.array(ChartPointSchema)
|
||||
.min(7)
|
||||
.max(7)
|
||||
.default([
|
||||
{ label: "Mon", value: 120 },
|
||||
{ label: "Tue", value: 200 },
|
||||
{ label: "Wed", value: 150 },
|
||||
{ label: "Thu", value: 80 },
|
||||
{ label: "Fri", value: 70 },
|
||||
{ label: "Sat", value: 110 },
|
||||
{ label: "Sun", value: 130 },
|
||||
])
|
||||
.meta({
|
||||
description: "Bar chart data shown in the top-left dashboard cell.",
|
||||
}),
|
||||
gaugeSegments: z
|
||||
.array(PieSegmentSchema)
|
||||
.min(3)
|
||||
.max(3)
|
||||
.default([
|
||||
{ name: "Category A", value: 45 },
|
||||
{ name: "Category B", value: 30 },
|
||||
{ name: "Category C", value: 25 },
|
||||
])
|
||||
.meta({
|
||||
description: "Three segments used in the top-center semi-donut chart.",
|
||||
}),
|
||||
trendSeries: z
|
||||
.array(DualChartPointSchema)
|
||||
.min(7)
|
||||
.max(7)
|
||||
.default([
|
||||
{ label: "Label", valueA: 22, valueB: 35 },
|
||||
{ label: "Label", valueA: 54, valueB: 26 },
|
||||
{ label: "Label", valueA: 44, valueB: 70 },
|
||||
{ label: "Label", valueA: 78, valueB: 52 },
|
||||
{ label: "Label", valueA: 50, valueB: 44 },
|
||||
{ label: "Label", valueA: 32, valueB: 60 },
|
||||
{ label: "Label", valueA: 58, valueB: 40 },
|
||||
])
|
||||
.meta({
|
||||
description: "Two-series line chart data shown in the top-right cell.",
|
||||
}),
|
||||
detailedArea: z
|
||||
.array(ChartPointSchema)
|
||||
.min(7)
|
||||
.max(7)
|
||||
.default([
|
||||
{ label: "12:00", value: 22 },
|
||||
{ label: "13:00", value: 64 },
|
||||
{ label: "14:00", value: 48 },
|
||||
{ label: "15:00", value: 56 },
|
||||
{ label: "16:00", value: 41 },
|
||||
{ label: "17:00", value: 58 },
|
||||
{ label: "18:00", value: 63 },
|
||||
])
|
||||
.meta({
|
||||
description: "Area chart data shown in the bottom-left dashboard cell.",
|
||||
}),
|
||||
shareBreakdown: z
|
||||
.array(PieSegmentSchema)
|
||||
.min(3)
|
||||
.max(3)
|
||||
.default([
|
||||
{ name: "Category A", value: 50 },
|
||||
{ name: "Category B", value: 30 },
|
||||
{ name: "Category C", value: 20 },
|
||||
])
|
||||
.meta({
|
||||
description: "Pie chart data shown in the bottom-center dashboard cell.",
|
||||
}),
|
||||
comparisonBars: z
|
||||
.array(ChartPointSchema)
|
||||
.min(7)
|
||||
.max(7)
|
||||
.default([
|
||||
{ label: "Jan", value: 70 },
|
||||
{ label: "Feb", value: 170 },
|
||||
{ label: "Mar", value: 110 },
|
||||
{ label: "Apr", value: 42 },
|
||||
{ label: "May", value: 88 },
|
||||
{ label: "Jun", value: 106 },
|
||||
{ label: "Jul", value: 112 },
|
||||
])
|
||||
.meta({
|
||||
description: "Bar chart data shown in the bottom-right dashboard cell.",
|
||||
}),
|
||||
charts: z.array(ChartItemSchema).min(1).max(6).default([
|
||||
{ type: 'bar', data: [{ name: 'Q1', value: 125000 }, { name: 'Q2', value: 158000 }, { name: 'Q3', value: 142000 }, { name: 'Q4', value: 189000 }] },
|
||||
{ type: 'donut', data: [{ name: 'North America', value: 35 }, { name: 'Europe', value: 28 }, { name: 'Asia Pacific', value: 25 }, { name: 'Others', value: 12 }] },
|
||||
{ type: 'line', data: [{ name: 'Jan', value: 30 }, { name: 'Feb', value: 45 }, { name: 'Mar', value: 52 }, { name: 'Apr', value: 48 }, { name: 'May', value: 67 }, { name: 'Jun', value: 82 }] },
|
||||
{ type: 'bar', data: [{ name: 'Sales', value: 87 }, { name: 'Marketing', value: 72 }, { name: 'Engineering', value: 95 }, { name: 'Support', value: 68 }] },
|
||||
{ type: 'bar-clustered', data: [{ name: 'Q1', values: { 'Product A': 45, 'Product B': 62 } }, { name: 'Q2', values: { 'Product A': 58, 'Product B': 71 } }, { name: 'Q3', values: { 'Product A': 72, 'Product B': 65 } }], series: ['Product A', 'Product B'] },
|
||||
{ type: 'bar-diverging', data: [{ name: 'Quality', positive: 78, negative: 22 }, { name: 'Service', positive: 65, negative: 35 }, { name: 'Price', positive: 42, negative: 58 }], series: ['Satisfied', 'Unsatisfied'] },
|
||||
]),
|
||||
});
|
||||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const getChartHeight = (count: number, hasMetrics: boolean) => {
|
||||
if (count <= 2) return hasMetrics ? 230 : 280;
|
||||
if (count <= 3) return hasMetrics ? 210 : 260;
|
||||
return hasMetrics ? 160 : 180;
|
||||
};
|
||||
|
||||
|
||||
function SummaryCard({
|
||||
value,
|
||||
label,
|
||||
|
|
@ -184,164 +140,96 @@ function SummaryCard({
|
|||
iconAlt?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex h-[74px] items-center rounded-[14px] bg-white px-[16px]">
|
||||
<div className="flex h-[22px] w-[22px] shrink-0 items-center justify-center rounded-full bg-[#4d4ef3] text-white">
|
||||
<div className="flex gap-[10px] items-center rounded-[14px] py-[9px]">
|
||||
<div className="flex h-[36px] w-[36px] border border-[#ECF5FE] shrink-0 items-center justify-center rounded-full bg-[#ECF5FE] ">
|
||||
<img
|
||||
src={iconUrl ?? ""}
|
||||
alt={iconAlt ?? ""}
|
||||
className="h-[10px] w-[10px] object-contain"
|
||||
style={{ filter: "brightness(0) invert(1)" }}
|
||||
className="h-[18px] w-[18px] object-contain"
|
||||
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-[10px]">
|
||||
<p className="text-[22px] leading-none tracking-[-0.04em] text-[#232223]">
|
||||
{value}
|
||||
</p>
|
||||
<p className="mt-[4px] text-[12px] leading-none text-[#535665]">{label}</p>
|
||||
<div className="">
|
||||
<p className="text-[18px] leading-none tracking-[-0.04em] text-[#4A4D53]">{value}</p>
|
||||
<p className="mt-[4px] text-[14px] leading-none text-[#6C6C6C]">{label}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ChartCell({
|
||||
children,
|
||||
footer,
|
||||
topLegend,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
footer?: ReactNode;
|
||||
topLegend?: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex h-full flex-col px-[10px] py-[10px]">
|
||||
{topLegend && <div className="mb-[4px] flex justify-center">{topLegend}</div>}
|
||||
<div className="min-h-0 flex-1">{children}</div>
|
||||
{footer && <div className="mt-[4px] flex justify-center">{footer}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DotLegend({
|
||||
items,
|
||||
}: {
|
||||
items: { label: string; color: string }[];
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-wrap items-center justify-center gap-[10px] text-[8px] text-[#6b7280]">
|
||||
{items.map((item) => (
|
||||
<span key={item.label} className="flex items-center gap-[4px]">
|
||||
<span
|
||||
className="block h-[6px] w-[6px] rounded-full"
|
||||
style={{ backgroundColor: item.color }}
|
||||
/>
|
||||
{item.label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
const DataAnalysisDashboardSlide = ({ data }: { data: Partial<SchemaType> }) => {
|
||||
|
||||
const { title, summaryIcon, summaryCards, workflowBars, gaugeSegments, trendSeries, detailedArea, shareBreakdown, comparisonBars } = data;
|
||||
const { title, summaryCards, charts } = data;
|
||||
const halfChart = charts?.slice(0, Math.ceil(charts.length / 2));
|
||||
const otherHalfChart = charts?.slice(Math.ceil(charts.length / 2));
|
||||
|
||||
return (
|
||||
<div className="relative h-[720px] w-[1280px] overflow-hidden rounded-[24px] bg-[#f9f8f8]">
|
||||
<div
|
||||
className="absolute left-0 top-0 w-[42px] rounded-b-[22px] bg-[#4d4ef3]"
|
||||
style={{ height: 188 }}
|
||||
/>
|
||||
<div className="relative flex flex-col h-[720px] w-[1280px] overflow-hidden bg-[#F9F8F8]">
|
||||
<div className="absolute left-0 top-0 w-[42px] rounded-b-[22px] bg-[#157CFF]" style={{ height: 185 }} />
|
||||
|
||||
<div className="px-[74px] pt-[44px]">
|
||||
<h2 className="text-[78px] font-semibold leading-none tracking-[-0.06em] text-[#232223]">
|
||||
{title}
|
||||
</h2>
|
||||
<div className="px-[64px] pt-[48px]">
|
||||
<h2 className="text-[80px] font-bold leading-[108.4%] tracking-[-2.419px] text-[#232223]">{title}</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 gap-[16px] px-[74px] pt-[14px]">
|
||||
{summaryCards && summaryCards.length > 0 && <div className=" mx-[64px] grid bg-white gap-[16px] p-[13px] mt-[22px] rounded-[14px] "
|
||||
|
||||
style={{ gridTemplateColumns: `repeat(${summaryCards.length}, minmax(220px, 1fr))` }}>
|
||||
{summaryCards?.map((card, index) => (
|
||||
<SummaryCard
|
||||
key={`${card.label}-${index}`}
|
||||
value={card.value}
|
||||
label={card.label}
|
||||
iconUrl={summaryIcon?.__icon_url__}
|
||||
iconAlt={summaryIcon?.__icon_query__}
|
||||
iconUrl={card.icon?.__icon_url__}
|
||||
iconAlt={card.icon?.__icon_query__}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>}
|
||||
<div className="flex-1 flex flex-col pb-[30px]">
|
||||
|
||||
<div className="flex flex-col gap-[12px] px-[74px] pt-[12px]">
|
||||
<div className="grid h-[168px] grid-cols-3 divide-x divide-[#ecf0f6] rounded-[16px] bg-white">
|
||||
<ChartCell
|
||||
footer={
|
||||
<DotLegend items={[{ label: "Traditional Workflow", color: "#4d4ef3" }]} />
|
||||
}
|
||||
{halfChart && halfChart.length > 0 && <div className="mt-[14px] px-[64px] flex-1">
|
||||
<div
|
||||
className={`grid h-full bg-white p-[13px] rounded-[14px] min-h-0 gap-[10px] grid-cols-3`}
|
||||
style={{ gridAutoRows: `minmax(150px, 1fr)` }}
|
||||
>
|
||||
<CompactBarChart data={workflowBars ?? []} />
|
||||
</ChartCell>
|
||||
{halfChart?.map((chart, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="rounded-[6px] flex flex-col overflow-hidden"
|
||||
|
||||
<ChartCell
|
||||
footer={
|
||||
<DotLegend
|
||||
items={[
|
||||
{ label: "Category A", color: "#4d4ef3" },
|
||||
{ label: "Category B", color: "#9fb6ff" },
|
||||
{ label: "Category C", color: "#e8eefb" },
|
||||
]}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<SemiDonutChart data={gaugeSegments ?? []} />
|
||||
</ChartCell>
|
||||
>
|
||||
|
||||
<ChartCell
|
||||
topLegend={
|
||||
<div className="flex gap-[10px] text-[8px] text-[#6b7280]">
|
||||
<p>Category A</p>
|
||||
<p>Category B</p>
|
||||
<div className="flex-1 " >
|
||||
<FlexibleReportChart density="compact" chartType={chart.type} data={chart.data} series={chart.series} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
))}
|
||||
</div>
|
||||
</div>}
|
||||
{otherHalfChart && otherHalfChart.length > 0 && <div className="mt-[14px] px-[64px] flex-1">
|
||||
<div
|
||||
className={`grid h-full bg-white p-[13px] rounded-[14px] min-h-0 gap-[10px] grid-cols-3`}
|
||||
style={{ gridAutoRows: `minmax(150px, 1fr)` }}
|
||||
>
|
||||
<TrendLineChart data={trendSeries ?? []} />
|
||||
</ChartCell>
|
||||
</div>
|
||||
{otherHalfChart?.map((chart, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="rounded-[6px] flex flex-col overflow-hidden"
|
||||
|
||||
<div className="grid h-[168px] grid-cols-3 divide-x divide-[#ecf0f6] rounded-[16px] bg-white">
|
||||
<ChartCell
|
||||
footer={
|
||||
<DotLegend items={[{ label: "Detailed Workflow", color: "#4d4ef3" }]} />
|
||||
}
|
||||
>
|
||||
<AreaTrendChart data={detailedArea ?? []} idPrefix="dashboard-area" />
|
||||
</ChartCell>
|
||||
>
|
||||
|
||||
<ChartCell
|
||||
footer={
|
||||
<DotLegend
|
||||
items={[
|
||||
{ label: "Category A", color: "#4d4ef3" },
|
||||
{ label: "Category B", color: "#9fb6ff" },
|
||||
{ label: "Category C", color: "#d7dff4" },
|
||||
]}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<CompactPieChart data={shareBreakdown ?? []} />
|
||||
</ChartCell>
|
||||
<div className="flex-1 " >
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<FlexibleReportChart density="compact" chartType={chart.type} data={chart.data} series={chart.series} />
|
||||
</ResponsiveContainer>
|
||||
|
||||
<ChartCell
|
||||
footer={
|
||||
<DotLegend
|
||||
items={[
|
||||
{ label: "Category A", color: "#4d4ef3" },
|
||||
{ label: "Category B", color: "#9fb6ff" },
|
||||
]}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<CompactBarChart data={comparisonBars ?? []} />
|
||||
</ChartCell>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,57 +1,54 @@
|
|||
"use client";
|
||||
|
||||
import * as z from "zod";
|
||||
|
||||
import { ResponsiveContainer } from "recharts";
|
||||
|
||||
import { WorkflowBarChart } from "./chartPrimitives";
|
||||
|
||||
const ChartPointSchema = z.object({
|
||||
label: z.string().min(1).max(12).meta({
|
||||
description: "Chart axis label.",
|
||||
}),
|
||||
value: z.number().min(0).max(1000).meta({
|
||||
description: "Bar chart value.",
|
||||
}),
|
||||
});
|
||||
import { FlexibleReportChart, flexibleChartDataSchema } from "./flexibleReportChart";
|
||||
|
||||
export const slideLayoutId = "data-analysis-insight-bar-slide";
|
||||
export const slideLayoutName = "Data Analysis Insight Bar Slide";
|
||||
export const slideLayoutDescription =
|
||||
"A slide with a title at the top, a single featured insight block on the left containing an icon badge and a paragraph, and a bar chart on the right with a legend below it.";
|
||||
"A slide with a title at the top, a single featured insight block on the left containing an icon badge and a paragraph, and a chart on the right with a legend below it.";
|
||||
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().min(3).max(28).default("Data Analysis").meta({
|
||||
title: z.string().min(3).max(12).default("Data Analysis").meta({
|
||||
description: "Slide title shown at the top-left.",
|
||||
}),
|
||||
insightIcon: z.object({
|
||||
__icon_url__: z.string().default("https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg"),
|
||||
__icon_query__: z.string().default("pulse icon"),
|
||||
}).default({
|
||||
__icon_url__:
|
||||
"https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
|
||||
__icon_query__: "pulse icon",
|
||||
}).meta({
|
||||
description: "Icon shown in the featured insight badge.",
|
||||
}),
|
||||
insightBody: z.string().min(80).max(320).default(
|
||||
"Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut alut enim ad minima veniam, quis. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut alut enim ad minima veniam, quis"
|
||||
).meta({
|
||||
description: "Featured insight paragraph shown in the left content area.",
|
||||
}),
|
||||
chartData: z
|
||||
.array(ChartPointSchema)
|
||||
.min(7)
|
||||
.max(7)
|
||||
.default([
|
||||
{ label: "Mon", value: 120 },
|
||||
{ label: "Tue", value: 200 },
|
||||
{ label: "Wed", value: 150 },
|
||||
{ label: "Thu", value: 80 },
|
||||
{ label: "Fri", value: 70 },
|
||||
{ label: "Sat", value: 110 },
|
||||
{ label: "Sun", value: 130 },
|
||||
])
|
||||
insightIcon: z
|
||||
.object({
|
||||
__icon_url__: z
|
||||
.string()
|
||||
.default("https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg"),
|
||||
__icon_query__: z.string().default("pulse icon"),
|
||||
})
|
||||
.default({
|
||||
__icon_url__: "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
|
||||
__icon_query__: "pulse icon",
|
||||
})
|
||||
.meta({
|
||||
description: "Weekly values shown in the right-side bar chart.",
|
||||
description: "Icon shown in the featured insight badge.",
|
||||
}),
|
||||
insightBody: z
|
||||
.string()
|
||||
.min(80)
|
||||
.max(320)
|
||||
.default(
|
||||
"Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut alut enim ad minima veniam, quis. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut alut enim ad minima veniam, quis"
|
||||
)
|
||||
.meta({
|
||||
description: "Featured insight paragraph shown in the left content area.",
|
||||
}),
|
||||
chartData: flexibleChartDataSchema.default({
|
||||
type: "pie",
|
||||
data: [
|
||||
{ name: "Q1", value: 45 },
|
||||
{ name: "Q2", value: 72 },
|
||||
{ name: "Q3", value: 58 },
|
||||
{ name: "Q4", value: 89 },
|
||||
],
|
||||
}),
|
||||
legendLabel: z.string().min(3).max(32).default("Traditional Workflow").meta({
|
||||
description: "Legend label shown below the chart.",
|
||||
}),
|
||||
|
|
@ -64,46 +61,40 @@ const DataAnalysisInsightBarSlide = ({
|
|||
}: {
|
||||
data: Partial<SchemaType>;
|
||||
}) => {
|
||||
|
||||
const { title, insightIcon, insightBody, chartData, legendLabel } = data;
|
||||
const chartData = data?.chartData?.data ?? [];
|
||||
const chartType = data?.chartData?.type ?? "bar";
|
||||
const series = data?.chartData?.series ?? [];
|
||||
|
||||
return (
|
||||
<div className="relative h-[720px] w-[1280px] overflow-hidden rounded-[24px] bg-[#f9f8f8]">
|
||||
<div
|
||||
className="absolute left-0 top-0 w-[42px] rounded-b-[22px] bg-[#157CFF]"
|
||||
style={{ height: 185 }}
|
||||
/>
|
||||
<div className="absolute left-0 top-0 w-[42px] rounded-b-[22px] bg-[#157CFF]" style={{ height: 185 }} />
|
||||
|
||||
<div className="px-[64px] pt-[48px]">
|
||||
<h2 className="text-[80px] font-bold leading-[108.4%] tracking-[-2.419px] text-[#232223]">
|
||||
{title}
|
||||
</h2>
|
||||
<h2 className="text-[80px] font-bold leading-[108.4%] tracking-[-2.419px] text-[#232223]">{data.title}</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between px-[74px] pt-[96px]">
|
||||
<div className="w-[380px] pt-[24px]">
|
||||
<div className="flex justify-between px-[74px] gap-10 pt-[96px]">
|
||||
<div className=" pt-[24px] w-1/2">
|
||||
<div className="flex items-center gap-[14px]">
|
||||
<div className="flex h-[55px] w-[55px] items-center justify-center rounded-full bg-[#157CFF] text-white">
|
||||
<img
|
||||
src={insightIcon?.__icon_url__}
|
||||
alt={insightIcon?.__icon_query__}
|
||||
src={data.insightIcon?.__icon_url__}
|
||||
alt={data.insightIcon?.__icon_query__}
|
||||
className="h-[25px] w-[25px] object-contain"
|
||||
|
||||
style={{ filter: "invert(1)" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-[20px] text-[24px] leading-[26.667px] text-[#232223]">
|
||||
{insightBody}
|
||||
</p>
|
||||
<p className="mt-[20px] text-[24px] leading-[26.667px] text-[#232223]">{data.insightBody}</p>
|
||||
</div>
|
||||
|
||||
<div className="ml-[28px] flex flex-col items-center">
|
||||
<div className="h-[346px] w-[560px]">
|
||||
<WorkflowBarChart data={chartData ?? []} />
|
||||
</div>
|
||||
<div className="ml-[28px] flex w-1/2 flex-col items-center">
|
||||
<ResponsiveContainer height={400} width="100%">
|
||||
<FlexibleReportChart chartType={chartType} data={chartData} series={series} colorFallback="#157CFF" />
|
||||
</ResponsiveContainer>
|
||||
<div className="mt-[12px] flex items-center gap-[10px] text-[24px] tracking-[-0.03em] text-[#157CFF]">
|
||||
<span className="h-[12px] w-[12px] rounded-full bg-[#157CFF]" />
|
||||
<p>{legendLabel}</p>
|
||||
<p>{data.legendLabel}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,11 @@
|
|||
"use client";
|
||||
|
||||
import { Fragment } from "react/jsx-runtime";
|
||||
import * as z from "zod";
|
||||
|
||||
import { DualLineChart } from "./chartPrimitives";
|
||||
import { ResponsiveContainer } from "recharts";
|
||||
|
||||
const LinePointSchema = z.object({
|
||||
label: z.string().min(1).max(12).meta({
|
||||
description: "Chart axis label.",
|
||||
}),
|
||||
valueA: z.number().min(0).max(1000).meta({
|
||||
description: "First series value.",
|
||||
}),
|
||||
valueB: z.number().min(0).max(1000).meta({
|
||||
description: "Second series value.",
|
||||
}),
|
||||
});
|
||||
import { FlexibleReportChart, flexibleChartDataSchema } from "./flexibleReportChart";
|
||||
|
||||
const MetricSchema = z.object({
|
||||
value: z.string().min(1).max(12).meta({
|
||||
|
|
@ -38,7 +31,7 @@ export const slideLayoutDescription =
|
|||
"A slide with a title at the top, a two-series line chart in the left content area, and two tall metric cards arranged side by side on the right. Each metric card contains two stacked metric blocks.";
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().min(3).max(28).default("Data Analysis").meta({
|
||||
title: z.string().min(3).max(12).default("Data Analysis").meta({
|
||||
description: "Slide title shown at the top-left.",
|
||||
}),
|
||||
seriesALabel: z.string().min(3).max(20).default("Category A").meta({
|
||||
|
|
@ -47,11 +40,9 @@ export const Schema = z.object({
|
|||
seriesBLabel: z.string().min(3).max(20).default("Category B").meta({
|
||||
description: "Legend label for the second line series.",
|
||||
}),
|
||||
lineData: z
|
||||
.array(LinePointSchema)
|
||||
.min(7)
|
||||
.max(7)
|
||||
.default([
|
||||
chartData: flexibleChartDataSchema.default({
|
||||
type: "pie",
|
||||
data: [
|
||||
{ label: "label", valueA: 24, valueB: 40 },
|
||||
{ label: "label", valueA: 55, valueB: 72 },
|
||||
{ label: "label", valueA: 50, valueB: 98 },
|
||||
|
|
@ -59,10 +50,11 @@ export const Schema = z.object({
|
|||
{ label: "label", valueA: 70, valueB: 52 },
|
||||
{ label: "label", valueA: 42, valueB: 78 },
|
||||
{ label: "label", valueA: 63, valueB: 51 },
|
||||
])
|
||||
.meta({
|
||||
description: "Line chart data displayed on the left side of the slide.",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
legendLabel: z.string().min(3).max(32).default("Traditional Workflow").meta({
|
||||
description: "Legend label shown below the chart.",
|
||||
}),
|
||||
statColumns: z
|
||||
.array(StatColumnSchema)
|
||||
.min(2)
|
||||
|
|
@ -94,60 +86,41 @@ type StatMetric = {
|
|||
description: string;
|
||||
};
|
||||
|
||||
function StatPill({
|
||||
metrics,
|
||||
|
||||
}: {
|
||||
metrics: StatMetric[];
|
||||
|
||||
}) {
|
||||
|
||||
|
||||
function StatPill({ metrics }: { metrics: StatMetric[] }) {
|
||||
return (
|
||||
<div className=" h-[438px] w-[248px] overflow-hidden rounded-[127px] bg-[#157CFF] px-[28px] py-[74px] text-center text-white">
|
||||
|
||||
<div className="h-[438px] w-[248px] overflow-hidden rounded-[127px] bg-[#157CFF] px-[28px] py-[74px] text-center text-white">
|
||||
{metrics.map((metric, index) => (
|
||||
<>
|
||||
<div
|
||||
key={`${metric.value}-${metric.label}-${index}`}
|
||||
className={``}
|
||||
>
|
||||
<p className="text-[55px] font-medium leading-[ 44.353px] tracking-[-1.09px]">
|
||||
{metric.value}
|
||||
</p>
|
||||
<Fragment key={`${metric.value}-${metric.label}-${index}`}>
|
||||
<div key={`${metric.value}-${metric.label}-${index}`} className={``}>
|
||||
<p className="text-[55px] font-medium leading-[ 44.353px] tracking-[-1.09px]">{metric.value}</p>
|
||||
<p className="mt-[6px] text-[20px] font-medium leading-none">{metric.label}</p>
|
||||
<p className=" text-[20px] leading-[1.15] text-white/90">
|
||||
{metric.description}
|
||||
</p>
|
||||
<p className="text-[20px] leading-[1.15] text-white/90">{metric.description}</p>
|
||||
</div>
|
||||
{index === 0 && <div className="py-[22px]">
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="181" height="1" viewBox="0 0 181 1" fill="none">
|
||||
<path opacity="0.2" d="M0 0.487305H180.122" stroke="white" strokeWidth="0.974913" strokeDasharray="3.9 1.95" />
|
||||
</svg>
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
{index === 0 && (
|
||||
<div className="py-[22px]">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="181" height="1" viewBox="0 0 181 1" fill="none">
|
||||
<path opacity="0.2" d="M0 0.487305H180.122" stroke="white" strokeWidth="0.974913" strokeDasharray="3.9 1.95" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const DataAnalysisLineStatsSlide = ({ data }: { data: Partial<SchemaType> }) => {
|
||||
const { title, seriesALabel, seriesBLabel, lineData, statColumns } = data;
|
||||
const { title, seriesALabel, seriesBLabel, chartData, statColumns, legendLabel } = data;
|
||||
const rows = chartData?.data ?? [];
|
||||
const chartType = chartData?.type ?? "line-dual";
|
||||
const series = chartData?.series ?? [];
|
||||
|
||||
return (
|
||||
<div className="relative h-[720px] w-[1280px] overflow-hidden rounded-[24px] bg-[#f9f8f8]">
|
||||
<div
|
||||
className="absolute left-0 top-0 w-[42px] rounded-b-[22px] bg-[#157CFF]"
|
||||
style={{ height: 185 }}
|
||||
/>
|
||||
<div className="absolute left-0 top-0 w-[42px] rounded-b-[22px] bg-[#157CFF]" style={{ height: 185 }} />
|
||||
|
||||
<div className="px-[64px] pt-[48px]">
|
||||
<h2 className="text-[80px] font-bold leading-[108.4%] tracking-[-2.419px] text-[#232223]">
|
||||
{title}
|
||||
</h2>
|
||||
<h2 className="text-[80px] font-bold leading-[108.4%] tracking-[-2.419px] text-[#232223]">{title}</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between px-[74px] pt-[40px]">
|
||||
|
|
@ -164,11 +137,20 @@ const DataAnalysisLineStatsSlide = ({ data }: { data: Partial<SchemaType> }) =>
|
|||
</div>
|
||||
|
||||
<div className="mt-[12px] h-[356px] w-full">
|
||||
<DualLineChart data={lineData ?? []} />
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<FlexibleReportChart
|
||||
chartType={chartType}
|
||||
data={rows}
|
||||
series={series}
|
||||
colorFallback="#157CFF"
|
||||
dualLineColors={["#9fb6ff", "#4d4ef3"]}
|
||||
/>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
<div className="mt-[2px] text-center text-[18px] text-[#4b5563]">
|
||||
X axis name
|
||||
<div className="mt-[12px] flex items-center gap-[10px] text-center justify-center text-[24px] tracking-[-0.03em] text-[#157CFF]">
|
||||
<span className="h-[12px] w-[12px] rounded-full bg-[#157CFF]" />
|
||||
<p>{data.legendLabel}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@ import * as z from "zod";
|
|||
|
||||
|
||||
const AnalysisItemSchema = z.object({
|
||||
title: z.string().min(3).max(18).meta({
|
||||
title: z.string().max(12).meta({
|
||||
description: "Short item title displayed next to the icon.",
|
||||
}),
|
||||
description: z.string().min(20).max(84).meta({
|
||||
description: z.string().max(30).meta({
|
||||
description: "Supporting sentence shown below the title.",
|
||||
}),
|
||||
});
|
||||
|
|
@ -16,7 +16,7 @@ export const slideLayoutDescription =
|
|||
"A slide with a title at the top and a two-column list of analysis points underneath. Each point contains a small circular icon badge, a short title on the same row, and a supporting description directly below.";
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().min(3).max(28).default("Data Analysis").meta({
|
||||
title: z.string().min(3).max(12).default("Data Analysis").meta({
|
||||
description: "Slide title shown at the top-left.",
|
||||
}),
|
||||
itemIcon: z.object({
|
||||
|
|
@ -31,7 +31,7 @@ export const Schema = z.object({
|
|||
}),
|
||||
items: z
|
||||
.array(AnalysisItemSchema)
|
||||
.min(6)
|
||||
|
||||
.max(6)
|
||||
.default([
|
||||
{ title: "Title 1", description: "Ut enim ad minima veniam, quis." },
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import * as z from "zod";
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().min(1).default("Company's "),
|
||||
subtitle: z.string().min(1).default("Report"),
|
||||
name: z.string().min(1).default("John Doe"),
|
||||
position: z.string().min(1).default("Company Name | Strategy, Content, growth"),
|
||||
title: z.string().min(1).max(12).default("Company's "),
|
||||
subtitle: z.string().min(1).max(15).default("Report"),
|
||||
name: z.string().min(1).max(10).default("John Doe"),
|
||||
position: z.string().min(1).max(20).default("Company Name | Strategy, Content, growth"),
|
||||
})
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
export const slideLayoutId = "intro-slide";
|
||||
|
|
|
|||
|
|
@ -7,17 +7,17 @@ export const slideLayoutDescription =
|
|||
"A slide with a title at the top-left, a paragraph block beneath the title, a short bulleted list in the lower-left area, and a large supporting image anchored on the right side of the slide.";
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().min(3).max(32).default("Introduction").meta({
|
||||
title: z.string().min(3).max(12).default("Introduction").meta({
|
||||
description: "Slide title shown at the top-left.",
|
||||
}),
|
||||
body: z.string().min(60).max(280).default(
|
||||
body: z.string().max(250).default(
|
||||
"Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut alut enim ad minima veniam, quis. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut alut enim ad minima veniam, quis"
|
||||
).meta({
|
||||
description: "Primary paragraph shown under the title.",
|
||||
}),
|
||||
bullets: z
|
||||
.array(z.string().min(20).max(80))
|
||||
.min(4)
|
||||
.array(z.string().max(35))
|
||||
|
||||
.max(4)
|
||||
.default([
|
||||
"Ut enim ad minima veniam, quis nostrum",
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
import { Fragment } from "react/jsx-runtime";
|
||||
import * as z from "zod";
|
||||
|
||||
const MetricSchema = z.object({
|
||||
value: z.string().min(1).max(12).meta({
|
||||
value: z.string().min(1).max(6).meta({
|
||||
description: "Primary metric value shown in the card.",
|
||||
}),
|
||||
label: z.string().min(3).max(24).meta({
|
||||
label: z.string().min(3).max(10).meta({
|
||||
description: "Short metric label shown below the value.",
|
||||
}),
|
||||
description: z.string().min(6).max(36).meta({
|
||||
description: z.string().min(6).max(20).meta({
|
||||
description: "Supporting text shown below the label.",
|
||||
}),
|
||||
});
|
||||
|
|
@ -24,17 +25,17 @@ export const slideLayoutDescription =
|
|||
"A slide with a title and explanatory text on the left, a bulleted list underneath the text, and two tall metric cards placed side by side on the right. Each metric card contains two stacked metric blocks.";
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().min(3).max(32).default("Introduction").meta({
|
||||
title: z.string().min(3).max(12).default("Introduction").meta({
|
||||
description: "Slide title shown at the top-left.",
|
||||
}),
|
||||
body: z.string().min(60).max(320).default(
|
||||
body: z.string().max(250).default(
|
||||
"Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut alut enim ad minima veniam, quis. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut alut enim ad minima veniam, quis"
|
||||
).meta({
|
||||
description: "Primary paragraph shown below the title.",
|
||||
}),
|
||||
bullets: z
|
||||
.array(z.string().min(20).max(80))
|
||||
.min(4)
|
||||
.array(z.string().max(35))
|
||||
|
||||
.max(4)
|
||||
.default([
|
||||
"Ut enim ad minima veniam, quis nostrum",
|
||||
|
|
@ -47,7 +48,7 @@ export const Schema = z.object({
|
|||
}),
|
||||
statColumns: z
|
||||
.array(StatColumnSchema)
|
||||
.min(2)
|
||||
|
||||
.max(2)
|
||||
.default([
|
||||
{
|
||||
|
|
@ -89,7 +90,7 @@ function StatPill({
|
|||
<div className=" h-[438px] w-[248px] overflow-hidden rounded-[127px] bg-[#157CFF] px-[28px] py-[74px] text-center text-white">
|
||||
|
||||
{metrics.map((metric, index) => (
|
||||
<>
|
||||
<Fragment key={`${metric.value}-${metric.label}-${index}`}>
|
||||
<div
|
||||
key={`${metric.value}-${metric.label}-${index}`}
|
||||
className={``}
|
||||
|
|
@ -109,7 +110,7 @@ function StatPill({
|
|||
</svg>
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
</Fragment>
|
||||
))}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ const MilestoneItemSchema = z.object({
|
|||
stepNumber: z.string().min(2).max(4).meta({
|
||||
description: "Short milestone number such as 01 or 05.",
|
||||
}),
|
||||
heading: z.string().min(3).max(18).meta({
|
||||
heading: z.string().min(3).max(10).meta({
|
||||
description: "Heading displayed below the milestone marker.",
|
||||
}),
|
||||
description: z.string().min(20).max(80).meta({
|
||||
description: z.string().min(20).max(50).meta({
|
||||
description: "Supporting milestone description shown under the heading.",
|
||||
}),
|
||||
});
|
||||
|
|
@ -18,7 +18,7 @@ export const slideLayoutDescription =
|
|||
"A slide with a title at the top and a single horizontal milestone sequence below it. The sequence contains five circular markers aligned in one row, and each marker has a heading and description placed directly underneath. The activeIndex field controls which marker is emphasized while the remaining markers stay in the default state.";
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().min(3).max(24).default("Milestone").meta({
|
||||
title: z.string().min(3).max(12).default("Milestone").meta({
|
||||
description: "Slide title shown at the top-left.",
|
||||
}),
|
||||
activeIndex: z.number().int().min(0).max(4).default(4).meta({
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
import { Fragment } from "react/jsx-runtime";
|
||||
import * as z from "zod";
|
||||
|
||||
const MetricSchema = z.object({
|
||||
value: z.string().min(1).max(12).meta({
|
||||
value: z.string().min(1).max(6).meta({
|
||||
description: "Primary metric value shown in the pill.",
|
||||
}),
|
||||
label: z.string().min(3).max(24).meta({
|
||||
label: z.string().min(3).max(10).meta({
|
||||
description: "Short label shown below the metric value.",
|
||||
}),
|
||||
description: z.string().min(6).max(36).meta({
|
||||
description: z.string().min(6).max(20).meta({
|
||||
description: "Supporting metric description shown below the label.",
|
||||
}),
|
||||
});
|
||||
|
|
@ -24,7 +25,7 @@ export const slideLayoutDescription =
|
|||
"A slide with a title at the top and three tall metric cards arranged horizontally below it. Each card can contain one or two stacked metric blocks, and each block includes a main value, a label, and a supporting description.";
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().min(3).max(40).default("Performance Snapshot").meta({
|
||||
title: z.string().min(3).max(12).default("Performance Snapshot").meta({
|
||||
description: "Slide title shown at the top-left.",
|
||||
}),
|
||||
columns: z
|
||||
|
|
@ -82,7 +83,7 @@ function StatPill({
|
|||
<div className=" h-[438px] w-[248px] overflow-hidden rounded-[127px] bg-[#157CFF] px-[28px] py-[74px] text-center text-white">
|
||||
|
||||
{metrics.map((metric, index) => (
|
||||
<>
|
||||
<Fragment key={`${metric.value}-${metric.label}-${index}`}>
|
||||
<div
|
||||
key={`${metric.value}-${metric.label}-${index}`}
|
||||
className={``}
|
||||
|
|
@ -102,7 +103,7 @@ function StatPill({
|
|||
</svg>
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
</Fragment>
|
||||
))}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ const ServiceItemSchema = z.object({
|
|||
heading: z.string().min(3).max(18).meta({
|
||||
description: "Heading shown below the service icon.",
|
||||
}),
|
||||
description: z.string().min(20).max(84).meta({
|
||||
description: z.string().min(20).max(50).meta({
|
||||
description: "Supporting description below the service heading.",
|
||||
}),
|
||||
});
|
||||
|
|
@ -27,7 +27,7 @@ export const slideLayoutDescription =
|
|||
"A slide with a title and a three-step horizontal service flow. Each step contains a circular icon area, a heading, and a description placed underneath. Directional connectors between the circles indicate sequence, and the activeIndex field determines which step is emphasized.";
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().min(3).max(24).default("Services").meta({
|
||||
title: z.string().min(3).max(12).default("Services").meta({
|
||||
description: "Slide title shown at the top-left.",
|
||||
}),
|
||||
|
||||
|
|
|
|||
|
|
@ -10,13 +10,13 @@ const CardSchema = z.object({
|
|||
stepNumber: z.string().min(2).max(4).meta({
|
||||
description: "Short card step number such as 01, 02, or 03.",
|
||||
}),
|
||||
description: z.string().min(20).max(90).meta({
|
||||
description: z.string().min(20).max(50).meta({
|
||||
description: "Card body copy displayed inside the feature pill.",
|
||||
}),
|
||||
});
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().min(3).max(24).default("Solution").meta({
|
||||
title: z.string().min(3).max(12).default("Solution").meta({
|
||||
description: "Slide heading shown in the top-left corner.",
|
||||
}),
|
||||
showImage: z.boolean().default(true).meta({
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@ import * as z from "zod";
|
|||
|
||||
|
||||
const MemberSchema = z.object({
|
||||
title: z.string().min(2).max(24).meta({
|
||||
title: z.string().min(2).max(12).meta({
|
||||
description: "Short role or title shown above the member name.",
|
||||
}),
|
||||
name: z.string().min(2).max(32).meta({
|
||||
name: z.string().min(2).max(20).meta({
|
||||
description: "Member name shown at the bottom of the card.",
|
||||
}),
|
||||
image: z.object({
|
||||
|
|
@ -25,7 +25,7 @@ export const slideLayoutDescription =
|
|||
export const Schema = z.object({
|
||||
members: z
|
||||
.array(MemberSchema)
|
||||
.min(5)
|
||||
.min(2)
|
||||
.max(5)
|
||||
.default([
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,338 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import {
|
||||
Area,
|
||||
AreaChart,
|
||||
Bar,
|
||||
BarChart,
|
||||
CartesianGrid,
|
||||
Cell,
|
||||
LabelList,
|
||||
Line,
|
||||
LineChart,
|
||||
Pie,
|
||||
PieChart,
|
||||
ResponsiveContainer,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
|
||||
type SimpleBarDatum = {
|
||||
label: string;
|
||||
value: number;
|
||||
};
|
||||
|
||||
type DualSeriesDatum = {
|
||||
label: string;
|
||||
valueA: number;
|
||||
valueB: number;
|
||||
};
|
||||
|
||||
type PieDatum = {
|
||||
name: string;
|
||||
value: number;
|
||||
};
|
||||
|
||||
type PieLabelProps = {
|
||||
cx?: number;
|
||||
cy?: number;
|
||||
midAngle?: number;
|
||||
innerRadius?: number;
|
||||
outerRadius?: number;
|
||||
percent?: number;
|
||||
};
|
||||
|
||||
const PRIMARY = "#4d4ef3";
|
||||
const SECONDARY = "#9fb6ff";
|
||||
const LIGHT = "#e8eefb";
|
||||
const GRID = "#8f96aa";
|
||||
|
||||
function renderOutsidePieLabel({
|
||||
cx = 0,
|
||||
cy = 0,
|
||||
midAngle = 0,
|
||||
outerRadius = 0,
|
||||
percent = 0,
|
||||
}: PieLabelProps) {
|
||||
const radius = outerRadius + 18;
|
||||
const x = cx + radius * Math.cos((-midAngle * Math.PI) / 180);
|
||||
const y = cy + radius * Math.sin((-midAngle * Math.PI) / 180);
|
||||
|
||||
return (
|
||||
<text
|
||||
x={x}
|
||||
y={y}
|
||||
fill="#7e8cb6"
|
||||
textAnchor={x > cx ? "start" : "end"}
|
||||
dominantBaseline="central"
|
||||
fontSize="10"
|
||||
fontWeight="500"
|
||||
>
|
||||
{(percent * 100).toFixed(1)}%
|
||||
</text>
|
||||
);
|
||||
}
|
||||
|
||||
export function CompactBarChart({
|
||||
data,
|
||||
}: {
|
||||
data: SimpleBarDatum[];
|
||||
}) {
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={data} margin={{ top: 14, right: 10, left: -12, bottom: 8 }}>
|
||||
<CartesianGrid vertical={false} stroke={GRID} strokeDasharray="3 3" />
|
||||
<XAxis
|
||||
dataKey="label"
|
||||
tick={{ fill: "#4b5563", fontSize: 9 }}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
/>
|
||||
<YAxis
|
||||
tick={{ fill: "#4b5563", fontSize: 9 }}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
width={28}
|
||||
/>
|
||||
<Bar dataKey="value" fill={PRIMARY} radius={[3, 3, 0, 0]} isAnimationActive={false}>
|
||||
<LabelList
|
||||
dataKey="value"
|
||||
position="top"
|
||||
fill={PRIMARY}
|
||||
fontSize={9}
|
||||
offset={4}
|
||||
/>
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export function WorkflowBarChart({
|
||||
data,
|
||||
}: {
|
||||
data: SimpleBarDatum[];
|
||||
}) {
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={data} margin={{ top: 18, right: 16, left: 0, bottom: 12 }}>
|
||||
<CartesianGrid vertical={false} stroke={GRID} strokeDasharray="3 3" />
|
||||
<XAxis
|
||||
dataKey="label"
|
||||
tick={{ fill: "#4b5563", fontSize: 12 }}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
/>
|
||||
<YAxis
|
||||
tick={{ fill: "#4b5563", fontSize: 12 }}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
width={40}
|
||||
/>
|
||||
<Bar dataKey="value" fill={PRIMARY} radius={[4, 4, 0, 0]} isAnimationActive={false}>
|
||||
<LabelList
|
||||
dataKey="value"
|
||||
position="top"
|
||||
fill={PRIMARY}
|
||||
fontSize={11}
|
||||
offset={4}
|
||||
/>
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export function TrendLineChart({
|
||||
data,
|
||||
}: {
|
||||
data: DualSeriesDatum[];
|
||||
}) {
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={data} margin={{ top: 12, right: 8, left: -16, bottom: 10 }}>
|
||||
<CartesianGrid vertical={false} stroke={GRID} strokeDasharray="3 3" />
|
||||
<XAxis
|
||||
dataKey="label"
|
||||
tick={{ fill: "#4b5563", fontSize: 8 }}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
/>
|
||||
<YAxis
|
||||
tick={{ fill: "#4b5563", fontSize: 8 }}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
width={24}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="valueA"
|
||||
stroke={PRIMARY}
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="valueB"
|
||||
stroke={SECONDARY}
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export function DualLineChart({
|
||||
data,
|
||||
}: {
|
||||
data: DualSeriesDatum[];
|
||||
}) {
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={data} margin={{ top: 24, right: 18, left: 0, bottom: 24 }}>
|
||||
<CartesianGrid vertical={false} stroke={GRID} />
|
||||
<XAxis
|
||||
dataKey="label"
|
||||
tick={{ fill: "#4b5563", fontSize: 12 }}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
/>
|
||||
<YAxis
|
||||
tick={{ fill: "#4b5563", fontSize: 12 }}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
width={40}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="valueA"
|
||||
stroke={PRIMARY}
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="valueB"
|
||||
stroke={SECONDARY}
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export function AreaTrendChart({
|
||||
data,
|
||||
idPrefix,
|
||||
}: {
|
||||
data: SimpleBarDatum[];
|
||||
idPrefix: string;
|
||||
}) {
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={data} margin={{ top: 18, right: 12, left: -12, bottom: 6 }}>
|
||||
<defs>
|
||||
<linearGradient id={`${idPrefix}-fill`} x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor={PRIMARY} stopOpacity={0.45} />
|
||||
<stop offset="100%" stopColor={PRIMARY} stopOpacity={0.05} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid vertical={false} stroke={GRID} strokeDasharray="3 3" />
|
||||
<XAxis
|
||||
dataKey="label"
|
||||
tick={{ fill: "#4b5563", fontSize: 8 }}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
/>
|
||||
<YAxis
|
||||
tick={{ fill: "#4b5563", fontSize: 8 }}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
width={24}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="value"
|
||||
stroke={PRIMARY}
|
||||
strokeWidth={2}
|
||||
fill={`url(#${idPrefix}-fill)`}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export function SemiDonutChart({
|
||||
data,
|
||||
}: {
|
||||
data: PieDatum[];
|
||||
}) {
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={data}
|
||||
dataKey="value"
|
||||
startAngle={180}
|
||||
endAngle={0}
|
||||
cx="50%"
|
||||
cy="92%"
|
||||
innerRadius={58}
|
||||
outerRadius={88}
|
||||
paddingAngle={6}
|
||||
stroke="none"
|
||||
labelLine={false}
|
||||
label={renderOutsidePieLabel}
|
||||
isAnimationActive={false}
|
||||
>
|
||||
{data.map((entry, index) => (
|
||||
<Cell
|
||||
key={`${entry.name}-${index}`}
|
||||
fill={[PRIMARY, SECONDARY, LIGHT][index % 3]}
|
||||
/>
|
||||
))}
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export function CompactPieChart({
|
||||
data,
|
||||
}: {
|
||||
data: PieDatum[];
|
||||
}) {
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={data}
|
||||
dataKey="value"
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius={40}
|
||||
outerRadius={76}
|
||||
paddingAngle={1}
|
||||
stroke="none"
|
||||
labelLine={false}
|
||||
label={renderOutsidePieLabel}
|
||||
isAnimationActive={false}
|
||||
>
|
||||
{data.map((entry, index) => (
|
||||
<Cell
|
||||
key={`${entry.name}-${index}`}
|
||||
fill={[PRIMARY, SECONDARY, "#d7dff4"][index % 3]}
|
||||
/>
|
||||
))}
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,834 @@
|
|||
"use client";
|
||||
|
||||
import { useId } from "react";
|
||||
import * as z from "zod";
|
||||
import {
|
||||
Area,
|
||||
AreaChart,
|
||||
Bar,
|
||||
BarChart,
|
||||
CartesianGrid,
|
||||
Cell,
|
||||
Line,
|
||||
LineChart,
|
||||
Pie,
|
||||
PieChart,
|
||||
ReferenceLine,
|
||||
Scatter,
|
||||
ScatterChart,
|
||||
XAxis,
|
||||
YAxis,
|
||||
LabelList,
|
||||
ResponsiveContainer,
|
||||
} from "recharts";
|
||||
|
||||
export const simpleDataSchema = z.object({
|
||||
name: z.string().meta({ description: "Data point name" }),
|
||||
value: z.number().meta({ description: "Data point value" }),
|
||||
});
|
||||
|
||||
export const multiSeriesDataSchema = z.object({
|
||||
name: z.string().meta({ description: "Category name" }),
|
||||
values: z.any().meta({
|
||||
description:
|
||||
"Key-value pairs for each series (object with series names as keys and numbers as values)",
|
||||
}),
|
||||
});
|
||||
|
||||
export const divergingDataSchema = z.object({
|
||||
name: z.string().meta({ description: "Category name" }),
|
||||
positive: z.number().meta({ description: "Positive value" }),
|
||||
negative: z.number().meta({ description: "Negative value" }),
|
||||
});
|
||||
|
||||
export const scatterDataSchema = z.object({
|
||||
x: z.number().meta({ description: "X coordinate" }),
|
||||
y: z.number().meta({ description: "Y coordinate" }),
|
||||
});
|
||||
|
||||
/** Two series over categorical labels (line stats slide). */
|
||||
export const dualLinePointSchema = z.object({
|
||||
label: z.string().meta({ description: "Chart axis label" }),
|
||||
valueA: z.number().meta({ description: "First series value" }),
|
||||
valueB: z.number().meta({ description: "Second series value" }),
|
||||
});
|
||||
export const SimpleDataPointSchema = z.object({
|
||||
name: z.string(),
|
||||
value: z.number(),
|
||||
});
|
||||
|
||||
export const MultiSeriesDataPointSchema = z.object({
|
||||
name: z.string(),
|
||||
values: z.any(),
|
||||
});
|
||||
|
||||
export const DivergingDataPointSchema = z.object({
|
||||
name: z.string(),
|
||||
positive: z.number(),
|
||||
negative: z.number(),
|
||||
});
|
||||
|
||||
export const ScatterDataPointSchema = z.object({
|
||||
x: z.number(),
|
||||
y: z.number(),
|
||||
name: z.string().optional(),
|
||||
});
|
||||
|
||||
|
||||
export const flexibleChartTypeSchema = z.enum([
|
||||
"bar",
|
||||
"bar-horizontal",
|
||||
"bar-grouped-vertical",
|
||||
"bar-grouped-horizontal",
|
||||
"bar-stacked-vertical",
|
||||
"bar-stacked-horizontal",
|
||||
"bar-clustered",
|
||||
"bar-diverging",
|
||||
"line",
|
||||
"line-dual",
|
||||
"area",
|
||||
"area-stacked",
|
||||
"pie",
|
||||
"donut",
|
||||
"scatter",
|
||||
]);
|
||||
|
||||
export const flexibleChartDataSchema = z.object({
|
||||
type: flexibleChartTypeSchema.default("bar"),
|
||||
data: z.union([
|
||||
z.array(simpleDataSchema),
|
||||
z.array(multiSeriesDataSchema),
|
||||
z.array(divergingDataSchema),
|
||||
z.array(scatterDataSchema),
|
||||
z.array(dualLinePointSchema),
|
||||
]),
|
||||
series: z.array(z.string()).optional().meta({ description: "Series names for grouped/stacked charts" }),
|
||||
divergingLabels: z.tuple([z.string(), z.string()]).optional(),
|
||||
});
|
||||
|
||||
export type FlexibleChartData = z.infer<typeof flexibleChartDataSchema>;
|
||||
|
||||
const formatComma = (value: number) => value.toLocaleString("en-US");
|
||||
|
||||
export function deriveSeriesNames(data: any[], explicit: string[]): string[] {
|
||||
if (explicit.length > 0) return explicit;
|
||||
const first = data[0];
|
||||
if (!first) return [];
|
||||
if (first.values != null && typeof first.values === "object" && !Array.isArray(first.values)) {
|
||||
return Object.keys(first.values);
|
||||
}
|
||||
if (typeof first.value === "number") return ["value"];
|
||||
return [];
|
||||
}
|
||||
|
||||
export function transformMultiSeriesData(data: any[], series: string[]) {
|
||||
return data.map((item) => {
|
||||
const result: Record<string, any> = { name: item.name };
|
||||
series.forEach((s) => {
|
||||
if (item.values != null && typeof item.values === "object" && s in item.values) {
|
||||
result[s] = Number(item.values[s]) || 0;
|
||||
} else if (s === "value" && typeof item.value === "number") {
|
||||
result[s] = item.value;
|
||||
} else if (typeof item[s] === "number") {
|
||||
result[s] = item[s];
|
||||
} else {
|
||||
result[s] = Number(item.values?.[s]) || 0;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
export function transformDivergingData(data: any[]) {
|
||||
return data.map((item) => {
|
||||
if (typeof item.positive === "number" && typeof item.negative === "number") {
|
||||
return {
|
||||
name: item.name,
|
||||
positive: item.positive,
|
||||
negative: -Math.abs(item.negative),
|
||||
};
|
||||
}
|
||||
const v = Number(item.value);
|
||||
if (!Number.isNaN(v)) {
|
||||
return {
|
||||
name: item.name,
|
||||
positive: Math.max(0, v),
|
||||
negative: v < 0 ? v : 0,
|
||||
};
|
||||
}
|
||||
return { name: item.name, positive: 0, negative: 0 };
|
||||
});
|
||||
}
|
||||
|
||||
export function normalizeScatterPoints(data: any[]) {
|
||||
return data.map((item, i) => {
|
||||
if (typeof item.x === "number" && typeof item.y === "number") {
|
||||
return { ...item, x: item.x, y: item.y };
|
||||
}
|
||||
if (typeof item.value === "number") {
|
||||
return { ...item, x: typeof item.x === "number" ? item.x : i + 1, y: item.value };
|
||||
}
|
||||
return { ...item, x: i + 1, y: 0 };
|
||||
});
|
||||
}
|
||||
|
||||
/** Line-stats style rows: categorical `label` + two metrics (not a single `value` series). */
|
||||
function dataIsDualLineShape(data: any[]): boolean {
|
||||
const row = data[0];
|
||||
return (
|
||||
!!row &&
|
||||
typeof row === "object" &&
|
||||
typeof row.label === "string" &&
|
||||
typeof row.valueA === "number" &&
|
||||
typeof row.valueB === "number" &&
|
||||
typeof row.value !== "number"
|
||||
);
|
||||
}
|
||||
|
||||
const MULTI_SERIES_CHART_TYPES: FlexibleChartData["type"][] = [
|
||||
"bar-grouped-vertical",
|
||||
"bar-grouped-horizontal",
|
||||
"bar-stacked-vertical",
|
||||
"bar-stacked-horizontal",
|
||||
"bar-clustered",
|
||||
"area-stacked",
|
||||
];
|
||||
|
||||
/**
|
||||
* Aligns `data`/`series` with `chartType`. Line-stats slides often keep `{ label, valueA, valueB }`
|
||||
* while bar/line/pie/etc. expect `name`/`value` or `values` + series keys.
|
||||
*/
|
||||
export function normalizeFlexibleChartData(
|
||||
chartType: FlexibleChartData["type"],
|
||||
data: any[],
|
||||
seriesIn: string[],
|
||||
): { data: any[]; series: string[] } {
|
||||
const series = seriesIn ?? [];
|
||||
const rows = data ?? [];
|
||||
|
||||
if (chartType === "line-dual") {
|
||||
if (dataIsDualLineShape(rows)) return { data: rows, series };
|
||||
return {
|
||||
data: rows.map((r, i) => ({
|
||||
label: r.label ?? r.name ?? `P${i + 1}`,
|
||||
valueA: typeof r.valueA === "number" ? r.valueA : typeof r.value === "number" ? r.value : 0,
|
||||
valueB: typeof r.valueB === "number" ? r.valueB : typeof r.value === "number" ? r.value : 0,
|
||||
})),
|
||||
series,
|
||||
};
|
||||
}
|
||||
|
||||
if (!dataIsDualLineShape(rows)) {
|
||||
return { data: rows, series };
|
||||
}
|
||||
|
||||
const dual = rows as Array<{ label: string; valueA: number; valueB: number }>;
|
||||
|
||||
if (MULTI_SERIES_CHART_TYPES.includes(chartType)) {
|
||||
const keys = series.length >= 2 ? [series[0], series[1]] : ["A", "B"];
|
||||
const mapped = dual.map((r) => ({
|
||||
name: r.label,
|
||||
values: { [keys[0]]: r.valueA, [keys[1]]: r.valueB },
|
||||
}));
|
||||
return { data: mapped, series: keys };
|
||||
}
|
||||
|
||||
if (chartType === "bar-diverging") {
|
||||
const mapped = dual.map((r) => ({
|
||||
name: r.label,
|
||||
positive: Math.max(0, r.valueA),
|
||||
negative: Math.max(0, r.valueB),
|
||||
}));
|
||||
return { data: mapped, series };
|
||||
}
|
||||
|
||||
const mapped = dual.map((r) => ({
|
||||
name: r.label,
|
||||
value: r.valueA + r.valueB,
|
||||
}));
|
||||
return { data: mapped, series };
|
||||
}
|
||||
|
||||
const graphVar = (index: number, fallback: string) => `var(--graph-${index % 8}, ${fallback})`;
|
||||
|
||||
export type ChartDensity = "default" | "compact";
|
||||
|
||||
export type FlexibleReportChartProps = {
|
||||
chartType: FlexibleChartData["type"];
|
||||
data: any[];
|
||||
series?: string[];
|
||||
colorFallback?: string;
|
||||
/** For `line-dual` only */
|
||||
dualLineColors?: [string, string];
|
||||
/** Smaller type, margins, and labels for multi-chart dashboards */
|
||||
density?: ChartDensity;
|
||||
};
|
||||
|
||||
export function FlexibleReportChart({
|
||||
chartType,
|
||||
data: chartData,
|
||||
series = [],
|
||||
colorFallback = "#157CFF",
|
||||
dualLineColors = ["#9fb6ff", "#4d4ef3"],
|
||||
density = "default",
|
||||
}: FlexibleReportChartProps) {
|
||||
const areaGradientId = `flex-area-${useId().replace(/:/g, "")}`;
|
||||
const compact = density === "compact";
|
||||
|
||||
const { data: normalizedData, series: normalizedSeries } = normalizeFlexibleChartData(
|
||||
chartType,
|
||||
chartData,
|
||||
series,
|
||||
);
|
||||
const effectiveSeries = deriveSeriesNames(normalizedData as any[], normalizedSeries);
|
||||
const scatterPoints = normalizeScatterPoints(normalizedData as any[]);
|
||||
|
||||
const ui = {
|
||||
tickFs: compact ? 6 : 10,
|
||||
catAxisW: compact ? 36 : 60,
|
||||
barSize: compact ? 12 : 35,
|
||||
labelFs: compact ? 8 : 14,
|
||||
labelOffTop: compact ? 2 : 10,
|
||||
labelOffSide: compact ? 2 : 8,
|
||||
margin: compact
|
||||
? { top: 10, right: 15, left: -2, bottom: 0 }
|
||||
: { top: 15, right: 20, left: 0, bottom: 5 },
|
||||
lineStroke: compact ? 2 : 3,
|
||||
dotR: compact ? 2.5 : 4,
|
||||
dotStroke: compact ? 1 : 2,
|
||||
barRadiusLg: compact ? ([4, 4, 0, 0] as const) : ([8, 8, 0, 0] as const),
|
||||
barRadiusMd: compact ? ([2, 2, 0, 0] as const) : ([4, 4, 0, 0] as const),
|
||||
barRadiusH: compact ? ([0, 4, 4, 0] as const) : ([0, 6, 6, 0] as const),
|
||||
pieOuter: compact ? "100%" : "100%",
|
||||
donutOuter: compact ? "90%" : "100%",
|
||||
donutInner: compact ? "60%" : "70%",
|
||||
pieMargin: compact
|
||||
? { top: 0, right: 0, left: 0, bottom: 0 }
|
||||
: { top: 8, right: 8, left: 8, bottom: 8 },
|
||||
pieLabelMinPct: compact ? 0.12 : 0.06,
|
||||
pieMaxNameLen: compact ? 6 : 10,
|
||||
};
|
||||
|
||||
const axisProps = {
|
||||
tick: { fill: "var(--background-text, #232223)", fontSize: ui.tickFs, fontWeight: 500 },
|
||||
axisLine: { stroke: "var(--background-text, #232223)" },
|
||||
tickLine: { stroke: "var(--background-text, #232223)" },
|
||||
};
|
||||
|
||||
const gridProps = {
|
||||
strokeDasharray: "3 3",
|
||||
stroke: "var(--background-text, #232223)",
|
||||
opacity: 0.7,
|
||||
};
|
||||
|
||||
const renderPieInsideLabel = (props: any) => {
|
||||
const {
|
||||
cx = 0,
|
||||
cy = 0,
|
||||
midAngle = 0,
|
||||
innerRadius: ir = 0,
|
||||
outerRadius: or = 0,
|
||||
percent = 0,
|
||||
name,
|
||||
} = props;
|
||||
if (percent < ui.pieLabelMinPct) return null;
|
||||
const toNum = (v: unknown) => {
|
||||
if (typeof v === "number" && Number.isFinite(v)) return v;
|
||||
if (typeof v === "string" && v.trim().endsWith("%")) return NaN;
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) ? n : NaN;
|
||||
};
|
||||
let inner = toNum(ir);
|
||||
let outer = toNum(or);
|
||||
if (!Number.isFinite(outer)) {
|
||||
outer = compact ? 56 : 140;
|
||||
inner = Number.isFinite(inner) ? inner : 0;
|
||||
}
|
||||
if (!Number.isFinite(inner)) inner = 0;
|
||||
const midR = inner + (outer - inner) * 0.5;
|
||||
const rad = (-midAngle * Math.PI) / 180;
|
||||
const x = cx + midR * Math.cos(rad);
|
||||
const y = cy + midR * Math.sin(rad);
|
||||
const nm = String(name ?? "");
|
||||
const short = nm.length <= ui.pieMaxNameLen;
|
||||
const pct = `${(percent * 100).toFixed(0)}%`;
|
||||
const fontSize = compact ? (short ? 6 : 5) : short ? 10 : 9;
|
||||
const labelText = compact ? pct : short ? `${name} ${pct}` : pct;
|
||||
return (
|
||||
<text
|
||||
x={x}
|
||||
y={y}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="central"
|
||||
fill="#ffffff"
|
||||
fontSize={fontSize}
|
||||
fontWeight={600}
|
||||
style={{
|
||||
paintOrder: "stroke fill",
|
||||
stroke: "rgba(0,0,0,0.28)",
|
||||
strokeWidth: compact ? 1 : 2,
|
||||
}}
|
||||
>
|
||||
{labelText}
|
||||
</text>
|
||||
);
|
||||
};
|
||||
|
||||
const commonProps = {
|
||||
margin: ui.margin,
|
||||
|
||||
};
|
||||
|
||||
switch (chartType) {
|
||||
case "bar":
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
|
||||
|
||||
<BarChart data={normalizedData as any[]} {...commonProps}>
|
||||
<CartesianGrid vertical={false} {...gridProps} />
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
{...axisProps}
|
||||
tickFormatter={formatComma}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
/>
|
||||
<YAxis {...axisProps} tickFormatter={formatComma} tickLine={false} axisLine={false} />
|
||||
<Bar dataKey="value" fill={graphVar(0, colorFallback)} barSize={ui.barSize} radius={[...ui.barRadiusLg]}>
|
||||
<LabelList
|
||||
dataKey="value"
|
||||
position="top"
|
||||
fill={colorFallback}
|
||||
fontSize={ui.labelFs}
|
||||
offset={ui.labelOffTop}
|
||||
/>
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
|
||||
case "bar-horizontal":
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
|
||||
<BarChart data={normalizedData as any[]} layout="vertical" {...commonProps}>
|
||||
<CartesianGrid vertical={false} {...gridProps} />
|
||||
<XAxis type="number" {...axisProps} tickFormatter={formatComma} tickLine={false} axisLine={false} />
|
||||
<YAxis
|
||||
type="category"
|
||||
dataKey="name"
|
||||
{...axisProps}
|
||||
width={ui.catAxisW}
|
||||
tickFormatter={formatComma}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
/>
|
||||
<Bar dataKey="value" barSize={ui.barSize} fill={graphVar(0, colorFallback)} radius={[...ui.barRadiusH]}>
|
||||
<LabelList dataKey="value" position="right" fill={colorFallback} fontSize={ui.labelFs} offset={ui.labelOffSide} />
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
|
||||
case "bar-grouped-vertical": {
|
||||
const transformedData = transformMultiSeriesData(normalizedData as any[], effectiveSeries);
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
|
||||
<BarChart data={transformedData} {...commonProps}>
|
||||
<CartesianGrid vertical={false} {...gridProps} />
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
{...axisProps}
|
||||
tickFormatter={formatComma}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
/>
|
||||
<YAxis {...axisProps} tickFormatter={formatComma} tickLine={false} axisLine={false} />
|
||||
{effectiveSeries.map((s: string, index: number) => (
|
||||
<Bar key={s} dataKey={s} barSize={ui.barSize} fill={graphVar(index, colorFallback)} radius={[...ui.barRadiusMd]}>
|
||||
<LabelList
|
||||
dataKey={s}
|
||||
position="top"
|
||||
fill={graphVar(index, colorFallback)}
|
||||
fontSize={ui.labelFs}
|
||||
offset={ui.labelOffTop}
|
||||
/>
|
||||
</Bar>
|
||||
))}
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
}
|
||||
|
||||
case "bar-grouped-horizontal": {
|
||||
const transformedData = transformMultiSeriesData(normalizedData as any[], effectiveSeries);
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
|
||||
<BarChart data={transformedData} layout="vertical" {...commonProps}>
|
||||
<CartesianGrid vertical={false} {...gridProps} />
|
||||
<XAxis type="number" {...axisProps} tickFormatter={formatComma} tickLine={false} axisLine={false} />
|
||||
<YAxis
|
||||
type="category"
|
||||
dataKey="name"
|
||||
{...axisProps}
|
||||
width={ui.catAxisW}
|
||||
tickFormatter={formatComma}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
/>
|
||||
{effectiveSeries.map((s: string, index: number) => (
|
||||
<Bar key={s} dataKey={s} barSize={ui.barSize} fill={graphVar(index, colorFallback)} radius={[...ui.barRadiusH]}>
|
||||
<LabelList
|
||||
dataKey={s}
|
||||
position="right"
|
||||
fill={graphVar(index, colorFallback)}
|
||||
fontSize={ui.labelFs}
|
||||
offset={ui.labelOffSide}
|
||||
/>
|
||||
</Bar>
|
||||
))}
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
}
|
||||
|
||||
case "bar-stacked-vertical": {
|
||||
const transformedData = transformMultiSeriesData(normalizedData as any[], effectiveSeries);
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
|
||||
<BarChart data={transformedData} {...commonProps}>
|
||||
<CartesianGrid vertical={false} {...gridProps} />
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
{...axisProps}
|
||||
tickFormatter={formatComma}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
/>
|
||||
<YAxis {...axisProps} tickFormatter={formatComma} tickLine={false} axisLine={false} />
|
||||
{effectiveSeries.map((s: string, index: number) => (
|
||||
<Bar
|
||||
key={s}
|
||||
dataKey={s}
|
||||
stackId="stack"
|
||||
barSize={ui.barSize}
|
||||
fill={graphVar(index, colorFallback)}
|
||||
radius={index === effectiveSeries.length - 1 ? [...ui.barRadiusMd] : [0, 0, 0, 0]}
|
||||
>
|
||||
<LabelList
|
||||
dataKey={s}
|
||||
position="top"
|
||||
fill={graphVar(index, colorFallback)}
|
||||
fontSize={ui.labelFs}
|
||||
offset={ui.labelOffTop}
|
||||
/>
|
||||
</Bar>
|
||||
))}
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
}
|
||||
|
||||
case "bar-stacked-horizontal": {
|
||||
const transformedData = transformMultiSeriesData(normalizedData as any[], effectiveSeries);
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
|
||||
<BarChart data={transformedData} layout="vertical" {...commonProps}>
|
||||
<CartesianGrid vertical={false} {...gridProps} />
|
||||
<XAxis type="number" {...axisProps} tickFormatter={formatComma} tickLine={false} axisLine={false} />
|
||||
<YAxis
|
||||
type="category"
|
||||
dataKey="name"
|
||||
{...axisProps}
|
||||
width={ui.catAxisW}
|
||||
tickFormatter={formatComma}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
/>
|
||||
{effectiveSeries.map((s: string, index: number) => (
|
||||
<Bar
|
||||
key={s}
|
||||
dataKey={s}
|
||||
stackId="stack"
|
||||
barSize={ui.barSize}
|
||||
fill={graphVar(index, colorFallback)}
|
||||
radius={index === effectiveSeries.length - 1 ? [...ui.barRadiusH] : [0, 0, 0, 0]}
|
||||
>
|
||||
<LabelList
|
||||
dataKey={s}
|
||||
position="right"
|
||||
fill={graphVar(index, colorFallback)}
|
||||
fontSize={ui.labelFs}
|
||||
offset={ui.labelOffSide}
|
||||
/>
|
||||
</Bar>
|
||||
))}
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
}
|
||||
|
||||
case "bar-clustered": {
|
||||
const transformedData = transformMultiSeriesData(normalizedData as any[], effectiveSeries);
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
|
||||
<BarChart data={transformedData} barGap={1} barCategoryGap="15%" {...commonProps}>
|
||||
<CartesianGrid vertical={false} {...gridProps} />
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
{...axisProps}
|
||||
tickFormatter={formatComma}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
/>
|
||||
<YAxis {...axisProps} tickFormatter={formatComma} tickLine={false} axisLine={false} />
|
||||
{effectiveSeries.map((s: string, index: number) => (
|
||||
<Bar
|
||||
key={s}
|
||||
dataKey={s}
|
||||
barSize={Math.max(
|
||||
compact ? 6 : 15,
|
||||
(compact ? 22 : 50) / Math.max(1, effectiveSeries.length),
|
||||
)}
|
||||
fill={graphVar(index, colorFallback)}
|
||||
radius={compact ? [2, 2, 0, 0] : [3, 3, 0, 0]}
|
||||
>
|
||||
<LabelList
|
||||
dataKey={s}
|
||||
position="top"
|
||||
fill={graphVar(index, colorFallback)}
|
||||
fontSize={ui.labelFs}
|
||||
offset={ui.labelOffTop}
|
||||
/>
|
||||
</Bar>
|
||||
))}
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
}
|
||||
|
||||
case "bar-diverging": {
|
||||
const transformedData = transformDivergingData(normalizedData as any[]);
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={transformedData} layout="vertical" stackOffset="sign" {...commonProps}>
|
||||
<CartesianGrid vertical={false} {...gridProps} />
|
||||
<XAxis type="number" {...axisProps} tickFormatter={formatComma} tickLine={false} axisLine={false} />
|
||||
<YAxis
|
||||
type="category"
|
||||
dataKey="name"
|
||||
{...axisProps}
|
||||
width={ui.catAxisW}
|
||||
tickFormatter={formatComma}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
/>
|
||||
<ReferenceLine x={0} stroke="#9CA3AF" strokeWidth={1} />
|
||||
<Bar dataKey="positive" barSize={ui.barSize} fill={graphVar(0, colorFallback)} stackId="stack" radius={[...ui.barRadiusH]}>
|
||||
<LabelList dataKey="positive" position="right" fill={graphVar(0, colorFallback)} fontSize={ui.labelFs} offset={ui.labelOffSide} />
|
||||
</Bar>
|
||||
<Bar dataKey="negative" fill={graphVar(3, colorFallback)} stackId="stack" radius={compact ? [2, 0, 0, 2] : [4, 0, 0, 4]}>
|
||||
<LabelList dataKey="negative" position="left" fill={graphVar(3, colorFallback)} fontSize={ui.labelFs} offset={ui.labelOffSide} />
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
}
|
||||
|
||||
case "line":
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
|
||||
<LineChart data={normalizedData as any[]} {...commonProps}>
|
||||
<CartesianGrid vertical={false} {...gridProps} />
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
{...axisProps}
|
||||
tickFormatter={formatComma}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
/>
|
||||
<YAxis {...axisProps} tickFormatter={formatComma} tickLine={false} axisLine={false} />
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="value"
|
||||
stroke={graphVar(0, colorFallback)}
|
||||
strokeWidth={ui.lineStroke}
|
||||
dot={{ fill: graphVar(0, colorFallback), strokeWidth: ui.dotStroke, r: ui.dotR }}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
|
||||
case "line-dual":
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={normalizedData as any[]} {...commonProps}>
|
||||
<CartesianGrid vertical={false} {...gridProps} />
|
||||
<XAxis
|
||||
dataKey="label"
|
||||
{...axisProps}
|
||||
tickFormatter={formatComma}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
/>
|
||||
<YAxis {...axisProps} tickFormatter={formatComma} tickLine={false} axisLine={false} />
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="valueA"
|
||||
stroke={dualLineColors[0]}
|
||||
strokeWidth={ui.lineStroke}
|
||||
dot={{ fill: dualLineColors[0], strokeWidth: ui.dotStroke, r: ui.dotR }}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="valueB"
|
||||
stroke={dualLineColors[1]}
|
||||
strokeWidth={ui.lineStroke}
|
||||
dot={{ fill: dualLineColors[1], strokeWidth: ui.dotStroke, r: ui.dotR }}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
|
||||
case "area":
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
|
||||
|
||||
<AreaChart data={normalizedData as any[]} {...commonProps}>
|
||||
<CartesianGrid vertical={false} {...gridProps} />
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
{...axisProps}
|
||||
tickFormatter={formatComma}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
/>
|
||||
<YAxis {...axisProps} tickFormatter={formatComma} tickLine={false} axisLine={false} />
|
||||
<defs>
|
||||
<linearGradient id={areaGradientId} x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor={graphVar(0, colorFallback)} stopOpacity={0.4} />
|
||||
<stop offset="95%" stopColor={graphVar(0, colorFallback)} stopOpacity={0.05} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="value"
|
||||
stroke={graphVar(0, colorFallback)}
|
||||
strokeWidth={compact ? 1.5 : 2}
|
||||
fill={`url(#${areaGradientId})`}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
|
||||
case "area-stacked": {
|
||||
const transformedData = transformMultiSeriesData(normalizedData as any[], effectiveSeries);
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
|
||||
<AreaChart data={transformedData} {...commonProps}>
|
||||
<CartesianGrid vertical={false} {...gridProps} />
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
{...axisProps}
|
||||
tickFormatter={formatComma}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
/>
|
||||
<YAxis {...axisProps} tickFormatter={formatComma} tickLine={false} axisLine={false} />
|
||||
{effectiveSeries.map((s: string, index: number) => (
|
||||
<Area
|
||||
key={s}
|
||||
type="monotone"
|
||||
dataKey={s}
|
||||
stackId="1"
|
||||
stroke={graphVar(index, colorFallback)}
|
||||
fill={graphVar(index, colorFallback)}
|
||||
fillOpacity={0.4}
|
||||
/>
|
||||
))}
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
}
|
||||
|
||||
case "pie":
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
|
||||
<PieChart {...commonProps} margin={ui.pieMargin}>
|
||||
<Pie
|
||||
data={normalizedData as any[]}
|
||||
dataKey="value"
|
||||
nameKey="name"
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
outerRadius={ui.pieOuter}
|
||||
innerRadius={0}
|
||||
label={renderPieInsideLabel}
|
||||
labelLine={false}
|
||||
>
|
||||
{(normalizedData as any[]).map((_, index) => (
|
||||
<Cell key={`pie-cell-${index}`} fill={graphVar(index, colorFallback)} />
|
||||
))}
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
|
||||
case "donut":
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
|
||||
<PieChart {...commonProps} margin={ui.pieMargin}>
|
||||
<Pie
|
||||
data={normalizedData as any[]}
|
||||
dataKey="value"
|
||||
nameKey="name"
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
outerRadius={ui.donutOuter}
|
||||
innerRadius={ui.donutInner}
|
||||
label={renderPieInsideLabel}
|
||||
paddingAngle={compact ? 1 : 2}
|
||||
labelLine={false}
|
||||
>
|
||||
{(normalizedData as any[]).map((_, index) => (
|
||||
<Cell key={`donut-cell-${index}`} fill={graphVar(index, colorFallback)} />
|
||||
))}
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
|
||||
case "scatter":
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
|
||||
<ScatterChart {...commonProps}>
|
||||
<CartesianGrid vertical={false} {...gridProps} />
|
||||
<XAxis dataKey="x" type="number" {...axisProps} tickFormatter={formatComma} tickLine={false} axisLine={false} />
|
||||
<YAxis dataKey="y" type="number" {...axisProps} tickFormatter={formatComma} tickLine={false} axisLine={false} />
|
||||
<Scatter data={scatterPoints} name="Series">
|
||||
{scatterPoints.map((_, index) => (
|
||||
<Cell key={`scatter-cell-${index}`} fill={graphVar(index, colorFallback)} />
|
||||
))}
|
||||
</Scatter>
|
||||
</ScatterChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
|
||||
default:
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center text-gray-500">Unsupported chart type</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -22,7 +22,7 @@ import EducationTableOfContentsSlide, { Schema as EduTocSchema, slideLayoutId as
|
|||
import EducationAboutSlide, { Schema as EduAboutSchema, slideLayoutId as EduAboutId, slideLayoutName as EduAboutName, slideLayoutDescription as EduAboutDesc } from "./Education/EducationAboutSlide";
|
||||
import EducationContentSplitSlide, { Schema as EduContentSplitSchema, slideLayoutId as EduContentSplitId, slideLayoutName as EduContentSplitName, slideLayoutDescription as EduContentSplitDesc } from "./Education/EducationContentSplitSlide";
|
||||
import EducationImageGallerySlide, { Schema as EduImageGallerySchema, slideLayoutId as EduImageGalleryId, slideLayoutName as EduImageGalleryName, slideLayoutDescription as EduImageGalleryDesc } from "./Education/EducationImageGallerySlide";
|
||||
import EducationReportDonutSlide, { Schema as EduReportDonutSchema, slideLayoutId as EduReportDonutId, slideLayoutName as EduReportDonutName, slideLayoutDescription as EduReportDonutDesc } from "./Education/EducationReportDonutSlide";
|
||||
import EducationReportDonutSlide, { Schema as EduReportDonutSchema, slideLayoutId as EduReportDonutId, slideLayoutName as EduReportDonutName, slideLayoutDescription as EduReportDonutDesc } from "./Education/EducationReportChartSlide";
|
||||
import EducationServicesSplitSlide, { Schema as EduServicesSplitSchema, slideLayoutId as EduServicesSplitId, slideLayoutName as EduServicesSplitName, slideLayoutDescription as EduServicesSplitDesc } from "./Education/EducationServicesSplitSlide";
|
||||
import EducationStatisticsGridSlide, { Schema as EduStatisticsGridSchema, slideLayoutId as EduStatisticsGridId, slideLayoutName as EduStatisticsGridName, slideLayoutDescription as EduStatisticsGridDesc } from "./Education/EducationStatisticsGridSlide";
|
||||
import EducationTimelineSlide, { Schema as EduTimelineSchema, slideLayoutId as EduTimelineId, slideLayoutName as EduTimelineName, slideLayoutDescription as EduTimelineDesc } from "./Education/EducationTimelineSlide";
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue