Skip to content
Merged
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
22 changes: 22 additions & 0 deletions src/types/markers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Color, ColorUtils } from "./color";

export interface Marker {
color: Color;
pvName: string;
interactive: boolean;
visible: boolean;
}

export type Markers = Marker[];

export const newMarker = (props: {
color?: Color;
pvName?: string;
interactive?: boolean;
visible?: boolean;
}): Marker => ({
color: props.color ?? ColorUtils.fromRgba(0, 0, 255),
pvName: props.pvName ?? "",
interactive: props.interactive ?? false,
visible: props.visible ?? true
});
2 changes: 2 additions & 0 deletions src/types/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
ResponsiveLayout
} from "./responsiveBreakpoints";
import { Rois } from "./rois";
import { Markers } from "./markers";

export type GenericProp =
| string
Expand All @@ -35,6 +36,7 @@ export type GenericProp =
| WidgetActions
| OpiFile
| Trace[]
| Markers
| Axes
| Axis
| Points
Expand Down
24 changes: 24 additions & 0 deletions src/ui/widgets/EmbeddedDisplay/bobParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import {
bobParseResponsiveMargins
} from "./BobParsers/responsiveLayoutBobParser";
import { newRoi, Rois } from "../../../types/rois";
import { Markers, newMarker } from "../../../types/markers";

