mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
358 lines
12 KiB
Rust
358 lines
12 KiB
Rust
use serde::Deserialize;
|
|
use serde::Serialize;
|
|
|
|
use crate::CellAddress;
|
|
use crate::CellRange;
|
|
use crate::SpreadsheetArtifactError;
|
|
use crate::SpreadsheetSheet;
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "lowercase")]
|
|
pub enum SpreadsheetChartType {
|
|
Area,
|
|
Bar,
|
|
Doughnut,
|
|
Line,
|
|
Pie,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "lowercase")]
|
|
pub enum SpreadsheetChartLegendPosition {
|
|
Bottom,
|
|
Top,
|
|
Left,
|
|
Right,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SpreadsheetChartLegend {
|
|
pub visible: bool,
|
|
pub position: SpreadsheetChartLegendPosition,
|
|
pub overlay: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SpreadsheetChartAxis {
|
|
pub linked_number_format: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SpreadsheetChartSeries {
|
|
pub id: u32,
|
|
pub name: Option<String>,
|
|
pub category_sheet_name: Option<String>,
|
|
pub category_range: String,
|
|
pub value_sheet_name: Option<String>,
|
|
pub value_range: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SpreadsheetChart {
|
|
pub id: u32,
|
|
pub chart_type: SpreadsheetChartType,
|
|
pub source_sheet_name: Option<String>,
|
|
pub source_range: Option<String>,
|
|
pub title: Option<String>,
|
|
pub style_index: u32,
|
|
pub display_blanks_as: String,
|
|
pub legend: SpreadsheetChartLegend,
|
|
pub category_axis: SpreadsheetChartAxis,
|
|
pub value_axis: SpreadsheetChartAxis,
|
|
#[serde(default)]
|
|
pub series: Vec<SpreadsheetChartSeries>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct SpreadsheetChartLookup {
|
|
pub id: Option<u32>,
|
|
pub index: Option<usize>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SpreadsheetChartCreateOptions {
|
|
pub id: Option<u32>,
|
|
pub title: Option<String>,
|
|
pub legend_visible: Option<bool>,
|
|
pub legend_position: Option<SpreadsheetChartLegendPosition>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SpreadsheetChartProperties {
|
|
pub title: Option<String>,
|
|
pub legend_visible: Option<bool>,
|
|
pub legend_position: Option<SpreadsheetChartLegendPosition>,
|
|
}
|
|
|
|
impl SpreadsheetSheet {
|
|
pub fn list_charts(
|
|
&self,
|
|
range: Option<&CellRange>,
|
|
) -> Result<Vec<SpreadsheetChart>, SpreadsheetArtifactError> {
|
|
Ok(self
|
|
.charts
|
|
.iter()
|
|
.filter(|chart| {
|
|
range.is_none_or(|target| {
|
|
chart
|
|
.source_range
|
|
.as_deref()
|
|
.map(CellRange::parse)
|
|
.transpose()
|
|
.ok()
|
|
.flatten()
|
|
.is_some_and(|chart_range| chart_range.intersects(target))
|
|
})
|
|
})
|
|
.cloned()
|
|
.collect())
|
|
}
|
|
|
|
pub fn get_chart(
|
|
&self,
|
|
action: &str,
|
|
lookup: SpreadsheetChartLookup,
|
|
) -> Result<&SpreadsheetChart, SpreadsheetArtifactError> {
|
|
if let Some(id) = lookup.id {
|
|
return self
|
|
.charts
|
|
.iter()
|
|
.find(|chart| chart.id == id)
|
|
.ok_or_else(|| SpreadsheetArtifactError::InvalidArgs {
|
|
action: action.to_string(),
|
|
message: format!("chart id `{id}` was not found"),
|
|
});
|
|
}
|
|
if let Some(index) = lookup.index {
|
|
return self.charts.get(index).ok_or_else(|| {
|
|
SpreadsheetArtifactError::IndexOutOfRange {
|
|
action: action.to_string(),
|
|
index,
|
|
len: self.charts.len(),
|
|
}
|
|
});
|
|
}
|
|
Err(SpreadsheetArtifactError::InvalidArgs {
|
|
action: action.to_string(),
|
|
message: "chart id or index is required".to_string(),
|
|
})
|
|
}
|
|
|
|
pub fn create_chart(
|
|
&mut self,
|
|
action: &str,
|
|
chart_type: SpreadsheetChartType,
|
|
source_sheet_name: Option<String>,
|
|
source_range: &CellRange,
|
|
options: SpreadsheetChartCreateOptions,
|
|
) -> Result<u32, SpreadsheetArtifactError> {
|
|
if source_range.width() < 2 {
|
|
return Err(SpreadsheetArtifactError::InvalidArgs {
|
|
action: action.to_string(),
|
|
message: "chart source range must include at least two columns".to_string(),
|
|
});
|
|
}
|
|
let id = if let Some(id) = options.id {
|
|
if self.charts.iter().any(|chart| chart.id == id) {
|
|
return Err(SpreadsheetArtifactError::InvalidArgs {
|
|
action: action.to_string(),
|
|
message: format!("chart id `{id}` already exists"),
|
|
});
|
|
}
|
|
id
|
|
} else {
|
|
self.charts.iter().map(|chart| chart.id).max().unwrap_or(0) + 1
|
|
};
|
|
let series = (source_range.start.column + 1..=source_range.end.column)
|
|
.enumerate()
|
|
.map(|(index, value_column)| SpreadsheetChartSeries {
|
|
id: index as u32 + 1,
|
|
name: None,
|
|
category_sheet_name: source_sheet_name.clone(),
|
|
category_range: CellRange::from_start_end(
|
|
source_range.start,
|
|
CellAddress {
|
|
column: source_range.start.column,
|
|
row: source_range.end.row,
|
|
},
|
|
)
|
|
.to_a1(),
|
|
value_sheet_name: source_sheet_name.clone(),
|
|
value_range: CellRange::from_start_end(
|
|
CellAddress {
|
|
column: value_column,
|
|
row: source_range.start.row,
|
|
},
|
|
CellAddress {
|
|
column: value_column,
|
|
row: source_range.end.row,
|
|
},
|
|
)
|
|
.to_a1(),
|
|
})
|
|
.collect::<Vec<_>>();
|
|
self.charts.push(SpreadsheetChart {
|
|
id,
|
|
chart_type,
|
|
source_sheet_name,
|
|
source_range: Some(source_range.to_a1()),
|
|
title: options.title,
|
|
style_index: 102,
|
|
display_blanks_as: "gap".to_string(),
|
|
legend: SpreadsheetChartLegend {
|
|
visible: options.legend_visible.unwrap_or(true),
|
|
position: options
|
|
.legend_position
|
|
.unwrap_or(SpreadsheetChartLegendPosition::Bottom),
|
|
overlay: false,
|
|
},
|
|
category_axis: SpreadsheetChartAxis {
|
|
linked_number_format: true,
|
|
},
|
|
value_axis: SpreadsheetChartAxis {
|
|
linked_number_format: true,
|
|
},
|
|
series,
|
|
});
|
|
Ok(id)
|
|
}
|
|
|
|
pub fn add_chart_series(
|
|
&mut self,
|
|
action: &str,
|
|
lookup: SpreadsheetChartLookup,
|
|
mut series: SpreadsheetChartSeries,
|
|
) -> Result<u32, SpreadsheetArtifactError> {
|
|
validate_chart_series(action, &series)?;
|
|
let chart = self.get_chart_mut(action, lookup)?;
|
|
let next_id = chart.series.iter().map(|entry| entry.id).max().unwrap_or(0) + 1;
|
|
series.id = next_id;
|
|
chart.series.push(series);
|
|
Ok(next_id)
|
|
}
|
|
|
|
pub fn delete_chart(
|
|
&mut self,
|
|
action: &str,
|
|
lookup: SpreadsheetChartLookup,
|
|
) -> Result<(), SpreadsheetArtifactError> {
|
|
let index = if let Some(id) = lookup.id {
|
|
self.charts
|
|
.iter()
|
|
.position(|chart| chart.id == id)
|
|
.ok_or_else(|| SpreadsheetArtifactError::InvalidArgs {
|
|
action: action.to_string(),
|
|
message: format!("chart id `{id}` was not found"),
|
|
})?
|
|
} else if let Some(index) = lookup.index {
|
|
if index >= self.charts.len() {
|
|
return Err(SpreadsheetArtifactError::IndexOutOfRange {
|
|
action: action.to_string(),
|
|
index,
|
|
len: self.charts.len(),
|
|
});
|
|
}
|
|
index
|
|
} else {
|
|
return Err(SpreadsheetArtifactError::InvalidArgs {
|
|
action: action.to_string(),
|
|
message: "chart id or index is required".to_string(),
|
|
});
|
|
};
|
|
self.charts.remove(index);
|
|
Ok(())
|
|
}
|
|
|
|
pub fn set_chart_properties(
|
|
&mut self,
|
|
action: &str,
|
|
lookup: SpreadsheetChartLookup,
|
|
properties: SpreadsheetChartProperties,
|
|
) -> Result<(), SpreadsheetArtifactError> {
|
|
let chart = self.get_chart_mut(action, lookup)?;
|
|
if let Some(title) = properties.title {
|
|
chart.title = Some(title);
|
|
}
|
|
if let Some(visible) = properties.legend_visible {
|
|
chart.legend.visible = visible;
|
|
}
|
|
if let Some(position) = properties.legend_position {
|
|
chart.legend.position = position;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn validate_charts(&self, action: &str) -> Result<(), SpreadsheetArtifactError> {
|
|
for chart in &self.charts {
|
|
if let Some(source_range) = &chart.source_range {
|
|
let range = CellRange::parse(source_range)?;
|
|
if range.width() < 2 {
|
|
return Err(SpreadsheetArtifactError::InvalidArgs {
|
|
action: action.to_string(),
|
|
message: format!(
|
|
"chart `{}` source range `{source_range}` is too narrow",
|
|
chart.id
|
|
),
|
|
});
|
|
}
|
|
}
|
|
for series in &chart.series {
|
|
validate_chart_series(action, series)?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn get_chart_mut(
|
|
&mut self,
|
|
action: &str,
|
|
lookup: SpreadsheetChartLookup,
|
|
) -> Result<&mut SpreadsheetChart, SpreadsheetArtifactError> {
|
|
if let Some(id) = lookup.id {
|
|
return self
|
|
.charts
|
|
.iter_mut()
|
|
.find(|chart| chart.id == id)
|
|
.ok_or_else(|| SpreadsheetArtifactError::InvalidArgs {
|
|
action: action.to_string(),
|
|
message: format!("chart id `{id}` was not found"),
|
|
});
|
|
}
|
|
if let Some(index) = lookup.index {
|
|
let len = self.charts.len();
|
|
return self.charts.get_mut(index).ok_or_else(|| {
|
|
SpreadsheetArtifactError::IndexOutOfRange {
|
|
action: action.to_string(),
|
|
index,
|
|
len,
|
|
}
|
|
});
|
|
}
|
|
Err(SpreadsheetArtifactError::InvalidArgs {
|
|
action: action.to_string(),
|
|
message: "chart id or index is required".to_string(),
|
|
})
|
|
}
|
|
}
|
|
|
|
fn validate_chart_series(
|
|
action: &str,
|
|
series: &SpreadsheetChartSeries,
|
|
) -> Result<(), SpreadsheetArtifactError> {
|
|
let category_range = CellRange::parse(&series.category_range)?;
|
|
let value_range = CellRange::parse(&series.value_range)?;
|
|
if !category_range.is_single_column() || !value_range.is_single_column() {
|
|
return Err(SpreadsheetArtifactError::InvalidArgs {
|
|
action: action.to_string(),
|
|
message: "chart category and value ranges must be single-column ranges".to_string(),
|
|
});
|
|
}
|
|
if category_range.height() != value_range.height() {
|
|
return Err(SpreadsheetArtifactError::InvalidArgs {
|
|
action: action.to_string(),
|
|
message: "chart category and value series lengths must match".to_string(),
|
|
});
|
|
}
|
|
Ok(())
|
|
}
|