From 2cfbc418f95555383e7faee0df1352a89dbc64e0 Mon Sep 17 00:00:00 2001 From: AntonioMacaronio Date: Thu, 5 Jun 2025 03:31:51 -0700 Subject: [PATCH 1/3] barebones implementation, need to review and test --- src/viser/_gui_api.py | 74 +++++++ src/viser/_gui_handles.py | 42 ++++ src/viser/_messages.py | 39 ++++ src/viser/client/package.json | 1 + .../client/src/ControlPanel/Generated.tsx | 3 + src/viser/client/src/WebsocketMessages.ts | 25 +++ src/viser/client/src/components/LinePlot.tsx | 104 +++++++++ src/viser/client/yarn.lock | 208 +++++++++++++++++- 8 files changed, 493 insertions(+), 3 deletions(-) create mode 100644 src/viser/client/src/components/LinePlot.tsx diff --git a/src/viser/_gui_api.py b/src/viser/_gui_api.py index 1aeb331e8..a33730817 100644 --- a/src/viser/_gui_api.py +++ b/src/viser/_gui_api.py @@ -43,6 +43,7 @@ GuiFolderHandle, GuiHtmlHandle, GuiImageHandle, + GuiLinePlotHandle, GuiMarkdownHandle, GuiModalHandle, GuiMultiSliderHandle, @@ -784,6 +785,79 @@ def add_plotly( handle.aspect = aspect return handle + def add_mantine_lineplot( + self, + x_data: Sequence[float], + y_data: Sequence[float], + title: str | None = None, + x_label: str | None = None, + y_label: str | None = None, + series_name: str = "Series 1", + color: str | None = None, + height: int = 300, + visible: bool = True, + order: float | None = None, + ) -> GuiLinePlotHandle: + """Add a Mantine line plot to the GUI. + + Args: + x_data: X-axis data points. + y_data: Y-axis data points. + title: Optional title for the plot. + x_label: Optional label for the x-axis. + y_label: Optional label for the y-axis. + series_name: Name for the data series. + color: Optional color for the line (CSS color string). + height: Height of the plot in pixels. + visible: Whether the plot is visible. + order: Optional ordering, smallest values will be displayed first. + + Returns: + A handle that can be used to interact with the line plot. + """ + if len(x_data) != len(y_data): + raise ValueError("x_data and y_data must have the same length") + + # Create data points + data_points = tuple(_messages.GuiLinePlotDataPoint(x=float(x), y=float(y)) + for x, y in zip(x_data, y_data)) + + # Create series + series = _messages.GuiLinePlotSeries( + name=series_name, + data=data_points, + color=color + ) + + # Create message + uuid = _make_uuid() + order = _apply_default_order(order) + message = _messages.GuiLinePlotMessage( + uuid=uuid, + container_uuid=self._get_container_uuid(), + props=_messages.GuiLinePlotProps( + order=order, + title=title, + x_label=x_label, + y_label=y_label, + _series_data=(series,), + height=height, + visible=visible, + ), + ) + self._websock_interface.queue_message(message) + + return GuiLinePlotHandle( + _GuiHandleState( + uuid=message.uuid, + gui_api=self, + value=None, + props=message.props, + parent_container_id=message.container_uuid, + ), + _series_data=(series,), + ) + def add_button( self, label: str, diff --git a/src/viser/_gui_handles.py b/src/viser/_gui_handles.py index 1efad94f1..358ac39f6 100644 --- a/src/viser/_gui_handles.py +++ b/src/viser/_gui_handles.py @@ -851,3 +851,45 @@ def image(self, image: np.ndarray) -> None: ) self._data = data del media_type + + +class GuiLinePlotHandle(_GuiHandle[None], _messages.GuiLinePlotProps): + """Handle for updating and removing Mantine line plots.""" + + def __init__(self, _impl: _GuiHandleState, _series_data: tuple[_messages.GuiLinePlotSeries, ...]): + super().__init__(_impl=_impl) + self._series_data_internal = _series_data + + @property + def series_data(self) -> tuple[_messages.GuiLinePlotSeries, ...]: + """Current series data of this line plot. Synchronized automatically when assigned.""" + return self._series_data_internal + + @series_data.setter + def series_data(self, series_data: tuple[_messages.GuiLinePlotSeries, ...]) -> None: + self._series_data_internal = series_data + self._series_data = series_data + + def update_series(self, series_name: str, x_data: list[float], y_data: list[float], color: str | None = None) -> None: + """Update a single data series.""" + if len(x_data) != len(y_data): + raise ValueError("x_data and y_data must have the same length") + + # Create new data points + new_data_points = tuple(_messages.GuiLinePlotDataPoint(x=x, y=y) for x, y in zip(x_data, y_data)) + new_series = _messages.GuiLinePlotSeries(name=series_name, data=new_data_points, color=color) + + # Update or add the series + updated_series = [] + series_found = False + for series in self._series_data_internal: + if series.name == series_name: + updated_series.append(new_series) + series_found = True + else: + updated_series.append(series) + + if not series_found: + updated_series.append(new_series) + + self.series_data = tuple(updated_series) diff --git a/src/viser/_messages.py b/src/viser/_messages.py index 4164ca4e5..8b3f8c231 100644 --- a/src/viser/_messages.py +++ b/src/viser/_messages.py @@ -1022,6 +1022,45 @@ class GuiImageMessage(_CreateGuiComponentMessage): props: GuiImageProps +@dataclasses.dataclass +class GuiLinePlotDataPoint: + """Single data point for line plot.""" + x: float + y: float + + +@dataclasses.dataclass +class GuiLinePlotSeries: + """Data series for line plot.""" + name: str + data: Tuple[GuiLinePlotDataPoint, ...] + color: Optional[str] = None + + +@dataclasses.dataclass +class GuiLinePlotProps: + order: float + """Order value for arranging GUI elements. Synchronized automatically when assigned.""" + title: Optional[str] + """Title of the line plot. Synchronized automatically when assigned.""" + x_label: Optional[str] + """X-axis label. Synchronized automatically when assigned.""" + y_label: Optional[str] + """Y-axis label. Synchronized automatically when assigned.""" + _series_data: Tuple[GuiLinePlotSeries, ...] + """(Private) Plot data series. Synchronized automatically when assigned.""" + height: int + """Height of the plot in pixels. Synchronized automatically when assigned.""" + visible: bool + """Visibility state of the plot. Synchronized automatically when assigned.""" + + +@dataclasses.dataclass +class GuiLinePlotMessage(_CreateGuiComponentMessage): + container_uuid: str + props: GuiLinePlotProps + + @dataclasses.dataclass class GuiTabGroupProps: _tab_labels: Tuple[str, ...] diff --git a/src/viser/client/package.json b/src/viser/client/package.json index 935acbf96..31b2539ff 100644 --- a/src/viser/client/package.json +++ b/src/viser/client/package.json @@ -34,6 +34,7 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "react-error-boundary": "^4.0.10", + "recharts": "^2.12.7", "react-intersection-observer": "^9.13.1", "react-qr-code": "^2.0.12", "rehype-color-chips": "^0.1.3", diff --git a/src/viser/client/src/ControlPanel/Generated.tsx b/src/viser/client/src/ControlPanel/Generated.tsx index 3540ddb80..a95bd843d 100644 --- a/src/viser/client/src/ControlPanel/Generated.tsx +++ b/src/viser/client/src/ControlPanel/Generated.tsx @@ -17,6 +17,7 @@ import RgbaComponent from "../components/Rgba"; import ButtonGroupComponent from "../components/ButtonGroup"; import MarkdownComponent from "../components/Markdown"; import PlotlyComponent from "../components/PlotlyComponent"; +import LinePlotComponent from "../components/LinePlot"; import TabGroupComponent from "../components/TabGroup"; import FolderComponent from "../components/Folder"; import MultiSliderComponent from "../components/MultiSlider"; @@ -106,6 +107,8 @@ function GeneratedInput(props: { guiUuid: string }) { return ; case "GuiPlotlyMessage": return ; + case "GuiLinePlotMessage": + return ; case "GuiImageMessage": return ; case "GuiButtonMessage": diff --git a/src/viser/client/src/WebsocketMessages.ts b/src/viser/client/src/WebsocketMessages.ts index ce31874a3..76d240fb9 100644 --- a/src/viser/client/src/WebsocketMessages.ts +++ b/src/viser/client/src/WebsocketMessages.ts @@ -487,6 +487,28 @@ export interface GuiImageMessage { visible: boolean; }; } +/** GuiLinePlotMessage(uuid: 'str', container_uuid: 'str', props: 'GuiLinePlotProps') + * + * (automatically generated) + */ +export interface GuiLinePlotMessage { + type: "GuiLinePlotMessage"; + uuid: string; + container_uuid: string; + props: { + order: number; + title: string | null; + x_label: string | null; + y_label: string | null; + _series_data: { + name: string; + data: { x: number; y: number }[]; + color: string | null; + }[]; + height: number; + visible: boolean; + }; +} /** GuiTabGroupMessage(uuid: 'str', container_uuid: 'str', props: 'GuiTabGroupProps') * * (automatically generated) @@ -1290,6 +1312,7 @@ export type Message = | GuiProgressBarMessage | GuiPlotlyMessage | GuiImageMessage + | GuiLinePlotMessage | GuiTabGroupMessage | GuiButtonMessage | GuiUploadButtonMessage @@ -1376,6 +1399,7 @@ export type GuiComponentMessage = | GuiProgressBarMessage | GuiPlotlyMessage | GuiImageMessage + | GuiLinePlotMessage | GuiTabGroupMessage | GuiButtonMessage | GuiUploadButtonMessage @@ -1428,6 +1452,7 @@ const typeSetGuiComponentMessage = new Set([ "GuiProgressBarMessage", "GuiPlotlyMessage", "GuiImageMessage", + "GuiLinePlotMessage", "GuiTabGroupMessage", "GuiButtonMessage", "GuiUploadButtonMessage", diff --git a/src/viser/client/src/components/LinePlot.tsx b/src/viser/client/src/components/LinePlot.tsx new file mode 100644 index 000000000..6d72682f6 --- /dev/null +++ b/src/viser/client/src/components/LinePlot.tsx @@ -0,0 +1,104 @@ +import React from "react"; +import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts"; +import { Box, Paper, Text } from "@mantine/core"; +import { GuiLinePlotMessage } from "../WebsocketMessages"; +import { folderWrapper } from "./Folder.css"; + +interface DataPoint { + x: number; + y: number; + [key: string]: number; +} + +function processSeriesData(seriesData: GuiLinePlotMessage["props"]["_series_data"]): DataPoint[] { + if (seriesData.length === 0) return []; + + // Create a map of x values to data points + const xValueMap = new Map(); + + // Populate the map with all x values and their corresponding y values for each series + seriesData.forEach((series) => { + series.data.forEach((point) => { + if (!xValueMap.has(point.x)) { + xValueMap.set(point.x, { x: point.x, y: 0 }); + } + const dataPoint = xValueMap.get(point.x)!; + dataPoint[series.name] = point.y; + }); + }); + + // Convert map to array and sort by x value + return Array.from(xValueMap.values()).sort((a, b) => a.x - b.x); +} + +export default function LinePlotComponent({ + props: { visible, title, x_label, y_label, _series_data, height }, +}: GuiLinePlotMessage) { + if (!visible) return <>; + + const data = processSeriesData(_series_data); + + // Generate colors for series that don't have explicit colors + const getColor = (index: number, customColor?: string | null) => { + if (customColor) return customColor; + + const colors = [ + "#8884d8", "#82ca9d", "#ffc658", "#ff7c7c", "#8dd1e1", + "#d084d0", "#ffb347", "#87ceeb", "#98fb98", "#f0e68c" + ]; + return colors[index % colors.length]; + }; + + return ( + + {title && ( + + {title} + + )} + + + + + + + + {_series_data.length > 1 && ( + + )} + {_series_data.map((series, index) => ( + + ))} + + + + + ); +} \ No newline at end of file diff --git a/src/viser/client/yarn.lock b/src/viser/client/yarn.lock index 712e34b0a..56eab7848 100644 --- a/src/viser/client/yarn.lock +++ b/src/viser/client/yarn.lock @@ -963,6 +963,57 @@ dependencies: "@babel/types" "^7.20.7" +"@types/d3-array@^3.0.3": + version "3.2.1" + resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.2.1.tgz#1f6658e3d2006c4fceac53fde464166859f8b8c5" + integrity sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg== + +"@types/d3-color@*": + version "3.1.3" + resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.1.3.tgz#368c961a18de721da8200e80bf3943fb53136af2" + integrity sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A== + +"@types/d3-ease@^3.0.0": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-3.0.2.tgz#e28db1bfbfa617076f7770dd1d9a48eaa3b6c51b" + integrity sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA== + +"@types/d3-interpolate@^3.0.1": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz#412b90e84870285f2ff8a846c6eb60344f12a41c" + integrity sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA== + dependencies: + "@types/d3-color" "*" + +"@types/d3-path@*": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-3.1.1.tgz#f632b380c3aca1dba8e34aa049bcd6a4af23df8a" + integrity sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg== + +"@types/d3-scale@^4.0.2": + version "4.0.9" + resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.9.tgz#57a2f707242e6fe1de81ad7bfcccaaf606179afb" + integrity sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw== + dependencies: + "@types/d3-time" "*" + +"@types/d3-shape@^3.1.0": + version "3.1.7" + resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-3.1.7.tgz#2b7b423dc2dfe69c8c93596e673e37443348c555" + integrity sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg== + dependencies: + "@types/d3-path" "*" + +"@types/d3-time@*", "@types/d3-time@^3.0.0": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.4.tgz#8472feecd639691450dd8000eb33edd444e1323f" + integrity sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g== + +"@types/d3-timer@^3.0.0": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.2.tgz#70bbda77dc23aa727413e22e214afa3f0e852f70" + integrity sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw== + "@types/debug@^4.0.0": version "4.1.12" resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.12.tgz#a155f21690871953410df4b6b6f53187f0500917" @@ -1596,7 +1647,7 @@ character-reference-invalid@^2.0.0: resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz#85c66b041e43b47210faf401278abf808ac45cb9" integrity sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw== -clsx@^2.1.1: +clsx@^2.0.0, clsx@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== @@ -1684,11 +1735,77 @@ csstype@^3.0.2, csstype@^3.0.7: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== -d3-color@^3.1.0: +"d3-array@2 - 3", "d3-array@2.10.0 - 3", d3-array@^3.1.6: + version "3.2.4" + resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.4.tgz#15fec33b237f97ac5d7c986dc77da273a8ed0bb5" + integrity sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg== + dependencies: + internmap "1 - 2" + +"d3-color@1 - 3", d3-color@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2" integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA== +d3-ease@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4" + integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w== + +"d3-format@1 - 3": + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641" + integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA== + +"d3-interpolate@1.2.0 - 3", d3-interpolate@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d" + integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g== + dependencies: + d3-color "1 - 3" + +d3-path@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.1.0.tgz#22df939032fb5a71ae8b1800d61ddb7851c42526" + integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ== + +d3-scale@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396" + integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ== + dependencies: + d3-array "2.10.0 - 3" + d3-format "1 - 3" + d3-interpolate "1.2.0 - 3" + d3-time "2.1.1 - 3" + d3-time-format "2 - 4" + +d3-shape@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.2.0.tgz#a1a839cbd9ba45f28674c69d7f855bcf91dfc6a5" + integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA== + dependencies: + d3-path "^3.1.0" + +"d3-time-format@2 - 4": + version "4.1.0" + resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a" + integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg== + dependencies: + d3-time "1 - 3" + +"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.1.0.tgz#9310db56e992e3c0175e1ef385e545e48a9bb5c7" + integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q== + dependencies: + d3-array "2 - 3" + +d3-timer@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0" + integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA== + data-view-buffer@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.2.tgz#211a03ba95ecaf7798a8c7198d79536211f88570" @@ -1723,6 +1840,11 @@ debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3 dependencies: ms "^2.1.3" +decimal.js-light@^2.4.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934" + integrity sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg== + decode-named-character-reference@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.1.0.tgz#5d6ce68792808901210dac42a8e9853511e2b8bf" @@ -2273,6 +2395,11 @@ eval@0.1.8: "@types/node" "*" require-like ">= 0.1.1" +eventemitter3@^4.0.1: + version "4.0.7" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + extend@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" @@ -2283,6 +2410,11 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== +fast-equals@^5.0.1: + version "5.2.2" + resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-5.2.2.tgz#885d7bfb079fac0ce0e8450374bce29e9b742484" + integrity sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw== + fast-glob@^3.2.11, fast-glob@^3.2.9: version "3.3.3" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818" @@ -2687,6 +2819,11 @@ internal-slot@^1.1.0: hasown "^2.0.2" side-channel "^1.1.0" +"internmap@1 - 2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009" + integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg== + is-alphabetical@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-2.0.1.tgz#01072053ea7c1036df3c7d19a6daaec7f19e789b" @@ -3037,6 +3174,11 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== +lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + longest-streak@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-3.1.0.tgz#62fa67cd958742a1574af9f39866364102d90cd4" @@ -4053,6 +4195,11 @@ react-is@^16.13.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== +react-is@^18.3.1: + version "18.3.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" + integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== + react-number-format@^5.4.3: version "5.4.3" resolved "https://registry.yarnpkg.com/react-number-format/-/react-number-format-5.4.3.tgz#e634df907da7742faf597afab3f25f9a59689d60" @@ -4097,6 +4244,15 @@ react-remove-scroll@^2.6.2: use-callback-ref "^1.3.3" use-sidecar "^1.1.3" +react-smooth@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/react-smooth/-/react-smooth-4.0.4.tgz#a5875f8bb61963ca61b819cedc569dc2453894b4" + integrity sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q== + dependencies: + fast-equals "^5.0.1" + prop-types "^15.8.1" + react-transition-group "^4.4.5" + react-style-singleton@^2.2.2, react-style-singleton@^2.2.3: version "2.2.3" resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.3.tgz#4265608be69a4d70cfe3047f2c6c88b2c3ace388" @@ -4114,7 +4270,7 @@ react-textarea-autosize@8.5.6: use-composed-ref "^1.3.0" use-latest "^1.2.1" -react-transition-group@4.4.5: +react-transition-group@4.4.5, react-transition-group@^4.4.5: version "4.4.5" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1" integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g== @@ -4134,6 +4290,27 @@ react@^19.0.0: resolved "https://registry.yarnpkg.com/react/-/react-19.0.0.tgz#6e1969251b9f108870aa4bff37a0ce9ddfaaabdd" integrity sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ== +recharts-scale@^0.4.4: + version "0.4.5" + resolved "https://registry.yarnpkg.com/recharts-scale/-/recharts-scale-0.4.5.tgz#0969271f14e732e642fcc5bd4ab270d6e87dd1d9" + integrity sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w== + dependencies: + decimal.js-light "^2.4.1" + +recharts@^2.12.7: + version "2.15.3" + resolved "https://registry.yarnpkg.com/recharts/-/recharts-2.15.3.tgz#b94d05e91e3a5df1b02368ef64400dec9e9a77d4" + integrity sha512-EdOPzTwcFSuqtvkDoaM5ws/Km1+WTAO2eizL7rqiG0V2UVhTnz0m7J2i0CjVPUCdEkZImaWvXLbZDS2H5t6GFQ== + dependencies: + clsx "^2.0.0" + eventemitter3 "^4.0.1" + lodash "^4.17.21" + react-is "^18.3.1" + react-smooth "^4.0.4" + recharts-scale "^0.4.4" + tiny-invariant "^1.3.1" + victory-vendor "^36.6.8" + recma-build-jsx@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz#c02f29e047e103d2fab2054954e1761b8ea253c4" @@ -4675,6 +4852,11 @@ three@^0.174.0: resolved "https://registry.yarnpkg.com/three/-/three-0.174.0.tgz#53f46d6fd27515231b2af321f798f1e0ecf3f905" integrity sha512-p+WG3W6Ov74alh3geCMkGK9NWuT62ee21cV3jEnun201zodVF4tCE5aZa2U122/mkLRmhJJUQmLLW1BH00uQJQ== +tiny-invariant@^1.3.1: + version "1.3.3" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127" + integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg== + to-regex-range@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" @@ -4991,6 +5173,26 @@ vfile@^6.0.0: "@types/unist" "^3.0.0" vfile-message "^4.0.0" +victory-vendor@^36.6.8: + version "36.9.2" + resolved "https://registry.yarnpkg.com/victory-vendor/-/victory-vendor-36.9.2.tgz#668b02a448fa4ea0f788dbf4228b7e64669ff801" + integrity sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ== + dependencies: + "@types/d3-array" "^3.0.3" + "@types/d3-ease" "^3.0.0" + "@types/d3-interpolate" "^3.0.1" + "@types/d3-scale" "^4.0.2" + "@types/d3-shape" "^3.1.0" + "@types/d3-time" "^3.0.0" + "@types/d3-timer" "^3.0.0" + d3-array "^3.1.6" + d3-ease "^3.0.1" + d3-interpolate "^3.0.1" + d3-scale "^4.0.2" + d3-shape "^3.1.0" + d3-time "^3.0.0" + d3-timer "^3.0.1" + vite-node@^3.0.4: version "3.0.9" resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-3.0.9.tgz#97d0b062d3857fb8eaeb6cc6a1d400f847d4a15d" From 2df89e7e0067b87b05de70ae7166a3f7436a6b2a Mon Sep 17 00:00:00 2001 From: AntonioMacaronio Date: Fri, 6 Jun 2025 10:32:19 -0700 Subject: [PATCH 2/3] initial cleanup --- src/viser/_gui_api.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/viser/_gui_api.py b/src/viser/_gui_api.py index a33730817..6add15368 100644 --- a/src/viser/_gui_api.py +++ b/src/viser/_gui_api.py @@ -818,22 +818,19 @@ def add_mantine_lineplot( if len(x_data) != len(y_data): raise ValueError("x_data and y_data must have the same length") - # Create data points + # Create data points and series to send to the client data_points = tuple(_messages.GuiLinePlotDataPoint(x=float(x), y=float(y)) for x, y in zip(x_data, y_data)) - - # Create series series = _messages.GuiLinePlotSeries( name=series_name, data=data_points, color=color ) - # Create message - uuid = _make_uuid() + # this is very similar to Plotly order = _apply_default_order(order) message = _messages.GuiLinePlotMessage( - uuid=uuid, + uuid=_make_uuid(), container_uuid=self._get_container_uuid(), props=_messages.GuiLinePlotProps( order=order, From be35b6c0efbfef1c91520f2ffd7ab81160d7663e Mon Sep 17 00:00:00 2001 From: AntonioMacaronio Date: Fri, 6 Jun 2025 16:50:15 -0700 Subject: [PATCH 3/3] refactoed from lineplot to linechart --- src/viser/__init__.py | 1 + src/viser/_gui_api.py | 20 +++++++------- src/viser/_gui_handles.py | 16 ++++++------ src/viser/_messages.py | 26 +++++++++---------- .../client/src/ControlPanel/Generated.tsx | 6 ++--- src/viser/client/src/WebsocketMessages.ts | 12 ++++----- .../{LinePlot.tsx => LineChart.tsx} | 8 +++--- 7 files changed, 45 insertions(+), 44 deletions(-) rename src/viser/client/src/components/{LinePlot.tsx => LineChart.tsx} (93%) diff --git a/src/viser/__init__.py b/src/viser/__init__.py index b375a9ebd..419217c1c 100644 --- a/src/viser/__init__.py +++ b/src/viser/__init__.py @@ -8,6 +8,7 @@ from ._gui_handles import GuiHtmlHandle as GuiHtmlHandle from ._gui_handles import GuiImageHandle as GuiImageHandle from ._gui_handles import GuiInputHandle as GuiInputHandle +from ._gui_handles import GuiLineChartHandle as GuiLineChartHandle from ._gui_handles import GuiMarkdownHandle as GuiMarkdownHandle from ._gui_handles import GuiMultiSliderHandle as GuiMultiSliderHandle from ._gui_handles import GuiNumberHandle as GuiNumberHandle diff --git a/src/viser/_gui_api.py b/src/viser/_gui_api.py index 6add15368..b2734b800 100644 --- a/src/viser/_gui_api.py +++ b/src/viser/_gui_api.py @@ -43,7 +43,7 @@ GuiFolderHandle, GuiHtmlHandle, GuiImageHandle, - GuiLinePlotHandle, + GuiLineChartHandle, GuiMarkdownHandle, GuiModalHandle, GuiMultiSliderHandle, @@ -785,7 +785,7 @@ def add_plotly( handle.aspect = aspect return handle - def add_mantine_lineplot( + def add_mantine_linechart( self, x_data: Sequence[float], y_data: Sequence[float], @@ -797,8 +797,8 @@ def add_mantine_lineplot( height: int = 300, visible: bool = True, order: float | None = None, - ) -> GuiLinePlotHandle: - """Add a Mantine line plot to the GUI. + ) -> GuiLineChartHandle: + """Add a Mantine line chart to the GUI. Args: x_data: X-axis data points. @@ -813,15 +813,15 @@ def add_mantine_lineplot( order: Optional ordering, smallest values will be displayed first. Returns: - A handle that can be used to interact with the line plot. + A handle that can be used to interact with the line chart. """ if len(x_data) != len(y_data): raise ValueError("x_data and y_data must have the same length") # Create data points and series to send to the client - data_points = tuple(_messages.GuiLinePlotDataPoint(x=float(x), y=float(y)) + data_points = tuple(_messages.GuiLineChartDataPoint(x=float(x), y=float(y)) for x, y in zip(x_data, y_data)) - series = _messages.GuiLinePlotSeries( + series = _messages.GuiLineChartSeries( name=series_name, data=data_points, color=color @@ -829,10 +829,10 @@ def add_mantine_lineplot( # this is very similar to Plotly order = _apply_default_order(order) - message = _messages.GuiLinePlotMessage( + message = _messages.GuiLineChartMessage( uuid=_make_uuid(), container_uuid=self._get_container_uuid(), - props=_messages.GuiLinePlotProps( + props=_messages.GuiLineChartProps( order=order, title=title, x_label=x_label, @@ -844,7 +844,7 @@ def add_mantine_lineplot( ) self._websock_interface.queue_message(message) - return GuiLinePlotHandle( + return GuiLineChartHandle( _GuiHandleState( uuid=message.uuid, gui_api=self, diff --git a/src/viser/_gui_handles.py b/src/viser/_gui_handles.py index 358ac39f6..e0ab56139 100644 --- a/src/viser/_gui_handles.py +++ b/src/viser/_gui_handles.py @@ -853,20 +853,20 @@ def image(self, image: np.ndarray) -> None: del media_type -class GuiLinePlotHandle(_GuiHandle[None], _messages.GuiLinePlotProps): - """Handle for updating and removing Mantine line plots.""" +class GuiLineChartHandle(_GuiHandle[None], _messages.GuiLineChartProps): + """Handle for updating and removing Mantine line charts.""" - def __init__(self, _impl: _GuiHandleState, _series_data: tuple[_messages.GuiLinePlotSeries, ...]): + def __init__(self, _impl: _GuiHandleState, _series_data: tuple[_messages.GuiLineChartSeries, ...]): super().__init__(_impl=_impl) self._series_data_internal = _series_data @property - def series_data(self) -> tuple[_messages.GuiLinePlotSeries, ...]: - """Current series data of this line plot. Synchronized automatically when assigned.""" + def series_data(self) -> tuple[_messages.GuiLineChartSeries, ...]: + """Current series data of this line chart. Synchronized automatically when assigned.""" return self._series_data_internal @series_data.setter - def series_data(self, series_data: tuple[_messages.GuiLinePlotSeries, ...]) -> None: + def series_data(self, series_data: tuple[_messages.GuiLineChartSeries, ...]) -> None: self._series_data_internal = series_data self._series_data = series_data @@ -876,8 +876,8 @@ def update_series(self, series_name: str, x_data: list[float], y_data: list[floa raise ValueError("x_data and y_data must have the same length") # Create new data points - new_data_points = tuple(_messages.GuiLinePlotDataPoint(x=x, y=y) for x, y in zip(x_data, y_data)) - new_series = _messages.GuiLinePlotSeries(name=series_name, data=new_data_points, color=color) + new_data_points = tuple(_messages.GuiLineChartDataPoint(x=x, y=y) for x, y in zip(x_data, y_data)) + new_series = _messages.GuiLineChartSeries(name=series_name, data=new_data_points, color=color) # Update or add the series updated_series = [] diff --git a/src/viser/_messages.py b/src/viser/_messages.py index 8b3f8c231..22b9f62d3 100644 --- a/src/viser/_messages.py +++ b/src/viser/_messages.py @@ -1023,42 +1023,42 @@ class GuiImageMessage(_CreateGuiComponentMessage): @dataclasses.dataclass -class GuiLinePlotDataPoint: - """Single data point for line plot.""" +class GuiLineChartDataPoint: + """Single data point for line chart.""" x: float y: float @dataclasses.dataclass -class GuiLinePlotSeries: - """Data series for line plot.""" +class GuiLineChartSeries: + """Data series for line chart.""" name: str - data: Tuple[GuiLinePlotDataPoint, ...] + data: Tuple[GuiLineChartDataPoint, ...] color: Optional[str] = None @dataclasses.dataclass -class GuiLinePlotProps: +class GuiLineChartProps: order: float """Order value for arranging GUI elements. Synchronized automatically when assigned.""" title: Optional[str] - """Title of the line plot. Synchronized automatically when assigned.""" + """Title of the line chart. Synchronized automatically when assigned.""" x_label: Optional[str] """X-axis label. Synchronized automatically when assigned.""" y_label: Optional[str] """Y-axis label. Synchronized automatically when assigned.""" - _series_data: Tuple[GuiLinePlotSeries, ...] - """(Private) Plot data series. Synchronized automatically when assigned.""" + _series_data: Tuple[GuiLineChartSeries, ...] + """(Private) Chart data series. Synchronized automatically when assigned.""" height: int - """Height of the plot in pixels. Synchronized automatically when assigned.""" + """Height of the chart in pixels. Synchronized automatically when assigned.""" visible: bool - """Visibility state of the plot. Synchronized automatically when assigned.""" + """Visibility state of the chart. Synchronized automatically when assigned.""" @dataclasses.dataclass -class GuiLinePlotMessage(_CreateGuiComponentMessage): +class GuiLineChartMessage(_CreateGuiComponentMessage): container_uuid: str - props: GuiLinePlotProps + props: GuiLineChartProps @dataclasses.dataclass diff --git a/src/viser/client/src/ControlPanel/Generated.tsx b/src/viser/client/src/ControlPanel/Generated.tsx index a95bd843d..cc3c16b1f 100644 --- a/src/viser/client/src/ControlPanel/Generated.tsx +++ b/src/viser/client/src/ControlPanel/Generated.tsx @@ -17,7 +17,7 @@ import RgbaComponent from "../components/Rgba"; import ButtonGroupComponent from "../components/ButtonGroup"; import MarkdownComponent from "../components/Markdown"; import PlotlyComponent from "../components/PlotlyComponent"; -import LinePlotComponent from "../components/LinePlot"; +import LineChartComponent from "../components/LineChart"; import TabGroupComponent from "../components/TabGroup"; import FolderComponent from "../components/Folder"; import MultiSliderComponent from "../components/MultiSlider"; @@ -107,8 +107,8 @@ function GeneratedInput(props: { guiUuid: string }) { return ; case "GuiPlotlyMessage": return ; - case "GuiLinePlotMessage": - return ; + case "GuiLineChartMessage": + return ; case "GuiImageMessage": return ; case "GuiButtonMessage": diff --git a/src/viser/client/src/WebsocketMessages.ts b/src/viser/client/src/WebsocketMessages.ts index 76d240fb9..6baea9e42 100644 --- a/src/viser/client/src/WebsocketMessages.ts +++ b/src/viser/client/src/WebsocketMessages.ts @@ -487,12 +487,12 @@ export interface GuiImageMessage { visible: boolean; }; } -/** GuiLinePlotMessage(uuid: 'str', container_uuid: 'str', props: 'GuiLinePlotProps') +/** GuiLineChartMessage(uuid: 'str', container_uuid: 'str', props: 'GuiLineChartProps') * * (automatically generated) */ -export interface GuiLinePlotMessage { - type: "GuiLinePlotMessage"; +export interface GuiLineChartMessage { + type: "GuiLineChartMessage"; uuid: string; container_uuid: string; props: { @@ -1312,7 +1312,7 @@ export type Message = | GuiProgressBarMessage | GuiPlotlyMessage | GuiImageMessage - | GuiLinePlotMessage + | GuiLineChartMessage | GuiTabGroupMessage | GuiButtonMessage | GuiUploadButtonMessage @@ -1399,7 +1399,7 @@ export type GuiComponentMessage = | GuiProgressBarMessage | GuiPlotlyMessage | GuiImageMessage - | GuiLinePlotMessage + | GuiLineChartMessage | GuiTabGroupMessage | GuiButtonMessage | GuiUploadButtonMessage @@ -1452,7 +1452,7 @@ const typeSetGuiComponentMessage = new Set([ "GuiProgressBarMessage", "GuiPlotlyMessage", "GuiImageMessage", - "GuiLinePlotMessage", + "GuiLineChartMessage", "GuiTabGroupMessage", "GuiButtonMessage", "GuiUploadButtonMessage", diff --git a/src/viser/client/src/components/LinePlot.tsx b/src/viser/client/src/components/LineChart.tsx similarity index 93% rename from src/viser/client/src/components/LinePlot.tsx rename to src/viser/client/src/components/LineChart.tsx index 6d72682f6..91126837a 100644 --- a/src/viser/client/src/components/LinePlot.tsx +++ b/src/viser/client/src/components/LineChart.tsx @@ -1,7 +1,7 @@ import React from "react"; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts"; import { Box, Paper, Text } from "@mantine/core"; -import { GuiLinePlotMessage } from "../WebsocketMessages"; +import { GuiLineChartMessage } from "../WebsocketMessages"; import { folderWrapper } from "./Folder.css"; interface DataPoint { @@ -10,7 +10,7 @@ interface DataPoint { [key: string]: number; } -function processSeriesData(seriesData: GuiLinePlotMessage["props"]["_series_data"]): DataPoint[] { +function processSeriesData(seriesData: GuiLineChartMessage["props"]["_series_data"]): DataPoint[] { if (seriesData.length === 0) return []; // Create a map of x values to data points @@ -31,9 +31,9 @@ function processSeriesData(seriesData: GuiLinePlotMessage["props"]["_series_data return Array.from(xValueMap.values()).sort((a, b) => a.x - b.x); } -export default function LinePlotComponent({ +export default function LineChartComponent({ props: { visible, title, x_label, y_label, _series_data, height }, -}: GuiLinePlotMessage) { +}: GuiLineChartMessage) { if (!visible) return <>; const data = processSeriesData(_series_data);