const BOB_WIDGET_MAPPING: { [key: string]: any } = {
action_button: "actionbutton",
Expand Down Expand Up @@ -331,6 +332,27 @@ function bobParseTraces(props: any): Trace[] {
return traces;
}

/**
* Parses props from an array of markers
* @param props list of props for this element
* @returns a array of marker objects
*/
function bobParseMarker(props: any): Markers {
if (props?.marker == null) {
return [] as Markers;
}

let markers = props.marker;
if (!Array.isArray(markers)) {
markers = [markers];
}

return markers.map((m: any) => {
const parsedProps = parseChildProps(m, BOB_SIMPLE_PARSERS);
return newMarker(parsedProps);
});
}

/**
* Parses props from an array of Y axes
* @param props
Expand Down Expand Up @@ -623,6 +645,7 @@ export const BOB_SIMPLE_PARSERS: ParserDict = {
displayHorizontal: ["displayHorizontal", opiParseBoolean],
xPv: ["xPv", opiParseString],
yPv: ["yPv", opiParseString],
pvName: ["pv_name", opiParseString],
heightPv: ["heightPv", opiParseString],
widthPv: ["widthPv", opiParseString],
axis: ["axis", bobParseNumber],
Expand Down Expand Up @@ -721,6 +744,7 @@ export async function parseBob(
scripts: (scripts: ElementCompact): Script[] =>
scriptParser(scripts, defaultProtocol, false),
traces: (props: ElementCompact) => bobParseTraces(props["traces"]),
marker: (props: ElementCompact) => bobParseMarker(props["marker"]),
axes: (props: ElementCompact) => bobParseYAxes(props["y_axes"]),
regionsOfInterest: (props: ElementCompact) => bobParseRois(props["rois"]),
plt: async (props: ElementCompact) =>
Expand Down
7 changes: 7 additions & 0 deletions src/ui/widgets/EmbeddedDisplay/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,13 @@ export async function genericParser(
);
}

if (newProps.hasOwnProperty("marker")) {
const markerPVNames = newProps.marker?.map((marker: any) => ({
pvName: PVUtils.parse(marker.pvName)
}));
newProps.pvMetadataList = [...newProps?.pvMetadataList, ...markerPVNames];
}

// attach an id if it does not exist
if (!newProps?.id) {
newProps["id"] = `${newProps.type}_${crypto.randomUUID()}`;
Expand Down
126 changes: 119 additions & 7 deletions src/ui/widgets/XYPlot/xyPlot.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ vi.mock("@mui/x-charts", () => ({
MarkPlot: () => <div data-testid="mark-plot" />,
ChartsXAxis: () => <div data-testid="x-axis" />,
ChartsYAxis: () => <div data-testid="y-axis" />,
ChartsLegend: () => <div data-testid="legend" />
ChartsLegend: () => <div data-testid="legend" />,
ChartsReferenceLine: (props: any) => (
<div data-testid="reference-line" data-x={props.x} />
)
}));

vi.mock("../../hooks/useStyle", () => ({
Expand All @@ -37,7 +40,8 @@ vi.mock("./xyPlot.utilities", () => ({
buildPlotDataSet: vi.fn(),
buildSeries: vi.fn(),
buildXAxes: vi.fn(),
buildYAxes: vi.fn()
buildYAxes: vi.fn(),
buildMarkerDataSet: vi.fn()
}));

const mockStyle = {
Expand All @@ -46,6 +50,7 @@ const mockStyle = {

const baseProps: any = {
traces: [],
marker: [],
axes: [],
pvData: [],
title: "Test Title",
Expand Down Expand Up @@ -111,22 +116,63 @@ describe("XYPlotComponent", () => {
baseProps.pvData,
baseProps.visible
);
expect(utils.buildPlotDataSet).toHaveBeenCalledWith(baseProps.pvData);

expect(utils.buildPlotDataSet).toHaveBeenCalledWith(
baseProps.pvData,
baseProps.traces
);

expect(utils.buildMarkerDataSet).toHaveBeenCalledWith(
baseProps.pvData,
baseProps.marker
);
});

it("adds x index when no x-axis data", () => {
(utils.buildXAxes as any).mockReturnValue({
xAxis: [],
xAxis: [{ id: "0", dataKey: "x" }],
hasXAxisData: false
});

(utils.buildPlotDataSet as any).mockReturnValue([{ y: 10 }, { y: 20 }]);
const mockDataset = [{ y: 10 }, { y: 20 }];
(utils.buildPlotDataSet as any).mockReturnValue(mockDataset);

render(<XYPlotComponent {...baseProps} />);

// Verify the component rendered (dataset was transformed)
expect(screen.getByTestId("charts-provider")).toBeInTheDocument();
});

it("does not render X-axis when xAxis.visible is false", () => {
const propsWithHiddenXAxis = {
...baseProps,
xAxis: { visible: false }
};

render(<XYPlotComponent {...propsWithHiddenXAxis} />);

expect(screen.queryByTestId("x-axis")).not.toBeInTheDocument();
});

it("renders X-axis by default", () => {
render(<XYPlotComponent {...baseProps} />);

const call = (utils.buildPlotDataSet as any).mock.results[0].value;
expect(screen.getByTestId("x-axis")).toBeInTheDocument();
});

it("renders only visible Y-axes", () => {
(utils.buildYAxes as any).mockReturnValue({
yAxes: [
{ id: "0", visible: true },
{ id: "1", visible: false }
],
yAxesStyle: {}
});

render(<XYPlotComponent {...baseProps} />);

expect(call).toBeDefined();
const yAxes = screen.getAllByTestId("y-axis");
expect(yAxes).toHaveLength(1);
});

it("renders legend when enabled", () => {
Expand All @@ -151,6 +197,53 @@ describe("XYPlotComponent", () => {
);
});

it("renders markers when marker data exists", () => {
const mockMarkers = [
{
pvName: "marker1",
pvValue: 5,
visible: true,
color: { colorString: "red" }
}
];

(utils.buildMarkerDataSet as any).mockReturnValue(mockMarkers);

render(<XYPlotComponent {...baseProps} marker={mockMarkers} />);

expect(screen.getByTestId("charts-surface")).toBeInTheDocument();
});

it("only renders visible markers with pvValue", () => {
const mockMarkers = [
{
pvName: "m1",
pvValue: 5,
visible: true,
color: { colorString: "red" }
},
{
pvName: "m2",
pvValue: null,
visible: true,
color: { colorString: "blue" }
},
{
pvName: "m3",
pvValue: 10,
visible: false,
color: { colorString: "green" }
}
];

(utils.buildMarkerDataSet as any).mockReturnValue(mockMarkers);

render(<XYPlotComponent {...baseProps} />);

const markers = screen.getAllByTestId("reference-line");
expect(markers).toHaveLength(1);
});

it("passes slotProps logic to LinePlot", () => {
const traces = [{ traceType: 0 }, { traceType: 1 }];

Expand All @@ -173,4 +266,23 @@ describe("XYPlotComponent", () => {
expect(lineFn({ seriesId: "1" })).toEqual({});
expect(lineFn({ seriesId: "2" })).toEqual({ stroke: "transparent" });
});

it("handles undefined traces gracefully", () => {
const propsWithUndefinedTraces = {
...baseProps,
traces: undefined
};

expect(() =>
render(<XYPlotComponent {...propsWithUndefinedTraces} />)
).not.toThrow();
});

it("handles empty pvData", () => {
(utils.buildPlotDataSet as any).mockReturnValue([]);

render(<XYPlotComponent {...baseProps} pvData={[]} />);

expect(screen.queryByTestId("charts-provider")).not.toBeInTheDocument();
});
});
33 changes: 29 additions & 4 deletions src/ui/widgets/XYPlot/xyPlot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import {
ArchivedDataPropOpt,
IntPropOpt,
TracesPropOpt,
AxisProp
AxisProp,
MarkersPropOpt
} from "../propTypes";
import { registerWidget } from "../register";
import { Box, Typography } from "@mui/material";
Expand All @@ -26,26 +27,30 @@ import {
ChartsTooltip,
ChartsAxisHighlight,
ChartsSurface,
ChartsDataProvider
ChartsDataProvider,
ChartsReferenceLine
} from "@mui/x-charts";
import { Axes, Axis } from "../../../types/axis";
import { fontToCss, newFont } from "../../../types/font";
import { useStyle } from "../../hooks/useStyle";
import { DatasetElementType } from "@mui/x-charts/internals";
import {
buildMarkerDataSet,
buildPlotDataSet,
buildSeries,
buildXAxes,
buildYAxes
} from "./xyPlot.utilities";
import { Trace } from "../../../types/trace";
import { Marker } from "../../../types/markers";

const widgetName = "xyplot";

const traceTypesWithoutLines = [0, 3];

const XYPlotProps = {
traces: TracesPropOpt,
marker: MarkersPropOpt,
axes: AxesProp,
xAxis: AxisProp,
start: StringPropOpt,
Expand Down Expand Up @@ -80,6 +85,7 @@ export const XYPlotComponent = (props: XYPlotComponentProps): JSX.Element => {

const {
traces,
marker,
axes,
xAxis,
pvData,
Expand All @@ -103,8 +109,13 @@ export const XYPlotComponent = (props: XYPlotComponentProps): JSX.Element => {
);

let plotDataSet: DatasetElementType<number>[] = useMemo(
() => buildPlotDataSet(pvData),
[pvData]
() => buildPlotDataSet(pvData, traces as Trace[]),
[pvData, traces]
);

const markerDataSet = useMemo(
() => buildMarkerDataSet(pvData, marker as Marker[]),
[pvData, marker]
);

if (!hasXAxisData) {
Expand Down Expand Up @@ -203,6 +214,20 @@ export const XYPlotComponent = (props: XYPlotComponentProps): JSX.Element => {
<ChartsYAxis key={axis.id} axisId={axis.id} />
) : null
)}

{markerDataSet
?.filter(m => m?.pvValue)
?.map(marker =>
marker.visible !== false ? (
<ChartsReferenceLine
key={marker.pvName}
x={marker.pvValue as number}
lineStyle={{
stroke: marker.color?.colorString ?? "black"
}}
/>
) : null
)}
</ChartsSurface>

{showLegend && (
Expand Down
Loading
Loading