/* Copyright 2026 Marimo. All rights reserved. */
/* oxlint-disable typescript/no-base-to-string */
import * as cql from "compassql/build/src";
import { createStore, Provider, useAtomValue } from "jotai";
import { ListFilterIcon } from "lucide-react";
import React, { type JSX, useMemo } from "react";
import { VegaEmbed, type VegaEmbedProps } from "react-vega";
import { tooltipHandler } from "@/components/charts/tooltip";
import { augmentSpecWithData } from "@/components/data-table/charts/chart-spec/spec";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Tooltip } from "@/components/ui/tooltip";
import { useAsyncData } from "@/hooks/useAsyncData";
import { useOnMount } from "@/hooks/useLifecycle";
import { type ResolvedTheme, useTheme } from "@/theme/useTheme";
import { cn } from "@/utils/cn";
import { Objects } from "@/utils/objects";
import { ErrorBanner } from "../common/error-banner";
import { vegaLoadData } from "../vega/loader";
import { getContainerWidth } from "../vega/utils";
import { ColumnSummary } from "./components/column-summary";
import { QueryForm } from "./components/query-form";
import type { SpecificEncoding } from "./encoding";
import {
chartSpecAtom,
relatedChartSpecsAtom,
useChartSpecActions,
} from "./state/reducer";
import type { ChartSpec } from "./state/types";
/**
* @param label - a label of the table
* @param data - the data to display
*/
export interface DataExplorerData {
label?: string | null;
data: string;
}
// Value is unused for now
export type DataExplorerState = ChartSpec | undefined;
const ConnectedDataExplorerComponent = (props: DataTableProps): JSX.Element => {
const store = useMemo(() => createStore(), []);
useOnMount(() => {
// Subscribe to the store
const unsub = store.sub(chartSpecAtom, () => {
const value = store.get(chartSpecAtom);
const { schema, ...withoutSchema } = value;
props.setValue(withoutSchema);
});
// Set the initial value
const value = props.value;
if (value && Object.keys(value).length > 0) {
store.set(chartSpecAtom, value);
}
return unsub;
});
return (
);
};
interface DataTableProps extends DataExplorerData {
value: DataExplorerState;
setValue: (value: DataExplorerState) => void;
}
function chartOptions(theme: ResolvedTheme): VegaEmbedProps["options"] {
return {
padding: { left: 20, right: 20, top: 20, bottom: 20 },
actions: {
export: { svg: true, png: true },
source: false,
compiled: false,
editor: false,
},
theme: theme === "dark" ? "dark" : undefined,
tooltip: tooltipHandler.call,
renderer: "canvas",
};
}
export default ConnectedDataExplorerComponent;
export const DataExplorerComponent = ({
data: dataUrl,
}: DataTableProps): JSX.Element => {
const actions = useChartSpecActions();
const { data, isPending, error } = useAsyncData(async () => {
if (!dataUrl) {
return {};
}
const chartData = await vegaLoadData(
dataUrl,
{
type: "csv",
parse: "auto",
},
{
replacePeriod: true,
},
);
const schema = cql.schema.build(chartData);
actions.setSchema(schema);
return { chartData, schema };
}, [dataUrl]);
const { mark } = useAtomValue(chartSpecAtom);
const charts = useAtomValue(relatedChartSpecsAtom);
const { theme } = useTheme();
if (error) {
return ;
}
if (!data) {
return
;
}
const { chartData, schema } = data;
if (isPending || !schema) {
return
;
}
const mainPlot = charts.main?.plots?.[0];
const existingEncodingNames = new Set(
mainPlot?.fieldInfos.map((info) => info.fieldDef.field),
);
const renderMainPlot = () => {
if (!mainPlot) {
return ;
}
const spec = mainPlot.spec;
const responsiveSpec = makeResponsive(spec);
// TODO: We can optimize by updating the data dynamically. https://github.com/vega/react-vega?tab=readme-ov-file#recipes
const augmentedSpec = augmentSpecWithData(responsiveSpec, chartData);
return (
);
};
return (
{renderMainPlot()}
{[
charts.histograms?.plots,
charts.addCategoricalField?.plots,
charts.addQuantitativeField?.plots,
charts.addTemporalField?.plots,
]
.filter(Boolean)
.flat()
.map((plot, idx) => (
{plot.fieldInfos.map((info) => {
const label =
info.fieldDef.field === "*"
? "Count"
: info.fieldDef.fn
? `${info.fieldDef.fn}(${info.fieldDef.field})`
: info.fieldDef.field.toString();
return (
{label}
);
})}
}
actions={
{
const encoding: SpecificEncoding = Objects.fromEntries(
plot.fieldInfos.map((info) => [
info.channel,
info.fieldDef,
]),
);
actions.setEncoding(encoding);
}}
>
}
>
))}
);
};
const HorizontalCarousel = ({
children,
}: {
children: React.ReactNode;
}): React.ReactNode => {
if (React.Children.count(children) === 0) {
return null;
}
return (
{children}
);
};
const HorizontalCarouselItem = ({
title,
children,
actions,
}: {
title?: React.ReactNode;
actions?: React.ReactNode;
children: React.ReactNode;
}): React.ReactNode => {
return (
);
};
// Make the plot responsive
// oxlint-disable-next-line typescript/no-explicit-any
function makeResponsive(spec: any) {
// NOTE: for row/column, this applies to the inner plot
// so we tend to overflow due to the legends,
// So for row/column, we skip the responsive spec
// https://vega.github.io/vega-lite/docs/size.html#width-and-height-of-multi-view-displays
const hasRowOrColumn = Boolean(spec.encoding?.row || spec.encoding?.column);
if (!hasRowOrColumn) {
spec.width = "container";
}
return spec;
}