//! Maud HTML helpers for the charts UI.
use actix_web::HttpRequest;
use maud::{Markup, PreEscaped, html};
use crate::db::ColumnInfo;
use super::types::ChartType;
pub fn no_connection_html(req: &HttpRequest) -> Markup {
let content = html! {
div class="w-100 flex flex-column items-center justify-center" style="height:100%;" {
div class="bg-near-black ba b--white-20 br2 pa4 mw6 w-100 tc" {
p class="f4 white-70 mb3" { "No active database connection" }
a href="/connections"
hx-get="/connections"
hx-target="#feature"
hx-swap="innerHTML"
hx-push-url="true"
class="dib bg-orange white br2 ph3 pv2 no-underline f6 fw6 hover-bg-dark-orange" {
"Configure a connection"
}
}
}
};
if req.headers().get("HX-Request").is_some() {
content
} else {
crate::view::render_layout(&content)
}
}
pub fn select_class() -> &'static str {
"input-reset ba b--white-30 ph2 pv1 br2 f7 bg-white-10 white"
}
pub fn option_style() -> &'static str {
"background-color: #1a1a1a; color: white;"
}
/// CSS for the chart-area loading overlay. Tachyons doesn't ship anything
/// for `.htmx-indicator`, so we inline a tiny stylesheet once on the panel.
const CHART_INDICATOR_CSS: &str = r"
.chart-indicator {
position: absolute; inset: 0;
display: none;
align-items: center; justify-content: center;
background: rgba(18,18,18,0.55);
pointer-events: none;
}
.htmx-request .chart-indicator,
.chart-indicator.htmx-request { display: flex; }
.chart-spinner {
width: 36px; height: 36px;
border: 3px solid rgba(255,255,255,0.15);
border-top-color: #E07000;
border-radius: 50%;
animation: chart-spin 0.8s linear infinite;
}
@keyframes chart-spin { to { transform: rotate(360deg); } }
";
/// Renders the chart SVG + data-point count badge + loading overlay.
///
/// The overlay is wired to HTMX via `hx-indicator="#chart-area"`, which adds
/// the `htmx-request` class while a request is in flight.
pub fn chart_area(svg: &str, data_len: usize, error: Option<&str>) -> Markup {
html! {
div id="chart-area"
class="w-100 flex-auto flex flex-column relative"
style="overflow:hidden;" {
@if let Some(err) = error {
div class="pa3 bg-dark-red br2 mb2 f7 white" { (err) }
}
div class="f7 white-50 mb1 tr pr2" {
(data_len) " data points"
}
div class="w-100 flex-auto" style="overflow:hidden;" {
(PreEscaped(svg))
}
div class="chart-indicator" aria-hidden="true" {
div class="chart-spinner" {}
}
}
}
}
/// Full chart panel: controls + chart area. Returned when a table is selected.
#[allow(clippy::too_many_arguments)]
pub fn chart_panel(
cols: &[ColumnInfo],
chart_type: &ChartType,
x_col: &str,
y_col: &str,
limit: usize,
database: &str,
table: &str,
svg: &str,
data_len: usize,
error: Option<&str>,
) -> Markup {
let is_histogram = *chart_type == ChartType::Histogram;
let y_style = if is_histogram {
"opacity:0.35; pointer-events:none;"
} else {
""
};
// Common HTMX attributes for every control. Triggers debounce briefly to
// avoid firing a fetch on every keystroke / quick click.
let trigger = "change delay:200ms";
html! {
style { (PreEscaped(CHART_INDICATOR_CSS)) }
// ── Controls bar ──────────────────────────────────────────────────
div id="chart-controls"
class="flex flex-wrap items-end bg-black-70 ph3 pv2 bb b--white-10"
style="flex-shrink:0; gap:0.5rem;" {
// Chart type
div {
label class="db f7 white-50 mb1" { "Chart type" }
select
name="chart_type"
class=(select_class())
style="color:white;"
hx-get="/charts/render"
hx-include="#chart-form"
hx-target="#chart-area"
hx-swap="outerHTML"
hx-indicator="#chart-area"
hx-disabled-elt="this"
hx-trigger=(trigger) {
@for ct in ChartType::all() {
option
value=(ct.value())
selected[ct == chart_type]
style=(option_style()) {
(ct.label())
}
}
}
}
// X column
div {
label class="db f7 white-50 mb1" { "X axis / column" }
select
name="x_col"
class=(select_class())
style="color:white;"
hx-get="/charts/render"
hx-include="#chart-form"
hx-target="#chart-area"
hx-swap="outerHTML"
hx-indicator="#chart-area"
hx-disabled-elt="this"
hx-trigger=(trigger) {
@for c in cols {
option
value=(c.name)
selected[c.name == x_col]
style=(option_style()) {
(c.name) " (" (c.column_type) ")"
}
}
}
}
// Y column (dimmed for histogram)
div style=(y_style) {
label class="db f7 white-50 mb1" { "Y axis / value" }
select
name="y_col"
class=(select_class())
style="color:white;"
hx-get="/charts/render"
hx-include="#chart-form"
hx-target="#chart-area"
hx-swap="outerHTML"
hx-indicator="#chart-area"
hx-disabled-elt="this"
hx-trigger=(trigger) {
@for c in cols {
option
value=(c.name)
selected[c.name == y_col]
style=(option_style()) {
(c.name) " (" (c.column_type) ")"
}
}
}
}
// Row limit
div {
label class="db f7 white-50 mb1" { "Limit" }
select
name="limit"
class=(select_class())
style="color:white;"
hx-get="/charts/render"
hx-include="#chart-form"
hx-target="#chart-area"
hx-swap="outerHTML"
hx-indicator="#chart-area"
hx-disabled-elt="this"
hx-trigger=(trigger) {
@for &l in &[100usize, 500, 1000, 5000, 10000] {
option value=(l) selected[l == limit] style=(option_style()) { (l) }
}
}
}
}
// Hidden form fields so hx-include picks them all up together
form id="chart-form" style="display:none;" {
input type="hidden" name="database" value=(database);
input type="hidden" name="table" value=(table);
}
// Chart area
(chart_area(svg, data_len, error))
}
}