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
4 changes: 4 additions & 0 deletions crates/agent/src/session/property_allowlist.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub const PROPERTIES: &[PropertyId] = &[
properties::line_display::MARKER_EDGE_COLOR,
properties::line_display::MARKER_EDGE_WIDTH,
properties::line_display::MARKER_HOLLOW,
properties::line_display::MARKER_EVERY,
properties::line_display::BAR_WIDTH,
properties::line_display::BAR_EDGE_COLOR,
properties::line_display::BAR_EDGE_WIDTH,
Expand All @@ -46,6 +47,8 @@ pub const PROPERTIES: &[PropertyId] = &[
properties::axis::EQUAL_SCALE,
properties::axis::X_SCALE,
properties::axis::Y_SCALE,
properties::axis::X_MAJOR_STEP,
properties::axis::Y_MAJOR_STEP,
properties::plot_display::X_UNIT,
properties::plot_display::Y_UNIT,
properties::plot_display::X_RANGE,
Expand All @@ -56,6 +59,7 @@ pub const PROPERTIES: &[PropertyId] = &[
properties::right_axis::Y2_SHOW_TICK_LABELS,
properties::right_axis::Y2_SHOW_LABEL,
properties::right_axis::Y2_UNIT,
properties::right_axis::Y2_MAJOR_STEP,
properties::plot_display::X_BREAK,
properties::plot_display::Y_BREAK,
properties::plot_display::X_BREAK_FRACTION,
Expand Down
12 changes: 12 additions & 0 deletions crates/app/src/ui/properties/presentations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,19 @@ pub const PRESENTATIONS: &[PropertyPresentation] = &[
RIGHT_AXIS_HOME,
),
object_entry(right_axis::Y2_UNIT, "Right Y display unit", RIGHT_AXIS_HOME),
object_entry(
right_axis::Y2_MAJOR_STEP,
"Right Y major tick step",
RIGHT_AXIS_HOME,
),
object_entry(line_display::MARKER_SIZE, "Marker diameter", LINE_HOME),
object_entry(line_display::MARKER_FILL, "Marker fill", LINE_HOME),
object_entry(line_display::MARKER_HOLLOW, "Hollow marker", LINE_HOME),
object_entry(
line_display::MARKER_EVERY,
"Marker every N points",
LINE_HOME,
),
object_entry(line_display::BAR_WIDTH, "Bar width", LINE_HOME),
object_entry(line_display::BAR_EDGE_COLOR, "Bar outline color", LINE_HOME),
object_entry(line_display::BAR_EDGE_WIDTH, "Bar outline width", LINE_HOME),
Expand Down Expand Up @@ -159,6 +169,8 @@ pub const PRESENTATIONS: &[PropertyPresentation] = &[
"Y thousands separator",
AXIS_HOME,
),
object_entry(axis::X_MAJOR_STEP, "X major tick step", AXIS_HOME),
object_entry(axis::Y_MAJOR_STEP, "Y major tick step", AXIS_HOME),
PropertyPresentation {
id: axis::FRAME,
localized_label: LocalizedText("Frame"),
Expand Down
20 changes: 18 additions & 2 deletions crates/app/src/ui/windows/plot_text_edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,30 @@ pub(in crate::ui) fn plot_text_edit_window(app: &mut PlotiumApp, ctx: &egui::Con
let Some(edit) = app.session.ui.plot_text_edit.as_mut() else {
return;
};
let response = ui.add(egui::TextEdit::singleline(&mut edit.buffer).desired_width(320.0));
// Text annotations take several lines: Enter starts a new line and
// Command-Enter saves. Line labels stay on one line.
let response = if is_text {
ui.add(
egui::TextEdit::multiline(&mut edit.buffer)
.desired_rows(2)
.desired_width(320.0),
)
} else {
ui.add(egui::TextEdit::singleline(&mut edit.buffer).desired_width(320.0))
};
if edit.focus {
response.request_focus();
edit.focus = false;
}
// A text annotation needs some text; an empty line label removes it.
let valid = !is_text || !edit.buffer.trim().is_empty();
if valid && response.lost_focus() && ui.input(|input| input.key_pressed(egui::Key::Enter)) {
let submitted = if is_text {
response.has_focus()
&& ui.input(|input| input.modifiers.command && input.key_pressed(egui::Key::Enter))
} else {
response.lost_focus() && ui.input(|input| input.key_pressed(egui::Key::Enter))
};
if valid && submitted {
save = true;
}
if ui.input(|input| input.key_pressed(egui::Key::Escape)) {
Expand Down
5 changes: 2 additions & 3 deletions crates/core/src/automation/figure_edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ pub fn figure_edit_schema() -> Value {
let name = json!({"type":"string","minLength":1,"maxLength":1024});
let text = json!({"type":"string","maxLength":16384});
let font = json!({"type":"number","minimum":1,"maximum":72});
let annotation_text =
json!({"type":"string","minLength":1,"maxLength":crate::state::MAX_ANNOTATION_TEXT_BYTES});
let annotation_text = json!({"type":"string","minLength":1,"maxLength":crate::state::MAX_ANNOTATION_TEXT_BYTES,"description":"\\n starts a new line."});
let annotation_id = json!({"type":"integer","minimum":0,"description":"Plot-local annotation id from document inspection or an earlier annotation_edits result."});
let data_point = json!({"type":"array","minItems":2,"maxItems":2,"items":number,"description":"Finite [x,y] anchor in the plot's source data coordinates (the units in its coordinate_units, before any display-unit conversion). The label is centred above the anchor, or moved up to three text heights to the nearest spot clear of data, and follows axis range, zoom and pan; anchors outside the visible range are clipped."});
let orientation = json!({"enum":["vertical","horizontal"],"description":"vertical: constant x, spans the plot's height; horizontal: constant y, spans its width."});
Expand Down Expand Up @@ -161,7 +160,7 @@ pub fn figure_edit_schema() -> Value {
),
];
variants[0]["not"] = json!({"required":["size_mm","size_preset"]});
json!({"type":"object","additionalProperties":false,"required":["canvas","edits"],"properties":{"canvas":target,"edits":{"type":"array","minItems":1,"maxItems":200,"items":{"oneOf":variants}}},"description":"Atomic native authoring on one exact canvas. Panel frames [x,y,w,h] are page pt, resize scales child-local frames. New content frames are parent-local pt. Page size/margins/gutter use mm; margins [top,right,bottom,left]. Grid uses native Frame-basis cells in supplied Panel order, existing page margins/gutter; no page-layout preference change or non-target reflow. Align uses bounding edges/centres, distribution equal centre spacing with endpoints fixed. Panel layout ops require unlocked Panels with free or shared-x stack layout; new text/images need a free-layout parent. panel_stack needs at least two plots in the Panel and its merge Panels: members fill its inner width, heights keep their frame proportions, only the bottom plot draws x tick labels and x title and only the top plot a title, inner boundaries touch with inward x ticks, members get one x range covering all their data (x scales must match) and draw the bottom member's x direction, and x range, scale, display unit and thousands separator edits apply to every member or are rejected. content_frame requires the item and parent unlocked, a free-layout or shared-x stack parent (where only the height counts, as the member's share) and Auto height off; image_replace requires the item and parent unlocked, not a layout mode change. Deletion-only batches accept distinct content_delete, originally empty panel_delete and explicit-column series_delete targets on this canvas. Reject overlaps, last-Series removal, defaults/dynamic displays and mixing deletion with other ops. Preserve empty parents, datasets and assets; preflight lists exact deletions and relationship changes. Hidden exact targets remain addressable. Annotation ops target an exact unlocked plot in an unlocked parent, anchor text, reference lines (kind reference_line) or arrows (kind arrow: tail `at`, head `to`) in source data coordinates, allow at most 256 of any kind per plot and are allowed with Auto height. No top-level ids/targets."})
json!({"type":"object","additionalProperties":false,"required":["canvas","edits"],"properties":{"canvas":target,"edits":{"type":"array","minItems":1,"maxItems":200,"items":{"oneOf":variants}}},"description":"Atomic native authoring on one exact canvas. Panel frames [x,y,w,h] are page pt, resize scales child-local frames. New content frames are parent-local pt. Page size/margins/gutter use mm; margins [top,right,bottom,left]. Grid uses native Frame-basis cells in supplied Panel order, existing page margins/gutter; no page-layout preference change or non-target reflow. Align uses bounding edges/centres, distribution equal centre spacing with endpoints fixed. Panel layout ops require unlocked Panels with free or shared-x stack layout; new text/images need a free-layout parent. panel_stack needs at least two plots in the Panel and its merge Panels: members fill its inner width, heights keep their frame proportions, only the bottom plot draws x tick labels and x title and only the top plot a title, inner boundaries touch with inward x ticks, members get one x range covering all their data and the top member's x major tick step (x scales must match) and draw the bottom member's x direction, and x range, scale, display unit, thousands separator and major tick step edits apply to every member or are rejected. content_frame requires the item and parent unlocked, a free-layout or shared-x stack parent (where only the height counts, as the member's share) and Auto height off; image_replace requires the item and parent unlocked, not a layout mode change. Deletion-only batches accept distinct content_delete, originally empty panel_delete and explicit-column series_delete targets on this canvas. Reject overlaps, last-Series removal, defaults/dynamic displays and mixing deletion with other ops. Preserve empty parents, datasets and assets; preflight lists exact deletions and relationship changes. Hidden exact targets remain addressable. Annotation ops target an exact unlocked plot in an unlocked parent, anchor text, reference lines (kind reference_line) or arrows (kind arrow: tail `at`, head `to`) in source data coordinates, allow at most 256 of any kind per plot and are allowed with Auto height. No top-level ids/targets."})
}
/// Built-in page size presets by id, shared by page edits and new canvases.
pub(super) fn size_preset_schema() -> Value {
Expand Down
27 changes: 27 additions & 0 deletions crates/core/src/project/axis_overrides.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ pub struct AxisOverridesDto {
#[serde(default, skip_serializing_if = "Option::is_none")]
y_thousands_separator: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
x_major_step: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
y_major_step: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
guide_visibility: Option<GuideVisibility>,
#[serde(default, skip_serializing_if = "Option::is_none")]
guide_placement: Option<GuidePlacement>,
Expand All @@ -73,6 +77,8 @@ pub struct AxisOverridesDto {
y2_show_tick_labels: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
y2_show_label: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
y2_major_step: Option<f64>,
}

#[derive(Clone, Serialize, Deserialize)]
Expand Down Expand Up @@ -281,6 +287,8 @@ impl AxisOverridesDto {
y_show_label: overrides.y_show_label,
x_thousands_separator: overrides.x_thousands_separator,
y_thousands_separator: overrides.y_thousands_separator,
x_major_step: overrides.x_major_step,
y_major_step: overrides.y_major_step,
guide_visibility: overrides.guide_visibility,
guide_placement: overrides.guide_placement,
guide_layout: overrides.guide_layout,
Expand All @@ -293,6 +301,7 @@ impl AxisOverridesDto {
y2_scale: overrides.y2_scale,
y2_show_tick_labels: overrides.y2_show_tick_labels,
y2_show_label: overrides.y2_show_label,
y2_major_step: overrides.y2_major_step,
})
}

Expand All @@ -317,6 +326,8 @@ impl AxisOverridesDto {
y_show_label: self.y_show_label,
x_thousands_separator: self.x_thousands_separator,
y_thousands_separator: self.y_thousands_separator,
x_major_step: self.x_major_step,
y_major_step: self.y_major_step,
guide_visibility: self.guide_visibility,
guide_placement: self.guide_placement,
guide_layout: self.guide_layout,
Expand All @@ -333,6 +344,7 @@ impl AxisOverridesDto {
y2_scale: self.y2_scale,
y2_show_tick_labels: self.y2_show_tick_labels,
y2_show_label: self.y2_show_label,
y2_major_step: self.y2_major_step,
}
.normalized()
}
Expand Down Expand Up @@ -381,6 +393,21 @@ mod tests {
assert_eq!(restored.to_overrides(), overrides);
}

#[test]
fn major_step_overrides_round_trip_through_the_project_format() {
let overrides = AxisOverrides {
x_major_step: Some(100.0),
y_major_step: Some(20.0),
y2_major_step: Some(0.5),
..AxisOverrides::default()
};
let dto = AxisOverridesDto::from_overrides(&overrides).expect("steps are stored");
let json = serde_json::to_string(&dto).expect("serializes");
assert!(json.contains("\"y_major_step\":20.0"));
let restored: AxisOverridesDto = serde_json::from_str(&json).expect("deserializes");
assert_eq!(restored.to_overrides(), overrides);
}

#[test]
fn right_axis_overrides_round_trip_through_the_project_format() {
let overrides = AxisOverrides {
Expand Down
10 changes: 10 additions & 0 deletions crates/core/src/project/field_catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,16 @@ pub(super) fn validate_series(
"{context} marker diameter must be between 0.1 and 20 pt"
)));
}
if let plotium_figure::SeriesEncoding::Line(line) = encoding
&& line
.appearance
.marker_every
.is_some_and(|n| !(2..=1000).contains(&n.get()))
{
return Err(ProjectError::Invalid(format!(
"{context} marker spacing must be between 2 and 1000 points"
)));
}
if let plotium_figure::SeriesEncoding::Line(line) = encoding
&& line
.appearance
Expand Down
Loading
Loading