mirror of
https://github.com/vercel-labs/natural-language-postgres.git
synced 2026-11-04 08:10:38 +00:00
working charts
This commit is contained in:
1 parent
05615d5e02
commit
299cf8f9da
8 files changed
+347
-185
No files matched your search
+62
-9
@@ -7,6 +7,7 @@ import {
|
||||
configSchema,
|
||||
dataSchema,
|
||||
explanationsSchema,
|
||||
Result,
|
||||
Unicorn,
|
||||
} from "@/lib/types";
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
@@ -20,8 +21,9 @@ export const generateQuery = async (input: string) => {
|
||||
try {
|
||||
const result = await generateObject({
|
||||
model: openai("gpt-4o"),
|
||||
system: `You are a SQL (postgres) expert. Your job is to help the user write a SQL query to retrieve the data they need. The table schema is as follows:
|
||||
unicorns (
|
||||
system: `You are a SQL (postgres) and data visualization expert. Your job is to help the user write a SQL query to retrieve the data they need. The table schema is as follows:
|
||||
|
||||
unicorns (
|
||||
id SERIAL PRIMARY KEY,
|
||||
company VARCHAR(255) NOT NULL UNIQUE,
|
||||
valuation DECIMAL(10, 2) NOT NULL,
|
||||
@@ -39,7 +41,7 @@ export const generateQuery = async (input: string) => {
|
||||
Note: select_investors is a comma-separated list of investors. Trim whitespace to ensure you're grouping properly. Note, some fields may be null or have onnly one value.
|
||||
When answering questions about a specific field, ensure you are selecting the identifying column (ie. what is Vercel's valuation would select company and valuation').
|
||||
|
||||
The industries tagged are:
|
||||
The industries available are:
|
||||
- healthcare & life sciences
|
||||
- consumer & retail
|
||||
- financial services
|
||||
@@ -53,8 +55,12 @@ export const generateQuery = async (input: string) => {
|
||||
|
||||
Note: valuation is in billions of dollars so 10b would be 10.0.
|
||||
Note: if the user asks for a rate, return it as a decimal. For example, 0.1 would be 10%.
|
||||
Ensure the query is also viable for building a chart. Use multiple columns where applicable. Use counts.
|
||||
|
||||
If the user asks for 'over time' data, return by year.
|
||||
|
||||
When searching for UK or USA, write out United Kingdom or United States respectively.
|
||||
|
||||
EVERY QUERY SHOULD RETURN QUANTITATIVE DATA THAT CAN BE PLOTTED ON A CHART! There should always be at least two columns. If the user asks for a single column, return the column and the count of the column. If the user asks for a rate, return the rate as a decimal. For example, 0.1 would be 10%.
|
||||
`,
|
||||
prompt: `Generate the query necessary to retrieve the data the user wants: ${input}`,
|
||||
schema: z.object({
|
||||
@@ -91,7 +97,7 @@ export const getCompanies = async (query: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
return data.rows as Unicorn[];
|
||||
return data.rows as Result[];
|
||||
};
|
||||
|
||||
export const explainQuery = async (input: string, sqlQuery: string) => {
|
||||
@@ -133,7 +139,7 @@ export const explainQuery = async (input: string, sqlQuery: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const generateAChart = async (results: Unicorn[], userQuery: string) => {
|
||||
export const generateAChart = async (results: Result[], userQuery: string) => {
|
||||
"use server";
|
||||
try {
|
||||
const result = await generateObject({
|
||||
@@ -162,7 +168,7 @@ export const generateAChart = async (results: Unicorn[], userQuery: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const generateChart = async (results: Unicorn[], userQuery: string) => {
|
||||
export const generateChart = async (results: Result[], userQuery: string) => {
|
||||
"use server";
|
||||
const system = `You are a data visualization expert. `;
|
||||
|
||||
@@ -196,8 +202,8 @@ export const generateChart = async (results: Unicorn[], userQuery: string) => {
|
||||
console.log(config);
|
||||
|
||||
const colors: Record<string, string> = {};
|
||||
config.yKeys.forEach((key) => {
|
||||
colors[key] = `#808080`;
|
||||
config.yKeys.forEach((key, index) => {
|
||||
colors[key] = `hsl(var(--chart-${index+1}))`;
|
||||
});
|
||||
|
||||
const updatedConfig: Config = { ...config, colors };
|
||||
@@ -302,3 +308,50 @@ export const generateChartData = async (
|
||||
throw Error();
|
||||
}
|
||||
};
|
||||
|
||||
export const generateChartConfig = async (results: Result[], userQuery: string) => {
|
||||
"use server";
|
||||
const system = `You are a SQL and data visualization expert.`;
|
||||
|
||||
try {
|
||||
const { object: config } = await generateObject({
|
||||
model: openai("gpt-4o"),
|
||||
system,
|
||||
prompt: `Given the following data from a SQL query result, generate the chart config that could visualises the data and answers the users query without any data modification.
|
||||
For multiple groups use multi-lines.
|
||||
|
||||
Here is an example complete config:
|
||||
export const chartConfig = {
|
||||
type: "pie",
|
||||
xKey: "month",
|
||||
yKeys: ["sales", "profit", "expenses"],
|
||||
colors: {
|
||||
sales: "#4CAF50", // Green for sales
|
||||
profit: "#2196F3", // Blue for profit
|
||||
expenses: "#F44336" // Red for expenses
|
||||
},
|
||||
legend: true
|
||||
}
|
||||
|
||||
User Query:
|
||||
${userQuery}
|
||||
|
||||
Data:
|
||||
${JSON.stringify(results, null, 2)}`,
|
||||
schema: configSchema,
|
||||
});
|
||||
console.log(config);
|
||||
|
||||
const colors: Record<string, string> = {};
|
||||
config.yKeys.forEach((key, index) => {
|
||||
colors[key] = `hsl(var(--chart-${index+1}))`;
|
||||
});
|
||||
|
||||
const updatedConfig: Config = { ...config, colors };
|
||||
return { config: updatedConfig };
|
||||
} catch (e) {
|
||||
// @ts-expect-errore
|
||||
console.error(e.message);
|
||||
throw new Error("Failed to generate chart suggestion");
|
||||
}
|
||||
};
|
||||
+35
-34
@@ -9,59 +9,60 @@
|
||||
}
|
||||
|
||||
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 240 10% 3.9%;
|
||||
--foreground: 0 0% 3.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 240 10% 3.9%;
|
||||
--card-foreground: 0 0% 3.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 240 10% 3.9%;
|
||||
--primary: 240 5.9% 10%;
|
||||
--popover-foreground: 0 0% 3.9%;
|
||||
--primary: 0 0% 9%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 240 4.8% 95.9%;
|
||||
--secondary-foreground: 240 5.9% 10%;
|
||||
--muted: 240 4.8% 95.9%;
|
||||
--muted-foreground: 240 3.8% 46.1%;
|
||||
--accent: 240 4.8% 95.9%;
|
||||
--accent-foreground: 240 5.9% 10%;
|
||||
--secondary: 0 0% 96.1%;
|
||||
--secondary-foreground: 0 0% 9%;
|
||||
--muted: 0 0% 96.1%;
|
||||
--muted-foreground: 0 0% 45.1%;
|
||||
--accent: 0 0% 96.1%;
|
||||
--accent-foreground: 0 0% 9%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 240 5.9% 90%;
|
||||
--input: 240 5.9% 90%;
|
||||
--ring: 240 5.9% 10%;
|
||||
--border: 0 0% 89.8%;
|
||||
--input: 0 0% 89.8%;
|
||||
--ring: 0 0% 3.9%;
|
||||
--radius: 0.5rem;
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
--chart-1: 240 2% 26%;
|
||||
--chart-2: 240 1% 42%;
|
||||
--chart-3: 240 1% 58%;
|
||||
--chart-4: 240 1% 74%;
|
||||
--chart-5: 359 2% 90%;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 240 10% 3.9%;
|
||||
--background: 0 0% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 240 10% 3.9%;
|
||||
--card: 0 0% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 240 10% 3.9%;
|
||||
--popover: 0 0% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 240 5.9% 10%;
|
||||
--secondary: 240 3.7% 15.9%;
|
||||
--primary-foreground: 0 0% 9%;
|
||||
--secondary: 0 0% 14.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 240 3.7% 15.9%;
|
||||
--muted-foreground: 240 5% 64.9%;
|
||||
--accent: 240 3.7% 15.9%;
|
||||
--muted: 0 0% 14.9%;
|
||||
--muted-foreground: 0 0% 63.9%;
|
||||
--accent: 0 0% 14.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 240 3.7% 15.9%;
|
||||
--input: 240 3.7% 15.9%;
|
||||
--ring: 240 4.9% 83.9%;
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%;
|
||||
--border: 0 0% 14.9%;
|
||||
--input: 0 0% 14.9%;
|
||||
--ring: 0 0% 83.1%;
|
||||
--chart-1: 359 2% 90%;
|
||||
--chart-2: 240 1% 74%;
|
||||
--chart-3: 240 1% 58%;
|
||||
--chart-4: 240 1% 42%;
|
||||
--chart-5: 240 2% 26%;
|
||||
}
|
||||
}
|
||||
+26
-110
@@ -27,36 +27,14 @@ import { Info, X, Search, Sparkles, Loader2, BarChart2 } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useOutsideClick } from "@/lib/use-outside-click";
|
||||
import { QueryWithTooltips } from "@/components/ui/query-with-tooltips";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { DynamicChart } from "@/components/dynamic-chart";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
|
||||
function parseCSV(csv: string) {
|
||||
const [header, ...rows] = csv.trim().split("\n");
|
||||
const columns = header.split(",");
|
||||
return rows.map((row) => {
|
||||
const values = row.split(",");
|
||||
return columns.reduce(
|
||||
(obj, col, index) => {
|
||||
obj[col] = isNaN(Number(values[index]))
|
||||
? values[index]
|
||||
: Number(values[index]);
|
||||
return obj;
|
||||
},
|
||||
{} as Record<string, string | number>,
|
||||
);
|
||||
});
|
||||
}
|
||||
import { SkeletonCard } from "@/components/skeleton-card";
|
||||
|
||||
export default function Component() {
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
const [results, setResults] = useState<Unicorn[]>([]);
|
||||
const [results, setResults] = useState<Record<string, string | number>[]>([]);
|
||||
const [columns, setColumns] = useState<string[]>([]);
|
||||
const [activeQuery, setActiveQuery] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -67,7 +45,6 @@ export default function Component() {
|
||||
>();
|
||||
const [loadingExplanation, setLoadingExplanation] = useState(false);
|
||||
const [isChartModalOpen, setIsChartModalOpen] = useState(false);
|
||||
const [chartData, setChartData] = useState<Data | null>(null);
|
||||
const [chartConfig, setChartConfig] = useState<Config | null>(null);
|
||||
const [isGeneratingChart, setIsGeneratingChart] = useState(false);
|
||||
|
||||
@@ -94,6 +71,12 @@ export default function Component() {
|
||||
}, [isModalOpen, isChartModalOpen]);
|
||||
|
||||
const suggestionQueries = [
|
||||
"Show the number of unicorns founded each year over the past two decades",
|
||||
"Track the average valuation of unicorns year by year",
|
||||
"Display the cumulative total valuation of unicorns over time",
|
||||
"Illustrate the growth in number of AI unicorns annually",
|
||||
"Compare the yearly funding amounts for fintech vs healthtech unicorns",
|
||||
"Plot the trend of unicorn valuations in the US vs China over time",
|
||||
"Which cities have with most AI unicorns",
|
||||
"Show the countries with highest unicorn density",
|
||||
"Show the number of unicorns (grouped by year) over the past decade",
|
||||
@@ -122,6 +105,9 @@ export default function Component() {
|
||||
setResults(companies);
|
||||
setColumns(columns);
|
||||
setLoading(false);
|
||||
const generation = await generateChart(companies, inputValue);
|
||||
console.log(generation.config);
|
||||
setChartConfig(generation.config);
|
||||
};
|
||||
|
||||
const handleSuggestionClick = (suggestion: string) => {
|
||||
@@ -134,7 +120,6 @@ export default function Component() {
|
||||
setActiveQuery("");
|
||||
setQueryExplanations(null);
|
||||
setChartConfig(null);
|
||||
setChartData(null);
|
||||
setIsGeneratingChart(false);
|
||||
};
|
||||
|
||||
@@ -185,14 +170,8 @@ export default function Component() {
|
||||
try {
|
||||
const generation = await generateChart(results, inputValue);
|
||||
setChartConfig(generation.config);
|
||||
console.log(results, generation.config);
|
||||
setIsChartModalOpen(true);
|
||||
const { data } = await generateChartData(
|
||||
generation.config,
|
||||
results,
|
||||
inputValue,
|
||||
);
|
||||
const parsed = parseCSV(data);
|
||||
setChartData(parsed);
|
||||
} catch (error) {
|
||||
console.error("Failed to generate chart:", error);
|
||||
} finally {
|
||||
@@ -215,33 +194,6 @@ export default function Component() {
|
||||
<Sparkles className="mr-2" />
|
||||
Unicorn Query
|
||||
</span>
|
||||
{chartConfig ? (
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
onClick={() => setIsChartModalOpen(true)}
|
||||
>
|
||||
Show chart
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleGenerateChart}
|
||||
disabled={results.length === 0 || isGeneratingChart}
|
||||
>
|
||||
{isGeneratingChart ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
Generating Chart...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<BarChart2 className="h-4 w-4 mr-2" />
|
||||
Generate Chart
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</h1>
|
||||
<form onSubmit={handleSubmit} className="mb-6">
|
||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center space-y-4 sm:space-y-0 sm:space-x-4">
|
||||
@@ -274,7 +226,7 @@ export default function Component() {
|
||||
</div>
|
||||
</form>
|
||||
<div className="h-[600px] flex flex-col">
|
||||
<div className="flex-grow overflow-hidden relative">
|
||||
<div className="flex-grow overflow-hidden relative h-full">
|
||||
<AnimatePresence>
|
||||
{!submitted && (
|
||||
<motion.div
|
||||
@@ -307,7 +259,7 @@ export default function Component() {
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="h-full flex flex-col"
|
||||
className="h-full relative flex flex-col"
|
||||
>
|
||||
{activeQuery.length > 0 && (
|
||||
<div className="mb-4 relative group">
|
||||
@@ -346,10 +298,13 @@ export default function Component() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-grow flex flex-col">
|
||||
<Tabs defaultValue="table" className="w-full flex-grow flex flex-col">
|
||||
<Tabs
|
||||
defaultValue="table"
|
||||
className="w-full flex-grow flex flex-col"
|
||||
>
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="table">Table</TabsTrigger>
|
||||
<TabsTrigger value="charts">Chart</TabsTrigger>
|
||||
<TabsTrigger value="charts" disabled={Object.keys(results[0] || {}).length <= 1}>Chart</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="table" className="flex-grow">
|
||||
<div className="h-[10px] bg-orange-500">
|
||||
@@ -389,42 +344,18 @@ export default function Component() {
|
||||
</Table>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="charts" className="flex-grow overflow-auto">
|
||||
<TabsContent
|
||||
value="charts"
|
||||
className="flex-grow overflow-auto"
|
||||
>
|
||||
<div className="mt-4">
|
||||
{chartConfig ? (
|
||||
<DynamicChart
|
||||
chartData={chartData}
|
||||
chartData={results}
|
||||
chartConfig={chartConfig}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleGenerateChart}
|
||||
disabled={isGeneratingChart}
|
||||
>
|
||||
{isGeneratingChart ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
Generating Chart...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<BarChart2 className="h-4 w-4 mr-2" />
|
||||
Generate Chart
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
{chartConfig && (
|
||||
<>
|
||||
<p className="mt-4 text-sm">
|
||||
{chartConfig.description}
|
||||
</p>
|
||||
<p className="mt-4 text-sm">
|
||||
{chartConfig.takeaway}
|
||||
</p>
|
||||
</>
|
||||
<SkeletonCard />
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
@@ -525,21 +456,6 @@ export default function Component() {
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<Dialog open={isChartModalOpen} onOpenChange={setIsChartModalOpen}>
|
||||
<DialogContent className="sm:max-w-[800px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{chartConfig?.title || "Chart"}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="mt-4">
|
||||
{chartConfig && (
|
||||
<DynamicChart chartData={chartData} chartConfig={chartConfig} />
|
||||
)}
|
||||
<p className="mt-4 text-sm">{chartConfig?.description}</p>
|
||||
<p className="mt-4 text-sm">{chartConfig?.takeaway}</p>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
+129
-31
@@ -28,46 +28,141 @@ import { Input } from "@/components/ui/input";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { generateChart } from "@/app/actions";
|
||||
import { Config, Data, Unicorn } from "@/lib/types";
|
||||
import { Label } from "recharts";
|
||||
import { transformDataForMultiLineChart } from "@/lib/rechart-format";
|
||||
|
||||
function toTitleCase(str: string): string {
|
||||
return str
|
||||
.split("_")
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(" ");
|
||||
}
|
||||
const colors = [
|
||||
"hsl(var(--chart-1))",
|
||||
"hsl(var(--chart-2))",
|
||||
"hsl(var(--chart-3))",
|
||||
"hsl(var(--chart-4))",
|
||||
"hsl(var(--chart-5))",
|
||||
"hsl(var(--chart-6))",
|
||||
"hsl(var(--chart-7))",
|
||||
"hsl(var(--chart-8))",
|
||||
];
|
||||
|
||||
export function DynamicChart({
|
||||
chartData,
|
||||
chartConfig,
|
||||
}: {
|
||||
chartData: Data | null;
|
||||
chartData: Data;
|
||||
chartConfig: Config;
|
||||
}) {
|
||||
const renderChart = () => {
|
||||
if (!chartData || !chartConfig) return <div>No chart data</div>;
|
||||
const parsedChartData = chartData.map((item) => {
|
||||
const parsedItem: { [key: string]: any } = {};
|
||||
for (const [key, value] of Object.entries(item)) {
|
||||
parsedItem[key] = isNaN(Number(value)) ? value : Number(value);
|
||||
}
|
||||
return parsedItem;
|
||||
});
|
||||
|
||||
chartData = parsedChartData;
|
||||
|
||||
const processChartData = (data: Data, chartType: string) => {
|
||||
if (chartType === "bar" || chartType === "pie") {
|
||||
if (data.length <= 8) {
|
||||
return data;
|
||||
}
|
||||
|
||||
const subset = data.slice(0, 20);
|
||||
return subset;
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
chartData = processChartData(chartData, chartConfig.type);
|
||||
console.log(chartData);
|
||||
|
||||
switch (chartConfig.type) {
|
||||
case "bar":
|
||||
return (
|
||||
<BarChart data={chartData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey={chartConfig.xKey} />
|
||||
<YAxis />
|
||||
<XAxis dataKey={chartConfig.xKey}>
|
||||
<Label
|
||||
value={toTitleCase(chartConfig.xKey)}
|
||||
offset={0}
|
||||
position="insideBottom"
|
||||
/>
|
||||
</XAxis>
|
||||
<YAxis>
|
||||
<Label
|
||||
value={toTitleCase(chartConfig.yKeys[0])}
|
||||
angle={-90}
|
||||
position="insideLeft"
|
||||
/>
|
||||
</YAxis>
|
||||
<ChartTooltip content={<ChartTooltipContent />} />
|
||||
{chartConfig.legend && <Legend />}
|
||||
{chartConfig.yKeys.map((key) => (
|
||||
<Bar key={key} dataKey={key} />
|
||||
{chartConfig.yKeys.map((key, index) => (
|
||||
<Bar
|
||||
key={key}
|
||||
dataKey={key}
|
||||
fill={colors[index % colors.length]}
|
||||
/>
|
||||
))}
|
||||
</BarChart>
|
||||
);
|
||||
case "line":
|
||||
const { data, xAxisField, lineFields } = transformDataForMultiLineChart(
|
||||
chartData,
|
||||
chartConfig.measurementColumn ?? "value",
|
||||
);
|
||||
const useTransformedData =
|
||||
chartConfig.multipleLines &&
|
||||
chartConfig.measurementColumn &&
|
||||
chartConfig.yKeys.includes(chartConfig.measurementColumn);
|
||||
console.log(useTransformedData, "useTransformedData");
|
||||
// const useTransformedData = false;
|
||||
return (
|
||||
<LineChart data={chartData}>
|
||||
<LineChart data={useTransformedData ? data : chartData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey={chartConfig.xKey} />
|
||||
<YAxis />
|
||||
<XAxis
|
||||
dataKey={useTransformedData ? chartConfig.xKey : chartConfig.xKey}
|
||||
>
|
||||
<Label
|
||||
value={toTitleCase(
|
||||
useTransformedData ? xAxisField : chartConfig.xKey,
|
||||
)}
|
||||
offset={0}
|
||||
position="insideBottom"
|
||||
/>
|
||||
</XAxis>
|
||||
<YAxis>
|
||||
<Label
|
||||
value={toTitleCase(chartConfig.yKeys[0])}
|
||||
angle={-90}
|
||||
position="insideLeft"
|
||||
/>
|
||||
</YAxis>
|
||||
<ChartTooltip content={<ChartTooltipContent />} />
|
||||
{chartConfig.legend && <Legend />}
|
||||
{chartConfig.yKeys.map((key) => (
|
||||
<Line
|
||||
key={key}
|
||||
type="monotone"
|
||||
dataKey={key}
|
||||
/>
|
||||
))}
|
||||
{useTransformedData
|
||||
? lineFields.map((key, index) => (
|
||||
<Line
|
||||
key={key}
|
||||
type="monotone"
|
||||
dataKey={key}
|
||||
stroke={colors[index % colors.length]}
|
||||
/>
|
||||
))
|
||||
: chartConfig.yKeys.map((key, index) => (
|
||||
<Line
|
||||
key={key}
|
||||
type="monotone"
|
||||
dataKey={key}
|
||||
stroke={colors[index % colors.length]}
|
||||
/>
|
||||
))}
|
||||
</LineChart>
|
||||
);
|
||||
case "area":
|
||||
@@ -78,13 +173,13 @@ export function DynamicChart({
|
||||
<YAxis />
|
||||
<ChartTooltip content={<ChartTooltipContent />} />
|
||||
{chartConfig.legend && <Legend />}
|
||||
{chartConfig.yKeys.map((key) => (
|
||||
{chartConfig.yKeys.map((key, index) => (
|
||||
<Area
|
||||
key={key}
|
||||
type="monotone"
|
||||
dataKey={key}
|
||||
stroke={chartConfig.colors[key]}
|
||||
fill={chartConfig.colors[key]}
|
||||
fill={colors[index % colors.length]}
|
||||
stroke={colors[index % colors.length]}
|
||||
/>
|
||||
))}
|
||||
</AreaChart>
|
||||
@@ -98,12 +193,12 @@ export function DynamicChart({
|
||||
nameKey={chartConfig.xKey}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
outerRadius={150}
|
||||
outerRadius={120}
|
||||
>
|
||||
{chartData.map((entry, index) => (
|
||||
{chartData.map((_, index) => (
|
||||
<Cell
|
||||
key={`cell-${index}`}
|
||||
fill={chartConfig.colors[entry[chartConfig.xKey]]}
|
||||
fill={colors[index % colors.length]}
|
||||
/>
|
||||
))}
|
||||
</Pie>
|
||||
@@ -117,26 +212,29 @@ export function DynamicChart({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
{!chartData ? (
|
||||
<div className="flex items-center justify-center h-[400px] w-full animate-pulse bg-neutral-100"></div>
|
||||
) : null}
|
||||
<div className="w-full flex flex-col justify-center items-center">
|
||||
<h2 className="text-lg font-bold mb-2">{chartConfig.title}</h2>
|
||||
{chartConfig && chartData && (
|
||||
<ChartContainer
|
||||
config={chartConfig.yKeys.reduce(
|
||||
(acc, key) => {
|
||||
acc[key] = { label: key, color: chartConfig.colors[key] };
|
||||
(acc, key, index) => {
|
||||
acc[key] = {
|
||||
label: key,
|
||||
color: colors[index % colors.length],
|
||||
};
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, { label: string; color: string }>,
|
||||
)}
|
||||
className="h-[400px]"
|
||||
className="h-[320px] w-full"
|
||||
>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
{renderChart()}
|
||||
</ResponsiveContainer>
|
||||
{renderChart()}
|
||||
</ChartContainer>
|
||||
)}
|
||||
<div className="">
|
||||
<p className="mt-4 text-sm">{chartConfig.description}</p>
|
||||
<p className="mt-4 text-sm">{chartConfig.takeaway}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
export function SkeletonCard() {
|
||||
return (
|
||||
<div className="flex flex-col space-y-3 w-full">
|
||||
<div className="w-full flex items-center justify-center">
|
||||
<Skeleton className="h-[36px] w-72 rounded-xl" />
|
||||
</div>
|
||||
<Skeleton className="h-[300px] w-full rounded-xl" />
|
||||
<div className="w-full flex justify-center items-center">
|
||||
<Skeleton className="h-4 w-20" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-[450px]" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-[350px]" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Skeleton({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={cn("animate-pulse rounded-md bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Skeleton }
|
||||
@@ -0,0 +1,52 @@
|
||||
type InputDataPoint = Record<string, string | number>;
|
||||
|
||||
interface TransformedDataPoint {
|
||||
[key: string]: string | number | null;
|
||||
}
|
||||
|
||||
interface TransformationResult {
|
||||
data: TransformedDataPoint[];
|
||||
xAxisField: string;
|
||||
lineFields: string[];
|
||||
}
|
||||
|
||||
export function transformDataForMultiLineChart(
|
||||
data: InputDataPoint[],
|
||||
measureColumn: string
|
||||
): TransformationResult {
|
||||
console.log("Input data:", data);
|
||||
|
||||
const fields = Object.keys(data[0]);
|
||||
const xAxisField = fields.find(field => field !== measureColumn && field !== 'country') || '';
|
||||
const lineField = 'country';
|
||||
|
||||
console.log("X-axis field:", xAxisField);
|
||||
console.log("Line field:", lineField);
|
||||
|
||||
const xAxisValues = Array.from(new Set(data.map(item => String(item[xAxisField]))));
|
||||
const lineCategories = Array.from(new Set(data.map(item => String(item[lineField]))));
|
||||
|
||||
console.log("X-axis values:", xAxisValues);
|
||||
console.log("Line categories:", lineCategories);
|
||||
|
||||
const transformedData: TransformedDataPoint[] = xAxisValues.map(xValue => {
|
||||
const dataPoint: TransformedDataPoint = { [xAxisField]: xValue };
|
||||
lineCategories.forEach(category => {
|
||||
const matchingItem = data.find(item =>
|
||||
String(item[xAxisField]) === xValue && String(item[lineField]) === category
|
||||
);
|
||||
dataPoint[category] = matchingItem ? matchingItem[measureColumn] : null;
|
||||
});
|
||||
return dataPoint;
|
||||
});
|
||||
|
||||
transformedData.sort((a, b) => Number(a[xAxisField]) - Number(b[xAxisField]));
|
||||
|
||||
console.log("Transformed data:", transformedData);
|
||||
|
||||
return {
|
||||
data: transformedData,
|
||||
xAxisField,
|
||||
lineFields: lineCategories
|
||||
};
|
||||
}
|
||||
+5
-1
@@ -11,6 +11,8 @@ export type Unicorn = {
|
||||
select_investors: string;
|
||||
};
|
||||
|
||||
export type Result = Record<string, string | number>;
|
||||
|
||||
export const explanationSchema = z.object({
|
||||
section: z.string(),
|
||||
explanation: z.string(),
|
||||
@@ -66,7 +68,9 @@ export const configSchema = z
|
||||
type: z.enum(["bar", "line", "area", "pie"]).describe("Type of chart"),
|
||||
title: z.string(),
|
||||
xKey: z.string().describe("Key for x-axis or category"),
|
||||
yKeys: z.array(z.string()).describe("Key(s) for y-axis values"),
|
||||
yKeys: z.array(z.string()).describe("Key(s) for y-axis values this is typically the quantitative column"),
|
||||
multipleLines: z.boolean().describe("For line charts only: whether the chart is comparing groups of data.").optional(),
|
||||
measurementColumn: z.string().describe("For line charts only: key for quantitative y-axis column to measure against (eg. values, counts etc.)").optional(),
|
||||
colors: z
|
||||
.record(
|
||||
z.string().describe("Any of the yKeys"),
|
||||
|
||||
Reference in new issue
Block a user