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
59 changes: 59 additions & 0 deletions src/components/charts/AreaGraph/AreaGraph.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,65 @@ export const Stacked: Story = {
},
};

const weekdayDataSeries = [
{
// x positions are integer indices; xTickText supplies the day labels
x: [0, 1, 2, 3, 4, 5, 6],
y: [120, 130, 100, 110, 140, 80, 60],
name: "Throughput",
},
];

export const CategoricalXLabels: Story = {
name: "Categorical X Labels",
parameters: {
// Auto-generated by sync-storybook-zephyr - do not add manually
zephyr: { testCaseId: "" },
docs: {
description: {
story:
"Use `xTickText` to display categorical x-axis labels (e.g. days of the week). The numeric `x` values still drive area positioning, but the rendered tick labels match `xTickText` in order.",
},
},
},
args: {
dataSeries: weekdayDataSeries,
xTickText: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
title: "Throughput by Weekday",
xTitle: "Day",
yTitle: "Files",
width: 1000,
height: 600,
variant: "normal",
},
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);

await step("Chart title is displayed", async () => {
expect(canvas.getByText("Throughput by Weekday")).toBeInTheDocument();
});

await step("Chart container renders", async () => {
expect(canvasElement.querySelector(".js-plotly-plot")).toBeInTheDocument();
});

await step("X-axis ticks show categorical labels, not integers", async () => {
const tickLabels = [
...canvasElement.querySelectorAll(".xtick text"),
].map((node) => node.textContent);
expect(tickLabels).toEqual([
"Mon",
"Tue",
"Wed",
"Thu",
"Fri",
"Sat",
"Sun",
]);
});
},
};

