mirror of
https://github.com/formbricks/formbricks.git
synced 2026-03-05 00:48:03 -06:00
fix: improve chart type safety and DRY up chart renderer
This commit is contained in:
@@ -28,16 +28,13 @@ import {
|
||||
parseQueryToState,
|
||||
} from "@/modules/ee/analysis/lib/query-builder";
|
||||
import { formatCubeColumnHeader } from "@/modules/ee/analysis/lib/schema-definition";
|
||||
import type { AnalyticsResponse, TChartDataRow } from "@/modules/ee/analysis/types/analysis";
|
||||
import type { AnalyticsResponse, TApiChartType, TChartDataRow } from "@/modules/ee/analysis/types/analysis";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { LoadingSpinner } from "@/modules/ui/components/loading-spinner";
|
||||
|
||||
// Filter out table, map, and scatter charts
|
||||
const AVAILABLE_CHART_TYPES = CHART_TYPES.filter((type) => !["table", "map", "scatter"].includes(type.id));
|
||||
|
||||
interface AdvancedChartBuilderProps {
|
||||
environmentId: string;
|
||||
initialChartType?: string;
|
||||
initialChartType?: TApiChartType;
|
||||
initialQuery?: TChartQuery;
|
||||
hidePreview?: boolean;
|
||||
onChartGenerated?: (data: AnalyticsResponse) => void;
|
||||
@@ -46,7 +43,7 @@ interface AdvancedChartBuilderProps {
|
||||
}
|
||||
|
||||
type Action =
|
||||
| { type: "SET_CHART_TYPE"; payload: string }
|
||||
| { type: "SET_CHART_TYPE"; payload: TApiChartType }
|
||||
| { type: "SET_MEASURES"; payload: string[] }
|
||||
| { type: "SET_CUSTOM_MEASURES"; payload: CustomMeasure[] }
|
||||
| { type: "SET_DIMENSIONS"; payload: string[] }
|
||||
@@ -141,9 +138,10 @@ export function AdvancedChartBuilder({
|
||||
useEffect(() => {
|
||||
if (initialQuery && !isInitialized) {
|
||||
setIsInitialized(true);
|
||||
const chartType = state.chartType;
|
||||
executeQueryAction({
|
||||
environmentId,
|
||||
query: initialQuery as TChartQuery,
|
||||
query: initialQuery,
|
||||
}).then((result) => {
|
||||
if (result?.serverError) {
|
||||
setError(getFormattedErrorMessage(result));
|
||||
@@ -154,19 +152,14 @@ export function AdvancedChartBuilder({
|
||||
setChartData(data);
|
||||
setQuery(initialQuery);
|
||||
lastStateRef.current = JSON.stringify({
|
||||
chartType: state.chartType,
|
||||
chartType,
|
||||
measures: state.selectedMeasures,
|
||||
dimensions: state.selectedDimensions,
|
||||
filters: state.filters,
|
||||
timeDimension: state.timeDimension,
|
||||
});
|
||||
if (onChartGenerated) {
|
||||
const analyticsResponse: AnalyticsResponse = {
|
||||
query: initialQuery,
|
||||
chartType: state.chartType as AnalyticsResponse["chartType"],
|
||||
data,
|
||||
};
|
||||
onChartGenerated(analyticsResponse);
|
||||
if (onChartGenerated && chartType) {
|
||||
onChartGenerated({ query: initialQuery, chartType, data });
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -202,26 +195,22 @@ export function AdvancedChartBuilder({
|
||||
return;
|
||||
}
|
||||
|
||||
const chartType = state.chartType;
|
||||
const updatedQuery = buildCubeQuery(state);
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
executeQueryAction({
|
||||
environmentId,
|
||||
query: updatedQuery as TChartQuery,
|
||||
query: updatedQuery,
|
||||
})
|
||||
.then((result) => {
|
||||
const data = Array.isArray(result?.data) ? result.data : [];
|
||||
if (data.length > 0) {
|
||||
setChartData(data);
|
||||
setQuery(updatedQuery);
|
||||
if (onChartGenerated) {
|
||||
const analyticsResponse: AnalyticsResponse = {
|
||||
query: updatedQuery,
|
||||
chartType: state.chartType as AnalyticsResponse["chartType"],
|
||||
data,
|
||||
};
|
||||
onChartGenerated(analyticsResponse);
|
||||
if (onChartGenerated && chartType) {
|
||||
onChartGenerated({ query: updatedQuery, chartType, data });
|
||||
}
|
||||
} else if (result?.serverError) {
|
||||
setError(getFormattedErrorMessage(result));
|
||||
@@ -282,7 +271,7 @@ export function AdvancedChartBuilder({
|
||||
|
||||
const result = await executeQueryAction({
|
||||
environmentId,
|
||||
query: cubeQuery as TChartQuery,
|
||||
query: cubeQuery,
|
||||
});
|
||||
|
||||
if (result?.serverError) {
|
||||
@@ -296,13 +285,8 @@ export function AdvancedChartBuilder({
|
||||
setError(null);
|
||||
toast.success(t("environments.analysis.charts.query_executed_successfully"));
|
||||
|
||||
if (onChartGenerated) {
|
||||
const analyticsResponse: AnalyticsResponse = {
|
||||
query: cubeQuery,
|
||||
chartType: state.chartType as AnalyticsResponse["chartType"],
|
||||
data,
|
||||
};
|
||||
onChartGenerated(analyticsResponse);
|
||||
if (onChartGenerated && state.chartType) {
|
||||
onChartGenerated({ query: cubeQuery, chartType: state.chartType, data });
|
||||
}
|
||||
} else {
|
||||
throw new Error(t("environments.analysis.charts.no_data_returned"));
|
||||
@@ -436,7 +420,7 @@ export function AdvancedChartBuilder({
|
||||
{t("environments.analysis.charts.chart_builder_choose_chart_type")}
|
||||
</h2>
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4">
|
||||
{AVAILABLE_CHART_TYPES.map((chart) => {
|
||||
{CHART_TYPES.map((chart) => {
|
||||
const isSelected = state.chartType === chart.id;
|
||||
return (
|
||||
<button
|
||||
@@ -514,7 +498,7 @@ export function AdvancedChartBuilder({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{chartData && Array.isArray(chartData) && chartData.length > 0 && !isLoading && (
|
||||
{chartData && Array.isArray(chartData) && chartData.length > 0 && !isLoading && state.chartType && (
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-lg border border-gray-200 bg-white p-4">
|
||||
<ChartRenderer chartType={state.chartType} data={chartData} />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { format, isValid, parseISO } from "date-fns";
|
||||
import type { ElementType, ReactNode } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Area,
|
||||
@@ -24,9 +25,21 @@ import {
|
||||
resolveChartKeys,
|
||||
} from "@/modules/ee/analysis/charts/lib/chart-utils";
|
||||
import { formatCubeColumnHeader } from "@/modules/ee/analysis/lib/schema-definition";
|
||||
import type { TChartDataRow } from "@/modules/ee/analysis/types/analysis";
|
||||
import type { TApiChartType, TChartDataRow } from "@/modules/ee/analysis/types/analysis";
|
||||
import type { ChartConfig } from "@/modules/ui/components/chart";
|
||||
import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@/modules/ui/components/chart";
|
||||
|
||||
function formatXAxisTick(value: unknown): string {
|
||||
if (value == null) return "";
|
||||
let str: string;
|
||||
if (typeof value === "string") str = value;
|
||||
else if (typeof value === "number") str = String(value);
|
||||
else return "";
|
||||
const date = parseISO(str);
|
||||
if (isValid(date)) return format(date, "MMM d, yyyy");
|
||||
return str;
|
||||
}
|
||||
|
||||
function ChartTooltipRow({ value, dataKey }: Readonly<{ value: unknown; dataKey: string }>) {
|
||||
return (
|
||||
<>
|
||||
@@ -53,20 +66,55 @@ function createTooltipFormatter(dataKey: string) {
|
||||
}
|
||||
|
||||
/** Tooltip content for bar/line/area charts with formatted label and value. Extracted to avoid inline component definitions. */
|
||||
function CartesianChartTooltip({
|
||||
dataKey,
|
||||
formatXAxisTick,
|
||||
}: Readonly<{ dataKey: string; formatXAxisTick: (value: unknown) => string }>) {
|
||||
function CartesianChartTooltip({ dataKey }: Readonly<{ dataKey: string }>) {
|
||||
return <ChartTooltipContent labelFormatter={formatXAxisTick} formatter={createTooltipFormatter(dataKey)} />;
|
||||
}
|
||||
|
||||
/** Shared layout for bar, line, and area charts to avoid duplicating grid/axis/tooltip boilerplate. */
|
||||
function CartesianChart({
|
||||
data,
|
||||
xAxisKey,
|
||||
dataKey,
|
||||
chartConfig,
|
||||
chart: Chart,
|
||||
children,
|
||||
}: Readonly<{
|
||||
data: TChartDataRow[];
|
||||
xAxisKey: string;
|
||||
dataKey: string;
|
||||
chartConfig: ChartConfig;
|
||||
chart: ElementType;
|
||||
children: ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<div className="h-64 w-full">
|
||||
<ChartContainer config={chartConfig} className="h-full w-full">
|
||||
<Chart data={data}>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} />
|
||||
<XAxis
|
||||
dataKey={xAxisKey}
|
||||
tickLine={false}
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
tickFormatter={formatXAxisTick}
|
||||
/>
|
||||
<YAxis tickLine={false} axisLine={false} />
|
||||
<ChartTooltip content={<CartesianChartTooltip dataKey={dataKey} />} />
|
||||
{children}
|
||||
</Chart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ChartRendererProps {
|
||||
chartType: string;
|
||||
chartType: TApiChartType;
|
||||
data: TChartDataRow[];
|
||||
}
|
||||
|
||||
export const ChartRenderer = ({ chartType, data }: Readonly<ChartRendererProps>) => {
|
||||
export function ChartRenderer({ chartType, data }: Readonly<ChartRendererProps>) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
return (
|
||||
<div className="flex h-64 items-center justify-center text-gray-500">
|
||||
@@ -76,114 +124,63 @@ export const ChartRenderer = ({ chartType, data }: Readonly<ChartRendererProps>)
|
||||
}
|
||||
|
||||
const { xAxisKey, dataKey } = resolveChartKeys(data, chartType);
|
||||
const chartConfig = {
|
||||
const chartConfig: ChartConfig = {
|
||||
[dataKey]: {
|
||||
label: formatCubeColumnHeader(dataKey),
|
||||
color: CHART_BRAND_DARK,
|
||||
},
|
||||
};
|
||||
|
||||
const formatXAxisTick = (value: unknown): string => {
|
||||
if (value == null) return "";
|
||||
let str: string;
|
||||
if (typeof value === "string") str = value;
|
||||
else if (typeof value === "number") str = String(value);
|
||||
else return "";
|
||||
const date = parseISO(str);
|
||||
if (isValid(date)) return format(date, "MMM d, yyyy");
|
||||
return str;
|
||||
};
|
||||
|
||||
switch (chartType) {
|
||||
case "bar":
|
||||
return (
|
||||
<div className="h-64 min-h-[256px] w-full">
|
||||
<ChartContainer config={chartConfig} className="h-full w-full">
|
||||
<BarChart data={data}>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} />
|
||||
<XAxis
|
||||
dataKey={xAxisKey}
|
||||
tickLine={false}
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
tickFormatter={formatXAxisTick}
|
||||
/>
|
||||
<YAxis tickLine={false} axisLine={false} />
|
||||
<ChartTooltip
|
||||
content={<CartesianChartTooltip dataKey={dataKey} formatXAxisTick={formatXAxisTick} />}
|
||||
/>
|
||||
<Bar dataKey={dataKey} fill={CHART_BRAND_DARK} radius={4} />
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
<CartesianChart
|
||||
chart={BarChart}
|
||||
data={data}
|
||||
xAxisKey={xAxisKey}
|
||||
dataKey={dataKey}
|
||||
chartConfig={chartConfig}>
|
||||
<Bar dataKey={dataKey} fill={CHART_BRAND_DARK} radius={4} />
|
||||
</CartesianChart>
|
||||
);
|
||||
case "line":
|
||||
return (
|
||||
<div className="h-64 min-h-[256px] w-full">
|
||||
<ChartContainer config={chartConfig} className="h-full w-full">
|
||||
<LineChart data={data}>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} />
|
||||
<XAxis
|
||||
dataKey={xAxisKey}
|
||||
tickLine={false}
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
tickFormatter={formatXAxisTick}
|
||||
/>
|
||||
<YAxis tickLine={false} axisLine={false} />
|
||||
<ChartTooltip
|
||||
content={<CartesianChartTooltip dataKey={dataKey} formatXAxisTick={formatXAxisTick} />}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey={dataKey}
|
||||
stroke={CHART_BRAND_DARK}
|
||||
strokeWidth={3}
|
||||
dot={{ fill: CHART_BRAND_DARK, r: 4 }}
|
||||
activeDot={{ r: 6 }}
|
||||
/>
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
<CartesianChart
|
||||
chart={LineChart}
|
||||
data={data}
|
||||
xAxisKey={xAxisKey}
|
||||
dataKey={dataKey}
|
||||
chartConfig={chartConfig}>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey={dataKey}
|
||||
stroke={CHART_BRAND_DARK}
|
||||
strokeWidth={3}
|
||||
dot={{ fill: CHART_BRAND_DARK, r: 4 }}
|
||||
activeDot={{ r: 6 }}
|
||||
/>
|
||||
</CartesianChart>
|
||||
);
|
||||
case "area":
|
||||
return (
|
||||
<div className="h-64 min-h-[256px] w-full">
|
||||
<ChartContainer config={chartConfig} className="h-full w-full">
|
||||
<AreaChart data={data}>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} />
|
||||
<XAxis
|
||||
dataKey={xAxisKey}
|
||||
tickLine={false}
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
tickFormatter={formatXAxisTick}
|
||||
/>
|
||||
<YAxis tickLine={false} axisLine={false} />
|
||||
<ChartTooltip
|
||||
content={<CartesianChartTooltip dataKey={dataKey} formatXAxisTick={formatXAxisTick} />}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey={dataKey}
|
||||
stroke={CHART_BRAND_DARK}
|
||||
fill={CHART_BRAND_LIGHT}
|
||||
fillOpacity={0.4}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
<CartesianChart
|
||||
chart={AreaChart}
|
||||
data={data}
|
||||
xAxisKey={xAxisKey}
|
||||
dataKey={dataKey}
|
||||
chartConfig={chartConfig}>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey={dataKey}
|
||||
stroke={CHART_BRAND_DARK}
|
||||
fill={CHART_BRAND_LIGHT}
|
||||
fillOpacity={0.4}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</CartesianChart>
|
||||
);
|
||||
case "pie":
|
||||
case "donut": {
|
||||
if (!dataKey || !xAxisKey) {
|
||||
return (
|
||||
<div className="flex h-64 items-center justify-center text-gray-500">
|
||||
{t("environments.analysis.charts.unable_to_determine_chart_data_structure")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const pieResult = preparePieData(data, dataKey);
|
||||
if (!pieResult) {
|
||||
return (
|
||||
@@ -195,9 +192,9 @@ export const ChartRenderer = ({ chartType, data }: Readonly<ChartRendererProps>)
|
||||
const { processedData, colors } = pieResult;
|
||||
|
||||
return (
|
||||
<div className="h-64 min-h-[256px] w-full min-w-0">
|
||||
<div className="h-64 w-full min-w-0">
|
||||
<ChartContainer config={chartConfig} className="h-full w-full min-w-0">
|
||||
<PieChart width={400} height={256}>
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={processedData}
|
||||
dataKey={dataKey}
|
||||
@@ -234,7 +231,7 @@ export const ChartRenderer = ({ chartType, data }: Readonly<ChartRendererProps>)
|
||||
<div className="flex h-64 items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="text-4xl font-bold text-gray-900">{total.toLocaleString()}</div>
|
||||
<div className="mt-2 text-sm text-gray-500">{dataKey}</div>
|
||||
<div className="mt-2 text-sm text-gray-500">{formatCubeColumnHeader(dataKey)}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -246,4 +243,4 @@ export const ChartRenderer = ({ chartType, data }: Readonly<ChartRendererProps>)
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { AIQuerySection } from "@/modules/ee/analysis/charts/components/ai-query
|
||||
import { ChartDialogFooterWithModals } from "@/modules/ee/analysis/charts/components/chart-dialog-footer-with-modals";
|
||||
import { ChartPreview } from "@/modules/ee/analysis/charts/components/chart-preview";
|
||||
import { ManualChartBuilder } from "@/modules/ee/analysis/charts/components/manual-chart-builder";
|
||||
import { AnalyticsResponse } from "@/modules/ee/analysis/types/analysis";
|
||||
import type { AnalyticsResponse, TApiChartType } from "@/modules/ee/analysis/types/analysis";
|
||||
import {
|
||||
Dialog,
|
||||
DialogBody,
|
||||
@@ -24,8 +24,8 @@ interface CreateChartViewProps {
|
||||
chartData: AnalyticsResponse | null;
|
||||
chartName: string;
|
||||
onChartNameChange: (name: string) => void;
|
||||
selectedChartType: string;
|
||||
onSelectedChartTypeChange: (type: string) => void;
|
||||
selectedChartType: TApiChartType | "";
|
||||
onSelectedChartTypeChange: (type: TApiChartType) => void;
|
||||
shouldShowAdvancedBuilder: boolean;
|
||||
onChartGenerated: (data: AnalyticsResponse) => void;
|
||||
onAdvancedBuilderSave: (savedChartId: string) => void;
|
||||
@@ -111,7 +111,7 @@ export function CreateChartView({
|
||||
{shouldShowAdvancedBuilder && (
|
||||
<AdvancedChartBuilder
|
||||
environmentId={environmentId}
|
||||
initialChartType={selectedChartType || chartData?.chartType || ""}
|
||||
initialChartType={selectedChartType || chartData?.chartType}
|
||||
initialQuery={chartData?.query}
|
||||
hidePreview={true}
|
||||
onChartGenerated={handleAdvancedChartGenerated}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { AdvancedChartBuilder } from "@/modules/ee/analysis/charts/components/ad
|
||||
import { ChartDialogFooterWithModals } from "@/modules/ee/analysis/charts/components/chart-dialog-footer-with-modals";
|
||||
import { ChartPreview } from "@/modules/ee/analysis/charts/components/chart-preview";
|
||||
import { ManualChartBuilder } from "@/modules/ee/analysis/charts/components/manual-chart-builder";
|
||||
import { AnalyticsResponse } from "@/modules/ee/analysis/types/analysis";
|
||||
import type { AnalyticsResponse, TApiChartType } from "@/modules/ee/analysis/types/analysis";
|
||||
import {
|
||||
Dialog,
|
||||
DialogBody,
|
||||
@@ -26,8 +26,8 @@ interface EditChartViewProps {
|
||||
isLoadingChart?: boolean;
|
||||
chartName: string;
|
||||
onChartNameChange: (name: string) => void;
|
||||
selectedChartType: string;
|
||||
onChartTypeChange: (type: string) => void;
|
||||
selectedChartType: TApiChartType | "";
|
||||
onChartTypeChange: (type: TApiChartType) => void;
|
||||
onChartGenerated: (data: AnalyticsResponse) => void;
|
||||
onAdvancedBuilderSave: (savedChartId: string) => void;
|
||||
onAdvancedBuilderAddToDashboard: (savedChartId: string, dashboardId?: string) => void;
|
||||
@@ -93,7 +93,7 @@ export function EditChartView({
|
||||
<ManualChartBuilder selectedChartType={selectedChartType} onChartTypeSelect={onChartTypeChange} />
|
||||
<AdvancedChartBuilder
|
||||
environmentId={environmentId}
|
||||
initialChartType={selectedChartType || chartData?.chartType || ""}
|
||||
initialChartType={selectedChartType || chartData?.chartType}
|
||||
initialQuery={chartData?.query ?? initialQuery}
|
||||
hidePreview={true}
|
||||
onChartGenerated={onChartGenerated}
|
||||
|
||||
@@ -3,14 +3,13 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { cn } from "@/lib/cn";
|
||||
import { CHART_TYPES } from "@/modules/ee/analysis/charts/lib/chart-types";
|
||||
import type { TApiChartType } from "@/modules/ee/analysis/types/analysis";
|
||||
|
||||
interface ManualChartBuilderProps {
|
||||
selectedChartType: string;
|
||||
onChartTypeSelect: (type: string) => void;
|
||||
selectedChartType: TApiChartType | "";
|
||||
onChartTypeSelect: (type: TApiChartType) => void;
|
||||
}
|
||||
|
||||
const AVAILABLE_CHART_TYPES = CHART_TYPES.filter((type) => !["table", "map", "scatter"].includes(type.id));
|
||||
|
||||
export function ManualChartBuilder({
|
||||
selectedChartType,
|
||||
onChartTypeSelect,
|
||||
@@ -24,7 +23,7 @@ export function ManualChartBuilder({
|
||||
|
||||
<div className="rounded-lg border border-gray-200 bg-white p-4">
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6">
|
||||
{AVAILABLE_CHART_TYPES.map((chart) => {
|
||||
{CHART_TYPES.map((chart) => {
|
||||
const isSelected = selectedChartType === chart.id;
|
||||
return (
|
||||
<button
|
||||
|
||||
@@ -40,7 +40,7 @@ export function useCreateChartDialog({
|
||||
onSuccess,
|
||||
}: Readonly<UseCreateChartDialogProps>) {
|
||||
const { t } = useTranslation();
|
||||
const [selectedChartType, setSelectedChartType] = useState<string>("");
|
||||
const [selectedChartType, setSelectedChartType] = useState<TApiChartType | "">("");
|
||||
const [chartData, setChartData] = useState<AnalyticsResponse | null>(null);
|
||||
const [isSaveDialogOpen, setIsSaveDialogOpen] = useState(false);
|
||||
const [isAddToDashboardDialogOpen, setIsAddToDashboardDialogOpen] = useState(false);
|
||||
|
||||
@@ -9,8 +9,13 @@ import {
|
||||
TableIcon,
|
||||
} from "lucide-react";
|
||||
import type React from "react";
|
||||
import type { TApiChartType } from "@/modules/ee/analysis/types/analysis";
|
||||
|
||||
export const CHART_TYPES = [
|
||||
export const CHART_TYPES: readonly {
|
||||
readonly id: TApiChartType;
|
||||
readonly name: string;
|
||||
readonly icon: React.ComponentType<{ className?: string; strokeWidth?: number }>;
|
||||
}[] = [
|
||||
{ id: "area", name: "Area Chart", icon: AreaChartIcon },
|
||||
{ id: "bar", name: "Bar Chart", icon: BarChart3Icon },
|
||||
{ id: "line", name: "Line Chart", icon: LineChartIcon },
|
||||
@@ -19,7 +24,7 @@ export const CHART_TYPES = [
|
||||
{ id: "big_number", name: "Big Number", icon: ActivityIcon },
|
||||
{ id: "scatter", name: "Scatter Plot", icon: ScatterChart },
|
||||
{ id: "map", name: "World Map", icon: MapIcon },
|
||||
] as const;
|
||||
];
|
||||
|
||||
export const CHART_TYPE_ICONS = Object.fromEntries(CHART_TYPES.map(({ id, icon }) => [id, icon])) as Record<
|
||||
string,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Query builder utility to construct Cube.js queries from chart builder state.
|
||||
*/
|
||||
import { TChartQuery, TCubeFilter, TMemberFilter, TTimeDimension } from "@formbricks/types/dashboard";
|
||||
import type { TApiChartType } from "@/modules/ee/analysis/types/analysis";
|
||||
|
||||
export interface CustomMeasure {
|
||||
id?: string;
|
||||
@@ -25,7 +26,7 @@ export interface TimeDimensionConfig {
|
||||
}
|
||||
|
||||
export interface ChartBuilderState {
|
||||
chartType: string;
|
||||
chartType: TApiChartType | "";
|
||||
selectedMeasures: string[];
|
||||
customMeasures: CustomMeasure[];
|
||||
selectedDimensions: string[];
|
||||
@@ -105,7 +106,7 @@ function isMemberFilter(f: TCubeFilter): f is TMemberFilter {
|
||||
* Parse a Cube.js query back into ChartBuilderState.
|
||||
* Preserves absent granularity / dateRange instead of injecting defaults.
|
||||
*/
|
||||
export function parseQueryToState(query: TChartQuery, chartType?: string): Partial<ChartBuilderState> {
|
||||
export function parseQueryToState(query: TChartQuery, chartType?: TApiChartType): Partial<ChartBuilderState> {
|
||||
const state: Partial<ChartBuilderState> = {
|
||||
chartType: chartType || "",
|
||||
selectedMeasures: query.measures || [],
|
||||
|
||||
@@ -97,7 +97,18 @@ export type TAddWidgetInput = z.infer<typeof ZAddWidgetInput>;
|
||||
|
||||
// ── Charts UI (query execution, AI response) ─────────────────────────────────
|
||||
|
||||
export const ZApiChartType = z.enum(["bar", "line", "donut", "kpi", "area", "pie", "big_number"]);
|
||||
export const ZApiChartType = z.enum([
|
||||
"bar",
|
||||
"line",
|
||||
"donut",
|
||||
"kpi",
|
||||
"area",
|
||||
"pie",
|
||||
"big_number",
|
||||
"table",
|
||||
"scatter",
|
||||
"map",
|
||||
]);
|
||||
export type TApiChartType = z.infer<typeof ZApiChartType>;
|
||||
|
||||
/** Row from Cube.js tablePivot - keys are measure/dimension names, values are primitives */
|
||||
|
||||
Reference in New Issue
Block a user