///|
/// A single point in a generated chart preview.
pub(all) struct CsvChartPoint {
label : String
value : Double
count : Int
} derive(Eq, Debug)
///|
/// Data model used by JSON, SVG, HTML, CLI, and browser chart previews.
pub(all) struct CsvChartSpec {
kind : String
title : String
label_column : String
value_column : String
measure : String
points : Array[CsvChartPoint]
skipped_rows : Int
warnings : Array[String]
} derive(Eq, Debug)
///|
/// Build an automatic bar-chart preview from CSV text.
pub fn chart_csv_spec(input : String) -> CsvChartSpec {
chart_csv_spec_kind(input, "bar")
}
///|
/// Build an automatic chart preview from CSV text.
///
/// Supported kinds are `bar`, `line`, and `pie`. Unknown values fall back to
/// `bar`. The function chooses a label column and a numeric value column when
/// possible; if no numeric column exists, it generates category counts.
pub fn chart_csv_spec_kind(input : String, kind : String) -> CsvChartSpec {
chart_auto_spec(parse_table_auto(input), kind)
}
///|
/// Build a chart preview from CSV text with explicit label and value columns.
pub fn chart_csv_spec_with_columns(
input : String,
kind : String,
label_column : String,
value_column : String,
) -> CsvChartSpec {
chart_table_spec(parse_table_auto(input), kind, label_column, value_column)
}
///|
/// Render an automatic chart spec as machine-readable JSON.
pub fn chart_csv_json(input : String, kind : String) -> String {
chart_spec_json(chart_csv_spec_kind(input, kind))
}
///|
/// Render a chart spec with explicit columns as machine-readable JSON.
pub fn chart_csv_json_with_columns(
input : String,
kind : String,
label_column : String,
value_column : String,
) -> String {
chart_spec_json(
chart_csv_spec_with_columns(input, kind, label_column, value_column),
)
}
///|
/// Render an automatic chart preview as standalone SVG.
pub fn chart_csv_svg(input : String, kind : String) -> String {
chart_spec_svg(chart_csv_spec_kind(input, kind))
}
///|
/// Render a chart preview with explicit columns as standalone SVG.
pub fn chart_csv_svg_with_columns(
input : String,
kind : String,
label_column : String,
value_column : String,
) -> String {
chart_spec_svg(
chart_csv_spec_with_columns(input, kind, label_column, value_column),
)
}
///|
/// Render an automatic chart preview as a standalone HTML document.
pub fn chart_csv_html(input : String, kind : String) -> String {
chart_spec_html(chart_csv_spec_kind(input, kind))
}
///|
/// Render a chart preview with explicit columns as a standalone HTML document.
pub fn chart_csv_html_with_columns(
input : String,
kind : String,
label_column : String,
value_column : String,
) -> String {
chart_spec_html(
chart_csv_spec_with_columns(input, kind, label_column, value_column),
)
}
///|
/// Build an automatic chart spec from a parsed table.
pub fn chart_auto_spec(table : CsvTable, kind : String) -> CsvChartSpec {
let normalized = chart_normalize_kind(kind)
let label_column = chart_pick_label_column(table)
match chart_pick_value_column(table) {
Some(value_column) =>
chart_table_spec(table, normalized, label_column, value_column)
None => chart_count_spec(table, normalized, label_column)
}
}
///|
/// Build a chart spec from explicit table columns. Duplicate labels are
/// aggregated by summing values in first-seen order.
pub fn chart_table_spec(
table : CsvTable,
kind : String,
label_column : String,
value_column : String,
) -> CsvChartSpec {
let normalized = chart_normalize_kind(kind)
let warnings : Array[String] = Array::new()
let points : Array[CsvChartPoint] = Array::new()
match
(
chart_column_index(table.headers, label_column),
chart_column_index(table.headers, value_column),
) {
(Some(label_index), Some(value_index)) => {
let labels : Array[String] = Array::new()
let values : Array[Double] = Array::new()
let counts : Array[Int] = Array::new()
let mut skipped_rows = 0
for row in table.rows {
let raw_label = chart_cell_at(row, label_index)
let label = if chart_trim_ascii(raw_label).is_empty() {
"(blank)"
} else {
raw_label
}
match chart_parse_double(chart_cell_at(row, value_index)) {
Some(value) =>
match chart_index_of(labels, label) {
Some(index) => {
values[index] += value
counts[index] += 1
}
None => {
labels.push(label)
values.push(value)
counts.push(1)
}
}
None => skipped_rows += 1
}
}
if skipped_rows > 0 {
warnings.push(
"\{skipped_rows} row(s) were skipped because `\{value_column}` was empty or non-numeric.",
)
}
chart_points_from_arrays(points, labels, values, counts, 24)
{
kind: normalized,
title: "sum of \{value_column} by \{label_column}",
label_column,
value_column,
measure: "sum",
points,
skipped_rows,
warnings,
}
}
(None, _) => {
warnings.push("Label column `\{label_column}` was not found.")
{
kind: normalized,
title: "chart preview",
label_column,
value_column,
measure: "sum",
points,
skipped_rows: table.rows.length(),
warnings,
}
}
(_, None) => {
warnings.push("Value column `\{value_column}` was not found.")
{
kind: normalized,
title: "chart preview",
label_column,
value_column,
measure: "sum",
points,
skipped_rows: table.rows.length(),
warnings,
}
}
}
}
///|
/// Build a category-count chart when no numeric column is available.
pub fn chart_count_spec(
table : CsvTable,
kind : String,
label_column : String,
) -> CsvChartSpec {
let normalized = chart_normalize_kind(kind)
let warnings : Array[String] = Array::new()
let points : Array[CsvChartPoint] = Array::new()
match chart_column_index(table.headers, label_column) {
Some(label_index) => {
let labels : Array[String] = Array::new()
let values : Array[Double] = Array::new()
let counts : Array[Int] = Array::new()
for row in table.rows {
let raw_label = chart_cell_at(row, label_index)
let label = if chart_trim_ascii(raw_label).is_empty() {
"(blank)"
} else {
raw_label
}
match chart_index_of(labels, label) {
Some(index) => {
values[index] += 1.0
counts[index] += 1
}
None => {
labels.push(label)
values.push(1.0)
counts.push(1)
}
}
}
warnings.push(
"No numeric column was found; generated a category-count chart.",
)
chart_points_from_arrays(points, labels, values, counts, 24)
{
kind: normalized,
title: "count by \{label_column}",
label_column,
value_column: "count",
measure: "count",
points,
skipped_rows: 0,
warnings,
}
}
None => {
warnings.push("No chartable columns were found.")
{
kind: normalized,
title: "chart preview",
label_column,
value_column: "count",
measure: "count",
points,
skipped_rows: table.rows.length(),
warnings,
}
}
}
}
///|
/// Render a chart spec as machine-readable JSON.
pub fn chart_spec_json(spec : CsvChartSpec) -> String {
let out = StringBuilder()
out.write_char('{')
out.write_string("\"kind\":")
chart_write_json_string(out, spec.kind)
out.write_string(",\"title\":")
chart_write_json_string(out, spec.title)
out.write_string(",\"label_column\":")
chart_write_json_string(out, spec.label_column)
out.write_string(",\"value_column\":")
chart_write_json_string(out, spec.value_column)
out.write_string(",\"measure\":")
chart_write_json_string(out, spec.measure)
out.write_string(",\"skipped_rows\":\{spec.skipped_rows}")
out.write_string(",\"points\":")
chart_write_points_json(out, spec.points)
out.write_string(",\"warnings\":")
chart_write_string_array_json(out, spec.warnings)
out.write_char('}')
out.to_string()
}
///|
/// Render a chart spec as standalone SVG.
pub fn chart_spec_svg(spec : CsvChartSpec) -> String {
if spec.kind == "line" {
chart_line_svg(spec)
} else if spec.kind == "pie" {
chart_pie_svg(spec)
} else {
chart_bar_svg(spec)
}
}
///|
/// Render a chart spec as a standalone HTML document.
pub fn chart_spec_html(spec : CsvChartSpec) -> String {
let out = StringBuilder()
out.write_string("\n\n\n")
out.write_string(" \n")
out.write_string(" ")
chart_write_html_escaped(out, spec.title)
out.write_string(" \n")
out.write_string(" \n\n\n")
out.write_string(" ")
chart_write_html_escaped(out, spec.title)
out.write_string("
\n")
out.write_string(" Kind: ")
chart_write_html_escaped(out, spec.kind)
out.write_string(" · Label: ")
chart_write_html_escaped(out, spec.label_column)
out.write_string(" · Value: ")
chart_write_html_escaped(out, spec.value_column)
out.write_string(" · Measure: ")
chart_write_html_escaped(out, spec.measure)
out.write_string("
\n")
for warning in spec.warnings {
out.write_string(" ")
chart_write_html_escaped(out, warning)
out.write_string("
\n")
}
out.write_string(chart_spec_svg(spec))
out.write_string("\n \n")
out.write_string(" Label Value Rows \n")
for point in spec.points {
out.write_string(" ")
chart_write_html_escaped(out, point.label)
out.write_string(
" \{point.value} \{point.count} \n",
)
}
out.write_string("
\n")
out.write_string("\n")
out.to_string()
}
///|
fn chart_bar_svg(spec : CsvChartSpec) -> String {
let out = StringBuilder()
chart_write_svg_start(out, spec)
if spec.points.length() == 0 {
chart_write_empty_svg(out)
out.write_string("")
return out.to_string()
}
let left = 70.0
let top = 54.0
let plot_width = 660.0
let plot_height = 250.0
let bottom = top + plot_height
let min_value = chart_min_value(spec.points).unwrap_or(0.0)
let max_value = chart_max_value(spec.points).unwrap_or(0.0)
let y_min = if min_value > 0.0 { 0.0 } else { min_value }
let y_max = if max_value < 0.0 { 0.0 } else { max_value }
let zero_y = chart_scale_y(0.0, y_min, y_max, top, plot_height)
let slot = plot_width / spec.points.length().to_double()
let bar_width = chart_max_double(8.0, slot * 0.62)
out.write_string(
"",
)
out.write_string(
"",
)
chart_write_axis_labels(out, spec, y_min, y_max, left, top, plot_height)
for i in 0..")
chart_write_xml_escaped(out, "\{point.label}: \{point.value}")
out.write_string(" ")
chart_write_x_label(out, point.label, x + bar_width / 2.0, bottom + 18.0)
}
out.write_string("")
out.to_string()
}
///|
fn chart_line_svg(spec : CsvChartSpec) -> String {
let out = StringBuilder()
chart_write_svg_start(out, spec)
if spec.points.length() == 0 {
chart_write_empty_svg(out)
out.write_string("")
return out.to_string()
}
let left = 70.0
let top = 54.0
let plot_width = 660.0
let plot_height = 250.0
let bottom = top + plot_height
let min_value = chart_min_value(spec.points).unwrap_or(0.0)
let max_value = chart_max_value(spec.points).unwrap_or(0.0)
let y_min = if min_value == max_value { min_value - 1.0 } else { min_value }
let y_max = if min_value == max_value { max_value + 1.0 } else { max_value }
out.write_string(
"",
)
out.write_string(
"",
)
chart_write_axis_labels(out, spec, y_min, y_max, left, top, plot_height)
out.write_string(
" 0 {
out.write_char(' ')
}
let x = chart_line_x(i, spec.points.length(), left, plot_width)
let y = chart_scale_y(spec.points[i].value, y_min, y_max, top, plot_height)
out.write_string("\{x},\{y}")
}
out.write_string("\"/>")
for i in 0..")
chart_write_xml_escaped(out, "\{point.label}: \{point.value}")
out.write_string(" ")
chart_write_x_label(out, point.label, x, bottom + 18.0)
}
out.write_string("")
out.to_string()
}
///|
fn chart_pie_svg(spec : CsvChartSpec) -> String {
let out = StringBuilder()
chart_write_svg_start(out, spec)
let total = chart_positive_total(spec.points)
if spec.points.length() == 0 || total <= 0.0 {
chart_write_empty_svg(out)
out.write_string("")
return out.to_string()
}
let cx = 220.0
let cy = 190.0
let radius = 88.0
let circumference = 552.92
let mut offset = 0.0
out.write_string(
"",
)
for i in 0.. 0.0 {
let dash = point.value / total * circumference
let rest = circumference - dash
out.write_string(
"",
)
chart_write_xml_escaped(out, "\{point.label}: \{point.value}")
out.write_string(" ")
offset += dash
}
}
out.write_string(
"",
)
out.write_string(
"",
)
chart_write_xml_escaped(out, spec.measure)
out.write_string(" ")
out.write_string(
"total \{total} ",
)
let legend_x = 390.0
let mut legend_y = 94.0
for i in 0.. ")
out.write_string(
"",
)
chart_write_xml_escaped(out, "\{point.label}: \{point.value}")
out.write_string(" ")
legend_y += 22.0
}
out.write_string("")
out.to_string()
}
///|
fn chart_write_svg_start(out : StringBuilder, spec : CsvChartSpec) -> Unit {
out.write_string(
"