Skip to content

Commit 8db3e04

Browse files
authored
Merge pull request #2172 from mito-ds/fix/chart-wizard-limited-ranges
Chart Wizard Input Ranges
2 parents d4cf33b + 1af6655 commit 8db3e04

File tree

5 files changed

+237
-12
lines changed

5 files changed

+237
-12
lines changed

mito-ai/mito_ai/completions/prompt_builders/prompt_constants.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,17 @@
2626
- NEVER include comments on the same line as a variable assignment. Each variable assignment must be on its own line with no trailing comments.
2727
- For string values, use either single or double quotes (e.g., TITLE = "Sales by Product" or TITLE = 'Sales by Product'). Do not use nested quotes (e.g., do NOT use '"value"').
2828
29+
Fixed acceptable ranges (matplotlib constraints):
30+
- For numeric variables that have a fixed acceptable range, add a line immediately BEFORE the variable assignment: # RANGE VARIABLE_NAME MIN MAX
31+
- This allows the Chart Wizard to clamp inputs and prevent invalid values. Use the following ranges when you use these variables:
32+
- ALPHA (opacity): 0 1
33+
- FIGURE_SIZE (tuple width, height in inches): 1 24 (each element)
34+
- LINE_WIDTH, LINEWIDTH, LWD: 0 20
35+
- FONT_SIZE, FONTSIZE, FONT_SIZE_TITLE, FONT_SIZE_LABEL: 0.1 72
36+
- MARKER_SIZE, MARKERSIZE, S: 0 1000
37+
- DPI: 1 600
38+
- Any other numeric or tuple variable that you know has matplotlib constraints: add # RANGE VARIABLE_NAME MIN MAX with the appropriate min and max.
39+
2940
Common Mistakes to Avoid:
3041
- WRONG: COLOR = '"#1877F2" # Meta Blue' (nested quotes and inline comment)
3142
- WRONG: COLOR = "#1877F2" # Meta Blue (inline comment)
@@ -38,6 +49,10 @@
3849
X_LABEL = "Product"
3950
Y_LABEL = "Sales"
4051
BAR_COLOR = "#000000"
52+
# RANGE ALPHA 0 1
53+
ALPHA = 0.8
54+
# RANGE FIGURE_SIZE 1 24
55+
FIGURE_SIZE = (12, 6)
4156
# === END CONFIG ===
4257
"""
4358

mito-ai/src/Extensions/ChartWizard/inputs/NumberInputRow.tsx

Lines changed: 119 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,129 @@
66
import React from 'react';
77
import { InputRowProps } from './types';
88

9+
function clamp(value: number, min: number, max: number): number {
10+
return Math.min(Math.max(value, min), max);
11+
}
12+
13+
/** Use slider when range is small and decimal-friendly (e.g. opacity 0–1). */
14+
const SLIDER_RANGE_THRESHOLD = 2;
15+
16+
/** Step for decimal ranges: fine for 0–1, coarser for 1–2. */
17+
function decimalStep(min: number, max: number): number {
18+
const range = max - min;
19+
return range <= 1 ? 0.01 : 0.1;
20+
}
21+
22+
interface SliderWithNumberInputProps {
23+
value: number;
24+
min: number;
25+
max: number;
26+
step: number;
27+
label: string;
28+
onChange: (value: number) => void;
29+
}
30+
31+
const SliderWithNumberInput: React.FC<SliderWithNumberInputProps> = ({
32+
value,
33+
min,
34+
max,
35+
step,
36+
label,
37+
onChange
38+
}) => (
39+
<div className="chart-wizard-number-slider-row">
40+
<input
41+
type="range"
42+
min={min}
43+
max={max}
44+
step={step}
45+
value={value}
46+
onChange={(e) => onChange(clamp(parseFloat(e.target.value), min, max))}
47+
className="chart-wizard-range-slider"
48+
aria-label={label}
49+
/>
50+
<input
51+
type="number"
52+
min={min}
53+
max={max}
54+
step={step}
55+
value={value}
56+
onChange={(e) => {
57+
let v = parseFloat(e.target.value);
58+
if (Number.isNaN(v)) v = min;
59+
onChange(clamp(v, min, max));
60+
}}
61+
className="chart-wizard-number-input chart-wizard-number-input-narrow"
62+
aria-label={`${label} (number)`}
63+
/>
64+
</div>
65+
);
66+
67+
interface NumberInputOnlyProps {
68+
value: number;
69+
min?: number;
70+
max?: number;
71+
step: number;
72+
onChange: (value: number) => void;
73+
}
74+
75+
const NumberInputOnly: React.FC<NumberInputOnlyProps> = ({
76+
value,
77+
min,
78+
max,
79+
step,
80+
onChange
81+
}) => (
82+
<input
83+
type="number"
84+
value={value}
85+
min={min}
86+
max={max}
87+
step={step}
88+
onChange={(e) => {
89+
let v = parseFloat(e.target.value);
90+
if (Number.isNaN(v)) v = min ?? 0;
91+
if (min !== undefined && max !== undefined) v = clamp(v, min, max);
92+
onChange(v);
93+
}}
94+
className="chart-wizard-number-input"
95+
/>
96+
);
97+
998
export const NumberInputRow: React.FC<InputRowProps> = ({ variable, label, onVariableChange }) => {
99+
const numValue = variable.value as number;
100+
const min = variable.min;
101+
const max = variable.max;
102+
const hasRange = min !== undefined && max !== undefined;
103+
const rangeSpan = hasRange ? (max as number) - (min as number) : 0;
104+
const useSlider = hasRange && rangeSpan <= SLIDER_RANGE_THRESHOLD;
105+
const step = useSlider ? decimalStep(min as number, max as number) : 1;
106+
107+
const handleChange = (value: number): void => {
108+
onVariableChange(variable.name, value);
109+
};
110+
10111
return (
11112
<div key={variable.name} className="chart-wizard-input-row">
12113
<label className="chart-wizard-input-label">{label}</label>
13-
<input
14-
type="number"
15-
value={variable.value as number}
16-
onChange={(e) => onVariableChange(variable.name, parseFloat(e.target.value) || 0)}
17-
className="chart-wizard-number-input"
18-
/>
114+
{useSlider && min !== undefined && max !== undefined ? (
115+
<SliderWithNumberInput
116+
value={numValue}
117+
min={min}
118+
max={max}
119+
step={step}
120+
label={label}
121+
onChange={handleChange}
122+
/>
123+
) : (
124+
<NumberInputOnly
125+
value={numValue}
126+
min={min}
127+
max={max}
128+
step={step}
129+
onChange={handleChange}
130+
/>
131+
)}
19132
</div>
20133
);
21134
};

mito-ai/src/Extensions/ChartWizard/inputs/TupleInputRow.tsx

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,20 @@
66
import React from 'react';
77
import { InputRowProps } from './types';
88

9+
function clamp(value: number, min: number, max: number): number {
10+
return Math.min(Math.max(value, min), max);
11+
}
12+
913
export const TupleInputRow: React.FC<InputRowProps> = ({ variable, label, onVariableChange }) => {
1014
const tupleValue = variable.value as [number, number];
15+
const min = variable.min;
16+
const max = variable.max;
17+
const clampOrIdentity = (v: number, fallback: number): number => {
18+
if (min !== undefined && max !== undefined) {
19+
return clamp(v, min, max);
20+
}
21+
return Number.isNaN(v) ? fallback : v;
22+
};
1123
return (
1224
<div key={variable.name} className="chart-wizard-input-row">
1325
<label className="chart-wizard-input-label">{label}</label>
@@ -16,19 +28,25 @@ export const TupleInputRow: React.FC<InputRowProps> = ({ variable, label, onVari
1628
<input
1729
type="number"
1830
value={tupleValue[0]}
31+
min={min}
32+
max={max}
33+
step={min !== undefined && max !== undefined && max - min <= 1 ? 'any' : 1}
1934
onChange={(e) => {
20-
const newValue: [number, number] = [parseFloat(e.target.value) || 0, tupleValue[1]];
21-
onVariableChange(variable.name, newValue);
35+
const v = clampOrIdentity(parseFloat(e.target.value), 0);
36+
onVariableChange(variable.name, [v, tupleValue[1]]);
2237
}}
2338
className="chart-wizard-tuple-input"
2439
/>
2540
<span>,</span>
2641
<input
2742
type="number"
2843
value={tupleValue[1]}
44+
min={min}
45+
max={max}
46+
step={min !== undefined && max !== undefined && max - min <= 1 ? 'any' : 1}
2947
onChange={(e) => {
30-
const newValue: [number, number] = [tupleValue[0], parseFloat(e.target.value) || 0];
31-
onVariableChange(variable.name, newValue);
48+
const v = clampOrIdentity(parseFloat(e.target.value), 0);
49+
onVariableChange(variable.name, [tupleValue[0], v]);
3250
}}
3351
className="chart-wizard-tuple-input"
3452
/>

mito-ai/src/Extensions/ChartWizard/utils/parser.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ export interface ChartConfigVariable {
77
name: string;
88
value: string | number | boolean | [number, number];
99
type: 'string' | 'number' | 'boolean' | 'tuple' | 'expression';
10+
/** Optional min value for numeric/tuple params (matplotlib constraints). */
11+
min?: number;
12+
/** Optional max value for numeric/tuple params (matplotlib constraints). */
13+
max?: number;
1014
}
1115

1216
export interface ParsedChartConfig {
@@ -33,10 +37,23 @@ export function parseChartConfig(sourceCode: string): ParsedChartConfig | null {
3337
const configSection = sourceCode.substring(startIndex + configStartMarker.length, endIndex).trim();
3438
const lines = configSection.split('\n').map(line => line.trim()).filter(line => line.length > 0);
3539

40+
// First pass: collect # RANGE lines (format: # RANGE VARIABLE_NAME MIN MAX)
41+
const rangeByVar = new Map<string, { min: number; max: number }>();
42+
const rangeRegex = /^#\s*RANGE\s+([A-Z_][A-Z0-9_]*)\s+(-?\d+(?:\.\d+)?)\s+(-?\d+(?:\.\d+)?)\s*$/;
43+
for (const line of lines) {
44+
const rangeMatch = line.match(rangeRegex);
45+
if (rangeMatch && rangeMatch[1] && rangeMatch[2] !== undefined && rangeMatch[3] !== undefined) {
46+
const varName = rangeMatch[1];
47+
const minVal = parseFloat(rangeMatch[2]);
48+
const maxVal = parseFloat(rangeMatch[3]);
49+
rangeByVar.set(varName, { min: minVal, max: maxVal });
50+
}
51+
}
52+
3653
const variables: ChartConfigVariable[] = [];
3754

3855
for (const line of lines) {
39-
// Skip comment lines
56+
// Skip comment-only lines (RANGE lines already processed above)
4057
if (line.startsWith('#')) {
4158
continue;
4259
}
@@ -48,10 +65,12 @@ export function parseChartConfig(sourceCode: string): ParsedChartConfig | null {
4865
const valueStr = match[2];
4966
const parsed = parseValue(valueStr.trim());
5067
if (parsed) {
68+
const range = rangeByVar.get(varName);
5169
variables.push({
5270
name: varName,
5371
value: parsed.value,
54-
type: parsed.type
72+
type: parsed.type,
73+
...(range && { min: range.min, max: range.max })
5574
});
5675
}
5776
}
@@ -228,6 +247,9 @@ export function updateChartConfig(sourceCode: string, variables: ChartConfigVari
228247
const variablesToWrite = parsed.variables.map(v => varMap.get(v.name) || v);
229248

230249
for (const variable of variablesToWrite) {
250+
if (variable.min !== undefined && variable.max !== undefined) {
251+
newConfigSection += `# RANGE ${variable.name} ${variable.min} ${variable.max}\n`;
252+
}
231253
const formattedValue = formatValue(variable.value, variable.type);
232254
newConfigSection += `${variable.name} = ${formattedValue}\n`;
233255
}

mito-ai/style/ChartWizardWidget.css

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,63 @@
109109
box-sizing: border-box;
110110
}
111111

112+
.chart-wizard-number-slider-row {
113+
display: flex;
114+
align-items: center;
115+
gap: 12px;
116+
width: 100%;
117+
}
118+
119+
.chart-wizard-range-slider {
120+
flex: 1;
121+
min-width: 0;
122+
height: 6px;
123+
-webkit-appearance: none;
124+
appearance: none;
125+
background: var(--jp-border-color2);
126+
border-radius: 3px;
127+
outline: none;
128+
}
129+
130+
.chart-wizard-range-slider::-webkit-slider-thumb {
131+
-webkit-appearance: none;
132+
appearance: none;
133+
width: 16px;
134+
height: 16px;
135+
border-radius: 50%;
136+
background: var(--jp-brand-color1);
137+
cursor: pointer;
138+
border: none;
139+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
140+
}
141+
142+
.chart-wizard-range-slider::-moz-range-thumb {
143+
width: 16px;
144+
height: 16px;
145+
border-radius: 50%;
146+
background: var(--jp-brand-color1);
147+
cursor: pointer;
148+
border: none;
149+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
150+
}
151+
152+
.chart-wizard-range-slider:focus {
153+
outline: none;
154+
}
155+
156+
.chart-wizard-range-slider:focus::-webkit-slider-thumb {
157+
box-shadow: 0 0 0 3px rgba(33, 150, 243, 0.25);
158+
}
159+
160+
.chart-wizard-range-slider:focus::-moz-range-thumb {
161+
box-shadow: 0 0 0 3px rgba(33, 150, 243, 0.25);
162+
}
163+
164+
.chart-wizard-number-input-narrow {
165+
width: 72px;
166+
flex-shrink: 0;
167+
}
168+
112169
.chart-wizard-color-container {
113170
display: flex;
114171
gap: 8px;

0 commit comments

Comments
 (0)