Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion frontend/frontend-kit/ui/src/icons/math.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { Plus, X } from "lucide-react";
export { Plus } from "lucide-react";
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { X } from "@workspace/ui/icons";
import { Eye, EyeOff, X } from "@workspace/ui/icons";
import { COLORS } from "../constants/chartsColors";
import type { WorkspaceChartSeries } from "../types/charts";
import { ChartSettings } from "./ChartSettings";
Expand All @@ -7,15 +7,21 @@ interface ChartLegendProps {
chartId: string;
series: WorkspaceChartSeries[];
disabledVariables: Set<string>;
visibleValueLabels: Set<string>;
valueLabelRefs: { current: Map<string, HTMLElement> };
onToggle: (seriesKey: string) => void;
onToggleValueLabel: (seriesKey: string) => void;
onRemove: (variable: string) => void;
}

export const ChartLegend = ({
chartId,
series,
disabledVariables,
visibleValueLabels,
valueLabelRefs,
onToggle,
onToggleValueLabel,
onRemove,
}: ChartLegendProps) => (
<div className="border-border mb-4 flex flex-wrap gap-2 border-b pb-3 pr-14">
Expand All @@ -37,6 +43,34 @@ export const ChartLegend = ({
style={{ background: COLORS[i % COLORS.length] }}
/>
{p.variable}
{visibleValueLabels.has(p.variable) && (
<span
ref={(el) => {
if (el) valueLabelRefs.current.set(p.variable, el);
else valueLabelRefs.current.delete(p.variable);
}}
className="text-muted-foreground text-[11px] tabular-nums normal-case"
/>
)}
</button>
<button
title={
visibleValueLabels.has(p.variable)
? `Hide value label for ${p.variable}`
: `Show value label for ${p.variable}`
}
onClick={() => onToggleValueLabel(p.variable)}
className={`border-border h-full border-l px-1.5 py-1 transition-colors ${
visibleValueLabels.has(p.variable)
? "text-foreground hover:bg-accent"
: "text-muted-foreground/40 hover:text-muted-foreground"
}`}
>
{visibleValueLabels.has(p.variable) ? (
<Eye className="h-3 w-3" />
) : (
<EyeOff className="h-3 w-3" />
)}
</button>
<button
title={`Remove variable ${p.variable}`}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,33 @@ import { useShallow } from "zustand/shallow";
import { config } from "../../../../config";
import { useStore } from "../../../store/store";
import { COLORS } from "../constants/chartsColors";
import { createTooltipPlugin } from "../plugins/tooltipPlugin";
import { createValueLabelsPlugin } from "../plugins/valueLabelsPlugin";
import type { WorkspaceChartSeries } from "../types/charts";
import { createTooltipPlugin } from "./tooltipPlugin";

interface ChartSurfaceProps {
chartId: string;
series: WorkspaceChartSeries[];
disabledVariables: Set<string>;
visibleValueLabels: Set<string>;
valueLabelRefs: { current: Map<string, HTMLElement> };
}

// IMPORTANT: This component was almost completely vibe-coded
// It could provoke bugs, thus it could be improved

export const ChartSurface = memo(
({ chartId, series, disabledVariables }: ChartSurfaceProps) => {
({
chartId,
series,
disabledVariables,
visibleValueLabels,
valueLabelRefs,
}: ChartSurfaceProps) => {
const containerRef = useRef<HTMLDivElement>(null);
const uplotRef = useRef<uPlot | null>(null);
const historyRef = useRef<any[]>([]);
const visibleValueLabelsRef = useRef(visibleValueLabels);

const [isZooming, setIsZooming] = useState(false);

Expand Down Expand Up @@ -80,11 +90,21 @@ export const ChartSurface = memo(
}
}, [disabledVariables, series]);

useEffect(() => {
visibleValueLabelsRef.current = visibleValueLabels;
uplotRef.current?.redraw();
}, [visibleValueLabels]);

const handleDoubleClick = () => {
setIsZooming(false);
};

const tooltipPlugin = createTooltipPlugin(series);
const valueLabelsPlugin = createValueLabelsPlugin(
series,
visibleValueLabelsRef,
valueLabelRefs,
);

// Initialize Chart
useEffect(() => {
Expand All @@ -104,7 +124,7 @@ export const ChartSurface = memo(
legend: {
show: false,
},
plugins: [tooltipPlugin],
plugins: [tooltipPlugin, valueLabelsPlugin],
padding: [20, 10, 5, 15],
scales: {
x: { time: false },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { DraggableAttributes } from "@dnd-kit/core";
import type { SyntheticListenerMap } from "@dnd-kit/core/dist/hooks/utilities";
import { GripVertical, Trash2 } from "@workspace/ui/icons";
import { cn } from "@workspace/ui/lib/utils";
import { useState } from "react";
import { useRef, useState } from "react";
import "uplot/dist/uPlot.min.css";
import { useStore } from "../../../store/store";
import type { WorkspaceChartSeries } from "../types/charts";
Expand Down Expand Up @@ -46,6 +46,10 @@ export const TelemetryChart = ({
const [disabledVariables, setDisabledVariables] = useState<Set<string>>(
new Set(),
);
const [visibleValueLabels, setVisibleValueLabels] = useState<Set<string>>(
new Set(),
);
const valueLabelRefs = useRef<Map<string, HTMLElement>>(new Map());

const toggleSeries = (variable: string) => {
setDisabledVariables((prev) => {
Expand All @@ -56,6 +60,15 @@ export const TelemetryChart = ({
});
};

const toggleValueLabel = (variable: string) => {
setVisibleValueLabels((prev) => {
const next = new Set(prev);
if (next.has(variable)) next.delete(variable);
else next.add(variable);
return next;
});
};

const handleRemoveSeries = (variable: string) => {
if (!activeWorkspaceId) return;
removeSeries(activeWorkspaceId, id, variable);
Expand Down Expand Up @@ -117,14 +130,19 @@ export const TelemetryChart = ({
chartId={id}
series={series}
disabledVariables={disabledVariables}
visibleValueLabels={visibleValueLabels}
valueLabelRefs={valueLabelRefs}
onToggle={toggleSeries}
onToggleValueLabel={toggleValueLabel}
onRemove={handleRemoveSeries}
/>

<ChartSurface
chartId={id}
series={series}
disabledVariables={disabledVariables}
visibleValueLabels={visibleValueLabels}
valueLabelRefs={valueLabelRefs}
/>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { WorkspaceChartSeries } from "../types/charts";

/**
* Writes each series' latest value into the corresponding legend element
* (registered by `ChartLegend` in `valueLabelRefs`), instead of touching
* React state on every draw.
*
* `visibleLabelsRef` is read on every draw, so toggling which series show a
* value in the legend doesn't require recreating the uPlot instance. Value
* labels are opt-in, so a series only renders one once it's in the set.
*/
export const createValueLabelsPlugin = (
series: WorkspaceChartSeries[],
visibleLabelsRef: { current: Set<string> },
valueLabelRefs: { current: Map<string, HTMLElement> },
) => ({
hooks: {
draw: (u: uPlot) => {
series.forEach((p, i) => {
const el = valueLabelRefs.current.get(p.variable);
if (!el) return;

const seriesIdx = i + 1;
const data = u.data[seriesIdx];

if (
!u.series[seriesIdx]?.show ||
!visibleLabelsRef.current.has(p.variable) ||
!data?.length
) {
el.textContent = "";
return;
}

let rawVal: number | null | undefined;
for (let j = data.length - 1; j >= 0; j--) {
if (data[j] != null) {
rawVal = data[j];
break;
}
}

el.textContent =
rawVal == null
? ""
: p.enumOptions?.length
? (p.enumOptions[Math.round(rawVal)] ?? String(rawVal))
: rawVal.toFixed(2);
});
},
},
});
Loading