Skip to content

Commit c8da10d

Browse files
mortenatorclaude
andcommitted
Add axis configuration (Phase 4)
- Add axes.ts with Y-axis and X-axis rendering (lines, ticks, labels, grid) - Add AxisPanel.tsx with collapsible UI for axis settings - Support scale type (linear/log), min/max override, reverse axis - Smart number formatting for tick labels (K, M suffixes) - Integrate axis shapes into rendering pipeline - Add axis settings section to ChartPanel (hidden for pie charts) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 54efc33 commit c8da10d

5 files changed

Lines changed: 482 additions & 3 deletions

File tree

src/components/AxisPanel.tsx

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import React, { useState, useEffect } from "react";
2+
import { Axis, ScaleType } from "../types/vectorChart";
3+
4+
interface AxisPanelProps {
5+
axis: Axis;
6+
label: string;
7+
onChange: (axis: Axis) => void;
8+
}
9+
10+
const AxisPanel: React.FC<AxisPanelProps> = ({ axis, label, onChange }) => {
11+
const [isExpanded, setIsExpanded] = useState(false);
12+
const [localAxis, setLocalAxis] = useState<Axis>(axis);
13+
14+
useEffect(() => {
15+
setLocalAxis(axis);
16+
}, [axis]);
17+
18+
const handleChange = (updates: Partial<Axis>) => {
19+
const updated = { ...localAxis, ...updates };
20+
setLocalAxis(updated);
21+
onChange(updated);
22+
};
23+
24+
const handleMinChange = (value: string) => {
25+
const numValue = value === "" ? null : parseFloat(value);
26+
handleChange({ minValue: isNaN(numValue as number) ? null : numValue });
27+
};
28+
29+
const handleMaxChange = (value: string) => {
30+
const numValue = value === "" ? null : parseFloat(value);
31+
handleChange({ maxValue: isNaN(numValue as number) ? null : numValue });
32+
};
33+
34+
return (
35+
<div className="border border-gray-200 rounded-lg overflow-hidden">
36+
{/* Header */}
37+
<button
38+
onClick={() => setIsExpanded(!isExpanded)}
39+
className="w-full px-3 py-2 flex items-center justify-between bg-gray-50 hover:bg-gray-100 transition-colors"
40+
>
41+
<span className="text-sm font-medium text-gray-700">{label}</span>
42+
<svg
43+
className={`w-4 h-4 text-gray-500 transition-transform ${isExpanded ? "rotate-180" : ""}`}
44+
fill="none"
45+
stroke="currentColor"
46+
viewBox="0 0 24 24"
47+
>
48+
<path
49+
strokeLinecap="round"
50+
strokeLinejoin="round"
51+
strokeWidth={2}
52+
d="M19 9l-7 7-7-7"
53+
/>
54+
</svg>
55+
</button>
56+
57+
{/* Content */}
58+
{isExpanded && (
59+
<div className="p-3 space-y-3 bg-white">
60+
{/* Scale Type */}
61+
<div>
62+
<label className="block text-xs font-medium text-gray-600 mb-1">
63+
Scale Type
64+
</label>
65+
<select
66+
value={localAxis.scaleType}
67+
onChange={(e) =>
68+
handleChange({ scaleType: e.target.value as ScaleType })
69+
}
70+
className="w-full px-2 py-1.5 text-sm border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-blue-500"
71+
>
72+
<option value="linear">Linear</option>
73+
<option value="log">Logarithmic</option>
74+
</select>
75+
</div>
76+
77+
{/* Min/Max Values */}
78+
<div className="grid grid-cols-2 gap-2">
79+
<div>
80+
<label className="block text-xs font-medium text-gray-600 mb-1">
81+
Min Value
82+
</label>
83+
<input
84+
type="number"
85+
value={localAxis.minValue ?? ""}
86+
onChange={(e) => handleMinChange(e.target.value)}
87+
placeholder="Auto"
88+
className="w-full px-2 py-1.5 text-sm border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-blue-500"
89+
/>
90+
</div>
91+
<div>
92+
<label className="block text-xs font-medium text-gray-600 mb-1">
93+
Max Value
94+
</label>
95+
<input
96+
type="number"
97+
value={localAxis.maxValue ?? ""}
98+
onChange={(e) => handleMaxChange(e.target.value)}
99+
placeholder="Auto"
100+
className="w-full px-2 py-1.5 text-sm border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-blue-500"
101+
/>
102+
</div>
103+
</div>
104+
105+
{/* Reverse Axis */}
106+
<div className="flex items-center gap-2">
107+
<input
108+
type="checkbox"
109+
id={`reverse-${axis.id}`}
110+
checked={localAxis.isReversed}
111+
onChange={(e) => handleChange({ isReversed: e.target.checked })}
112+
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
113+
/>
114+
<label
115+
htmlFor={`reverse-${axis.id}`}
116+
className="text-xs text-gray-600"
117+
>
118+
Reverse axis direction
119+
</label>
120+
</div>
121+
122+
{/* Reset Button */}
123+
<button
124+
onClick={() =>
125+
handleChange({
126+
minValue: null,
127+
maxValue: null,
128+
isReversed: false,
129+
scaleType: "linear",
130+
})
131+
}
132+
className="text-xs text-blue-600 hover:text-blue-800 underline"
133+
>
134+
Reset to auto
135+
</button>
136+
</div>
137+
)}
138+
</div>
139+
);
140+
};
141+
142+
interface AxisConfigurationProps {
143+
axes: Axis[];
144+
onChange: (axes: Axis[]) => void;
145+
}
146+
147+
export const AxisConfiguration: React.FC<AxisConfigurationProps> = ({
148+
axes,
149+
onChange,
150+
}) => {
151+
const yAxis = axes.find((a) => a.orientation === "y");
152+
const xAxis = axes.find((a) => a.orientation === "x");
153+
154+
const handleAxisChange = (updatedAxis: Axis) => {
155+
const newAxes = axes.map((a) =>
156+
a.id === updatedAxis.id ? updatedAxis : a
157+
);
158+
onChange(newAxes);
159+
};
160+
161+
return (
162+
<div className="space-y-2">
163+
<h3 className="text-sm font-semibold text-gray-700">Axis Configuration</h3>
164+
{yAxis && (
165+
<AxisPanel
166+
axis={yAxis}
167+
label="Value Axis (Y)"
168+
onChange={handleAxisChange}
169+
/>
170+
)}
171+
{xAxis && (
172+
<AxisPanel
173+
axis={xAxis}
174+
label="Category Axis (X)"
175+
onChange={handleAxisChange}
176+
/>
177+
)}
178+
{!yAxis && !xAxis && (
179+
<p className="text-xs text-gray-500 italic">
180+
No axes configured for this chart type.
181+
</p>
182+
)}
183+
</div>
184+
);
185+
};
186+
187+
export default AxisPanel;

