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: 2 additions & 2 deletions skills/ducklocal/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ plot "revenue" {
}
```

A `query` block holds one `sql` attribute (one read-only statement — SELECT, WITH, FROM, VALUES, SHOW, DESCRIBE, SUMMARIZE or PIVOT — heredoc or string; DDL, DML, COPY, ATTACH and INSTALL are rejected). A `plot` block holds `type` (`line`, `bar`, `area`, `scatter`, `pie`, `map`, `table`), `query` (a `query.name` reference), `x` and `y` (result columns, bare identifiers or quoted strings; `y` optional for `table`), optional `series` and `title`. A `pie` takes `x` as its slices and `y` as their sizes, and no `series`; it folds past eight slices into "other". A `map` takes `lat` and `lng` (degrees) instead of `x`/`y`/`series` — either may be omitted when a numeric column's name says it, such as `geo_lat` or `longitude` — plus an optional `color` column; for example `plot "stations" { type = "map" query = query.stations lat = geo_lat lng = geo_lng color = type }`. No functions, conditionals, or interpolation exist. There is a working example at `examples/analysis_app/dashboard.dash`.
A `query` block holds one `sql` attribute (one read-only statement — SELECT, WITH, FROM, VALUES, SHOW, DESCRIBE, SUMMARIZE or PIVOT — heredoc or string; DDL, DML, COPY, ATTACH and INSTALL are rejected). A `plot` block holds `type` (`line`, `bar`, `area`, `scatter`, `pie`, `map`, `table`), `query` (a `query.name` reference), `x` and `y` (result columns, bare identifiers or quoted strings; `y` optional for `table`), optional `series` and `title`. A `pie` takes `x` as its slices and `y` as their sizes, and no `series`; it folds past eight slices into "other". A `map` takes `lat` and `lng` (degrees) instead of `x`/`y`/`series` — either may be omitted when a numeric column's name says it, such as `geo_lat` or `longitude` — plus an optional `color` column, an optional numeric `size` column (points' area follows the value; `size_scale = "log"` for values spanning orders of magnitude) and an optional `tooltip` list of the columns to show on hover (default: the size column and other numbers, then the coordinates); for example `plot "stations" { type = "map" query = query.stations lat = geo_lat lng = geo_lng color = type size = passengers tooltip = [name, passengers] }`. No functions, conditionals, or interpolation exist. There is a working example at `examples/analysis_app/dashboard.dash`.

Always validate before handing a spec over: `ducklocal check dashboard.dash`, or `ducklocal check dashboard.dash --database warehouse.duckdb` to also run every query read-only and verify every `x`/`y`/`series`/`lat`/`lng`/`color` against the columns the queries actually return (a non-numeric `y` is an error outside `table`, and so is a non-numeric `lat` or `lng`). A spec mistake is exit 2 with kind `spec`, one `file:line: message` per diagnostic; a query that fails on the database is exit 1 with kind `sql`, one line per failing query — fix all of them, not just the first. Prefer the `--database` form whenever the database exists: without it nothing runs, so errors that appear only at execution (a cast the build cannot perform, a value that will not convert) go unseen. To see it rendered, open the file in the GUI (`ducklocal dashboard.dash` or drag it onto the window): it becomes a dashboard tab, a resizable vertical stack of the plots with per-plot inline errors.
Always validate before handing a spec over: `ducklocal check dashboard.dash`, or `ducklocal check dashboard.dash --database warehouse.duckdb` to also run every query read-only and verify every `x`/`y`/`series`/`lat`/`lng`/`color`/`size`/`tooltip` column against the columns the queries actually return (a non-numeric `y` is an error outside `table`, and so is a non-numeric `lat`, `lng` or `size`). A spec mistake is exit 2 with kind `spec`, one `file:line: message` per diagnostic; a query that fails on the database is exit 1 with kind `sql`, one line per failing query — fix all of them, not just the first. Prefer the `--database` form whenever the database exists: without it nothing runs, so errors that appear only at execution (a cast the build cannot perform, a value that will not convert) go unseen. To see it rendered, open the file in the GUI (`ducklocal dashboard.dash` or drag it onto the window): it becomes a dashboard tab, a resizable vertical stack of the plots with per-plot inline errors.

## Authoring an app

Expand Down
6 changes: 6 additions & 0 deletions src/i18n.rs
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,12 @@ static STRINGS: &[(&str, &str, &str)] = &[
// ── src/ui/geo.rs ───────────────────────────────────────────────────
("chart.map.title", "地图({} / {})", "Map of {} / {}"),
("chart.map.colored_by", "按 {} 着色", "Colored by {}"),
("chart.map.sized_by", "按 {} 定大小", "Sized by {}"),
(
"chart.map.notice.unsized",
"{} 个点没有可用的大小值,按最小绘制",
"{} points have no usable size value; drawn smallest",
),
("chart.map.other", "其他", "Other"),
("chart.map.row", "第 {} 行", "Row {}"),
("chart.map.no_label", "(结果中没有名称列)", "(no name column in the result)"),
Expand Down
45 changes: 38 additions & 7 deletions src/spec/complete.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,20 @@ use super::model;

/// Every attribute a plot block may hold, in the order completion offers
/// them: the common ones first, a map's own after.
pub(crate) const PLOT_ATTRS: [&str; 9] =
["type", "query", "x", "y", "series", "title", "lat", "lng", "color"];
pub(crate) const PLOT_ATTRS: [&str; 12] = [
"type",
"query",
"x",
"y",
"series",
"title",
"lat",
"lng",
"color",
"size",
"size_scale",
"tooltip",
];

/// One thing that could be inserted at the cursor.
#[derive(Debug, Clone, PartialEq)]
Expand Down Expand Up @@ -81,13 +93,23 @@ pub(crate) fn complete(source: &str, line: usize, col: usize) -> Vec<Completion>
.collect();
}

if let Some(quoted) = after_type_equals(&before) {
return model::PLOT_TYPES
// The two attributes whose value is one of a fixed set of strings.
let choices = [
("type", model::PLOT_TYPES, "plot type"),
("size_scale", model::SIZE_SCALES, "size scale"),
];
if let Some((quoted, values, detail)) = choices
.iter()
.find_map(|(attr, values, detail)| {
after_attr_equals(&before, attr).map(|quoted| (quoted, *values, *detail))
})
{
return values
.iter()
.map(|ty| Completion {
label: ty.to_string(),
kind: CompletionKind::Value,
detail: Some("plot type".to_string()),
detail: Some(detail.to_string()),
// Inside an open quote the bare word; outside one, bring the
// quotes — the value is a string either way.
insert_text: if quoted {
Expand Down Expand Up @@ -176,7 +198,7 @@ fn after_query_dot(before: &str) -> bool {

/// The cursor follows `type =`, optionally inside an opened quote. The answer
/// is whether the quote is there, so the insert text knows to bring its own.
fn after_type_equals(before: &str) -> Option<bool> {
fn after_attr_equals(before: &str, attr: &str) -> Option<bool> {
let before = before.trim_end();
let ident_len = trailing_ident_len(before);
let head = &before[..before.len() - ident_len];
Expand All @@ -185,7 +207,7 @@ fn after_type_equals(before: &str) -> Option<bool> {
None => (head, false),
};
let head = head.trim_end().strip_suffix('=')?;
(head.trim() == "type").then_some(quoted)
(head.trim() == attr).then_some(quoted)
}

/// Length of the ASCII identifier a line ends in — the partial word being
Expand Down Expand Up @@ -338,6 +360,15 @@ mod tests {
assert_eq!(labels(&candidates), ["sql"]);
}

#[test]
fn size_scales_after_size_scale_equals() {
let candidates = complete("plot \"p\" {\n size_scale = \n}", 2, 16);
assert_eq!(labels(&candidates), model::SIZE_SCALES);
// Outside a quote the value comes quoted: it is a string.
assert_eq!(candidates[1].insert_text, "\"log\"");
assert_eq!(candidates[0].detail.as_deref(), Some("size scale"));
}

#[test]
fn plot_types_after_type_equals() {
// Inside an opened quote the insert text is the bare word.
Expand Down
5 changes: 4 additions & 1 deletion src/spec/lsp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,7 @@ fn hover_at(source: &str, position: Position) -> Option<Hover> {
Hit::Attr { name, .. } => attr_doc(&name)?.to_string(),
Hit::Block { kind, name, .. } => match kind.as_str() {
"query" => format!("**query \"{name}\"**\n\nA named query: one `sql` attribute holding one statement, as a heredoc or a string."),
"plot" => format!("**plot \"{name}\"**\n\nA named plot: `type`, `query`, `x` and `y`, plus optional `series` and `title`; a `map` takes `lat`, `lng` and `color` instead of `x`, `y` and `series`."),
"plot" => format!("**plot \"{name}\"**\n\nA named plot: `type`, `query`, `x` and `y`, plus optional `series` and `title`; a `map` takes `lat`, `lng` and optional `color`, `size`, `size_scale` and `tooltip` instead of `x`, `y` and `series`."),
_ => return None,
},
Hit::Ref { segments, .. } => {
Expand Down Expand Up @@ -470,6 +470,9 @@ fn attr_doc(name: &str) -> Option<&'static str> {
"lat" => "**lat**\n\nA `map`'s latitude column, in degrees. Optional when a column's name says it (`lat`, `geo_lat`, `latitude`).",
"lng" => "**lng**\n\nA `map`'s longitude column, in degrees. Optional when a column's name says it (`lng`, `lon`, `longitude`).",
"color" => "**color**\n\nOptional, for a `map`: the column its points are colored by; the five most common values get a color, the rest share one.",
"size" => "**size**\n\nOptional, for a `map`: a numeric column its points are sized by — the area follows the value, so twice the value is twice the ink. A key in the corner reads sizes back.",
"size_scale" => "**size_scale**\n\nOptional, with `size`: `\"sqrt\"` (the default, area follows value) or `\"log\"`, for values spanning orders of magnitude.",
"tooltip" => "**tooltip**\n\nOptional, for a `map`: the columns its tooltip lists, in order — `tooltip = [place, requests, ips]`. Left out, it lists the size column and the other numbers, then the coordinates.",
"title" => "**title**\n\nOptional: the plot's title.",
_ => return None,
})
Expand Down
3 changes: 3 additions & 0 deletions src/spec/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,9 @@ pub fn check(args: &[OsString]) -> Result<String, CliError> {
"lat": p.lat,
"lng": p.lng,
"color": p.color,
"size": p.size,
"size_scale": p.size_scale,
"tooltip": p.tooltip,
"title": p.title,
"line": p.line,
})).collect::<Vec<_>>(),
Expand Down
Loading
Loading