export const CustomRange: Story = {
name: "Custom Range",
parameters: {
Expand Down
25 changes: 23 additions & 2 deletions src/components/charts/AreaGraph/AreaGraph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ interface AreaGraphProps {
xTitle?: string;
yTitle?: string;
title?: string;
/**
* Categorical labels for the x-axis ticks. When provided, the x data values
* still drive area positioning but the displayed tick labels match these
* strings in order (e.g. ["Mon", "Tue", …]). Should align 1:1 with the
* unique, ordered x values across all series.
*/
xTickText?: string[];
}

const AreaGraph: React.FC<AreaGraphProps> = ({
Expand All @@ -39,6 +46,7 @@ const AreaGraph: React.FC<AreaGraphProps> = ({
xTitle = "Columns",
yTitle = "Rows",
title = "Area Graph",
xTickText,
}) => {
const plotRef = useRef<HTMLDivElement>(null);
const theme = usePlotlyTheme();
Expand Down Expand Up @@ -125,6 +133,18 @@ const AreaGraph: React.FC<AreaGraphProps> = ({
return ticks;
}, [effectiveYRange]);

// When categorical labels are supplied, ticks must sit on the actual data
// x-positions rather than the computed nice-step values above. Sorted
// ascending so labels map deterministically to x regardless of series order.
const xDataValues = useMemo(
() => [...new Set(dataSeries.flatMap((s) => s.x))].sort((a, b) => a - b),
[dataSeries],
);
Comment thread
Copilot marked this conversation as resolved.

// Only apply categorical labels when they align 1:1 with the tick positions;
// a mismatch would silently mis-label ticks, so fall back to numeric ticks.
const useCategoricalX = !!xTickText && xTickText.length === xDataValues.length;

const tickOptions = useMemo(
() => ({
tickcolor: theme.tickColor,
Expand Down Expand Up @@ -258,7 +278,8 @@ const AreaGraph: React.FC<AreaGraphProps> = ({
range: xRange,
autorange: !xRange,
tickmode: "array" as const,
tickvals: xTicks,
tickvals: useCategoricalX ? xDataValues : xTicks,
...(useCategoricalX ? { ticktext: xTickText } : {}),
showgrid: true,
...tickOptions,
},
Expand Down Expand Up @@ -316,7 +337,7 @@ const AreaGraph: React.FC<AreaGraphProps> = ({
Plotly.purge(plotElement);
}
};
}, [dataSeries, width, height, xRange, yRange, effectiveXRange, effectiveYRange, variant, xTitle, yTitle, titleOptions, tickOptions, xTicks, yTicks, theme, bindTooltip]);
}, [dataSeries, width, height, xRange, yRange, effectiveXRange, effectiveYRange, variant, xTitle, yTitle, titleOptions, tickOptions, xTicks, yTicks, xDataValues, useCategoricalX, xTickText, theme, bindTooltip]);

return (
<div className="area-graph-container relative">
Expand Down
75 changes: 75 additions & 0 deletions src/components/charts/BarGraph/BarGraph.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,26 @@ const generateGroupedBarData = (): BarDataSeries[] => {
];
};

const generatePipelineRunsByStatus = (): BarDataSeries[] => {
// x positions are integer indices; xTickText supplies the day labels
const x = [0, 1, 2, 3, 4, 5, 6];

return [
{
name: "Success",
x,
y: [42, 51, 38, 60, 55, 12, 8],
color: "#29A634",
},
{
name: "Failed",
x,
y: [3, 5, 2, 7, 4, 1, 0],
color: "#CD4246",
},
];
};

const generateStackedBarData = (): BarDataSeries[] => {
const x = [200, 300, 400, 500, 600, 700, 800, 900, 1000];

Expand Down Expand Up @@ -171,6 +191,61 @@ export const StackedBars: Story = {
},
};

export const CategoricalXLabels: Story = {
name: "Categorical X Labels",
parameters: {
// Auto-generated by sync-storybook-zephyr - do not add manually
zephyr: { testCaseId: "" },
docs: {
description: {
story:
"Use `xTickText` to display categorical x-axis labels (e.g. days of the week). The numeric `x` values still drive bar positioning, but the rendered tick labels match `xTickText` in order.",
},
},
},
args: {
dataSeries: generatePipelineRunsByStatus(),
xTickText: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
variant: "group",
title: "Pipeline Runs by Status",
xTitle: "Day",
yTitle: "Runs",
width: 1000,
height: 600,
},
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);

await step("Chart title is displayed", async () => {
expect(canvas.getByText("Pipeline Runs by Status")).toBeInTheDocument();
});

await step("Chart container renders", async () => {
expect(canvasElement.querySelector(".js-plotly-plot")).toBeInTheDocument();
});

await step("X-axis ticks show categorical labels, not integers", async () => {
const tickLabels = [
...canvasElement.querySelectorAll(".xtick text"),
].map((node) => node.textContent);
expect(tickLabels).toEqual([
"Mon",
"Tue",
"Wed",
"Thu",
"Fri",
"Sat",
"Sun",
]);
});

await step("Legend shows all series names", async () => {
expect(canvas.getByText("Success")).toBeInTheDocument();
expect(canvas.getByText("Failed")).toBeInTheDocument();
});
},
};

export const CustomStyling: Story = {
name: "Custom Styling",
parameters: {
Expand Down
17 changes: 15 additions & 2 deletions src/components/charts/BarGraph/BarGraph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ interface BarGraphProps {
yTitle?: string;
title?: string;
barWidth?: number;
/**
* Categorical labels for the x-axis ticks. When provided, the x data values
* still drive bar positioning but the displayed tick labels match these
* strings in order (e.g. ["Mon", "Tue", …]). Should align 1:1 with the
* unique, ordered x values across all series.
*/
xTickText?: string[];
}

const BarGraph: React.FC<BarGraphProps> = ({
Expand All @@ -45,6 +52,7 @@ const BarGraph: React.FC<BarGraphProps> = ({
yTitle = "Rows",
title = "Bar Graph",
barWidth = 24,
xTickText,
}) => {
const plotRef = useRef<HTMLDivElement>(null);
const theme = usePlotlyTheme();
Expand Down Expand Up @@ -84,10 +92,14 @@ const BarGraph: React.FC<BarGraphProps> = ({
);

const xTicks = useMemo(
() => [...new Set(dataSeries.flatMap((s) => s.x))],
() => [...new Set(dataSeries.flatMap((s) => s.x))].sort((a, b) => a - b),
[dataSeries],
);

// Only apply categorical labels when they align 1:1 with the tick positions;
// a mismatch would silently mis-label ticks, so fall back to numeric ticks.
const useCategoricalX = !!xTickText && xTickText.length === xTicks.length;

const yTicks = useMemo(() => {
const range = effectiveYRange[1] - effectiveYRange[0];
let step = Math.pow(10, Math.floor(Math.log10(range)));
Expand Down Expand Up @@ -188,6 +200,7 @@ const BarGraph: React.FC<BarGraphProps> = ({
autorange: !xRange,
tickmode: "array" as const,
tickvals: xTicks,
...(useCategoricalX ? { ticktext: xTickText } : {}),
showgrid: true,
...tickOptions,
},
Expand Down Expand Up @@ -244,7 +257,7 @@ const BarGraph: React.FC<BarGraphProps> = ({
Plotly.purge(plotElement);
}
};
}, [dataSeries, width, height, xRange, yRange, xTitle, yTitle, title, barWidth, barMode, tickOptions, xTicks, yTicks, theme, bindTooltip]);
}, [dataSeries, width, height, xRange, yRange, xTitle, yTitle, title, barWidth, barMode, tickOptions, xTicks, yTicks, useCategoricalX, xTickText, theme, bindTooltip]);

return (
<div className="bar-graph-container relative">
Expand Down
73 changes: 73 additions & 0 deletions src/components/charts/LineGraph/LineGraph.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,26 @@ const generateDemoDataWithErrorBars = (): LineDataSeries[] => {
}));
};

const generateWeekdayData = (): LineDataSeries[] => {
// x positions are integer indices; xTickText supplies the day labels
const x = [0, 1, 2, 3, 4, 5, 6];

return [
{
name: "Instrument A",
symbol: "circle",
x,
y: [12, 18, 15, 22, 19, 8, 5],
},
{
name: "Instrument B",
symbol: "square",
x,
y: [20, 24, 21, 28, 26, 14, 10],
},
];
};

const meta: Meta<typeof LineGraph> = {
title: "Charts/Line Graph",
component: LineGraph,
Expand Down Expand Up @@ -298,6 +318,59 @@ export const Basic: Story = {
},
};

export const CategoricalXLabels: Story = {
name: "Categorical X Labels",
parameters: {
// Auto-generated by sync-storybook-zephyr - do not add manually
zephyr: { testCaseId: "" },
docs: {
description: {
story:
"Use `xTickText` to display categorical x-axis labels (e.g. days of the week). The numeric `x` values still drive line positioning, but the rendered tick labels match `xTickText` in order.",
},
},
},
args: {
dataSeries: generateWeekdayData(),
xTickText: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
variant: "lines+markers",
title: "Runs per Weekday",
xTitle: "Day",
yTitle: "Runs",
},
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);

await step("Chart title is displayed", async () => {
expect(canvas.getByText("Runs per Weekday")).toBeInTheDocument();
});

await step("Chart container renders", async () => {
expect(canvasElement.querySelector(".js-plotly-plot")).toBeInTheDocument();
});

await step("X-axis ticks show categorical labels, not integers", async () => {
const tickLabels = [
...canvasElement.querySelectorAll(".xtick text"),
].map((node) => node.textContent);
expect(tickLabels).toEqual([
"Mon",
"Tue",
"Wed",
"Thu",
"Fri",
"Sat",
"Sun",
]);
});

await step("Legend shows series names", async () => {
expect(canvas.getByText("Instrument A")).toBeInTheDocument();
expect(canvas.getByText("Instrument B")).toBeInTheDocument();
});
},
};

export const WithMarkers: Story = {
name: "With Markers",
parameters: {
Expand Down
Loading
Loading