src/components/ChartPanel.tsx

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
import React, { useState, useEffect } from "react";
2-
import { insertChart, insertChartV2, ChartType } from "../utils/chartUtils";
2+
import {
3+
insertChart,
4+
insertChartV2,
5+
ChartType,
6+
insertVectorChart,
7+
chartDataToVectorChart,
8+
} from "../utils/chartUtils";
39
import { ChartData, createDefaultChartData } from "../types/chartData";
10+
import { Axis, createAxis } from "../types/vectorChart";
411
import { updateChartPersistent } from "../utils/dataStorage";
512
import { ChartSelection } from "../utils/selectionManager";
613
import { EditorMode } from "../taskpane/App";
714
import DataGrid from "./DataGrid";
15+
import { AxisConfiguration } from "./AxisPanel";
816

917
interface ChartPanelProps {
1018
isLoading: boolean;
@@ -86,12 +94,19 @@ const ChartPanel: React.FC<ChartPanelProps> = ({
8694
const [chartData, setChartData] = useState<ChartData>(() =>
8795
createDefaultChartData("bar")
8896
);
97+
const [axes, setAxes] = useState<Axis[]>(() => [
98+
createAxis("category", "x"),
99+
createAxis("value", "y"),
100+
]);
101+
const [showAxisConfig, setShowAxisConfig] = useState(false);
89102

90103
// Load selected chart data when selection changes
91104
useEffect(() => {
92105
if (mode === "edit" && selectedChart) {
93106
setChartData(selectedChart.chartData);
94107
setSelectedChartType(selectedChart.chartData.type);
108+
// Reset axes for now - TODO: load from stored VectorChart
109+
setAxes([createAxis("category", "x"), createAxis("value", "y")]);
95110
}
96111
}, [mode, selectedChart]);
97112

@@ -114,8 +129,15 @@ const ChartPanel: React.FC<ChartPanelProps> = ({
114129
id: `chart_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
115130
};
116131

117-
// Use new rendering pipeline
118-
await insertChartV2(selectedChartType, dataToInsert);
132+
// Convert to VectorChart and apply axis configuration
133+
const vectorChart = chartDataToVectorChart(
134+
dataToInsert,
135+
selectedChartType as any
136+
);
137+
vectorChart.axes = axes;
138+
139+
// Use new rendering pipeline with VectorChart
140+
await insertVectorChart(vectorChart);
119141

120142
showMessage(
121143
`${selectedChartType.charAt(0).toUpperCase() + selectedChartType.slice(1)} chart inserted!`
@@ -212,6 +234,36 @@ const ChartPanel: React.FC<ChartPanelProps> = ({
212234
</div>
213235
</section>
214236

237+
{/* Axis Configuration (collapsible) */}
238+
{selectedChartType !== "pie" && (
239+
<section className="bg-white rounded-xl border border-gray-200 p-3 shadow-sm">
240+
<button
241+
onClick={() => setShowAxisConfig(!showAxisConfig)}
242+
className="w-full flex items-center justify-between text-sm font-semibold text-gray-700"
243+
>
244+
<span>Axis Settings</span>
245+
<svg
246+
className={`w-4 h-4 text-gray-500 transition-transform ${showAxisConfig ? "rotate-180" : ""}`}
247+
fill="none"
248+
stroke="currentColor"
249+
viewBox="0 0 24 24"
250+
>
251+
<path
252+
strokeLinecap="round"
253+
strokeLinejoin="round"
254+
strokeWidth={2}
255+
d="M19 9l-7 7-7-7"
256+
/>
257+
</svg>
258+
</button>
259+
{showAxisConfig && (
260+
<div className="mt-3">
261+
<AxisConfiguration axes={axes} onChange={setAxes} />
262+
</div>
263+
)}
264+
</section>
265+
)}
266+
215267
{/* Action Button */}
216268
<section>
217269
{isEditMode ? (

0 commit comments

Comments
 (0)