Skip to content

Alternate Views for Result Sets ​

Once a SQL statement returns a result set, the default view is the grid (see Result grid). But the grid isn't always the best lens — staring at a hundred rows of (month, revenue) to spot a trend is a thousand times harder than glancing at a line chart. SkylerX packs a set of alternate views into the result toolbar that re-render the same in-memory result without rerunning SQL.

This page explains: when an alternate view beats the grid, how each view computes, what data shape it needs, and what artifacts you can save.

When alternate views beat the grid ​

Data shapeRecommended viewTypical scenario
One categorical + one numeric columnBar / pie / donutSales by city, errors by endpoint
One time + one numeric column (continuous)Line / areaDAU trend, CPU usage
Two numeric columns (correlation)ScatterUser activity vs retention
Three categorical / numeric columnsPivotChannel × month = revenue
Two columns (lat, lng)Geo scatterStore distribution, user map
Time column + label columnTimelineDeploys, order lifecycle
(id, parent_id, ...)Self-FK treeThreaded comments, org charts
Multiple historic snapshots of the same rowRow historyAudit-table traceback

Toolbar triggers (packages/ui/src/components/ResultGrid.vue:1202-1215):

vue
<button :disabled="!result?.rows.length" @click="chartOpen = true">📊 Chart</button>
<div class="menu-box">
  <button @click="showViewMenu = !showViewMenu">📐 View</button>
  <!-- popup menu -->
  <button @click="altView = 'pivot'">⊞ Pivot</button>
  <button @click="altView = 'tree'">🌳 Tree</button>
  <button @click="altView = 'geo'">🗺 Geo</button>
  <button @click="altView = 'timeline'">⏱ Timeline</button>
</div>

All these views open in modals and return to the grid when closed — they're a magnifying glass on the grid, not a replacement.

1. Chart view (bar / line / pie + 4 extras) ​

packages/ui/src/components/ChartDialog.vue, 630 lines. Trigger: 📊 Chart.

Design choices ​

The code comments are refreshingly honest:

No ECharts; SVG charts hand-rolled (a couple hundred lines each for bar / line + pie). Reasons:

  • Desktop app bundle size matters; charts are a result-grid utility, not the headline
  • These three types cover 90% of ad-hoc data inspection; we can graduate to ECharts later
  • SVG renders cleanly to PNG (toDataURL via <canvas>)

All seven chart types are hand-written SVG:

ChartBest forCapNotes
📊 BarCategorical comparisonFirst 50 rowsY-axis ceiling auto-rounded
📈 LineTrend / time seriesFirst 200 rowsM / L path
🥧 PieShare / compositionFirst 50 rowsAuto percentage labels
⛰ AreaTrend with magnitudeFirst 200 rowsLine closed down to baseline
·· ScatterPoint cloudFirst 200 rowsDots
⭕ DonutComposition variantFirst 50 rowsOuter r * 1.0, inner r * 0.55
📡 RadarMulti-dimensionalFirst 50 rows, ≥ 3 pointsOne axis per row

Column picker ​

Three selectors in the top bar: Label (any column, .toString()), Value (auto-detected numeric columns; non-numeric ones get (?) suffix), Type. isNumericColumn samples the first 20 rows with Number.isFinite(Number(v)). Default Y = first numeric column. The selectors watch and reset when the result changes.

Row handling: rows where Number(v) is NaN are skipped; overflow past the row cap takes only the first N (bar / pie 50; line / area / scatter 200; radar 50).

Y-axis ​

For a clean axis, the ceiling is Math.ceil(m / 10^floor(log10(m))) * 10^floor(log10(m)). Tick labels are formatted as B / M / k (above 1e9 / 1e6 / 1e4).

Output: PNG export ​

Top-right ⬇ Export PNG → XMLSerializer serializes the SVG → <canvas> draws it at 2× HiDPI (dark #1d1e22 background) → canvas.toBlob('image/png') → routed through the custom SaveFileDialog. File name chart-{kind}-{ts}.png, resolution 1440×720 — fits straight into Lark / Slack.

2. Pivot table (PivotDialog) ​

packages/ui/src/components/PivotDialog.vue, 162 lines. Trigger: 📐 View → ⊞ Pivot.

The pitch: pivot the current result in memory without rerunning SQL. The algorithm is straightforward — group rows by (rowFields...), then bucket each group by colField, then agg per bucket.

Three axes + one aggregator ​

ControlBehavior
Rows (multi-select chips)Group by these columns; key joined with '|'
Columns (dropdown)Each distinct value becomes a header column (lexicographic order)
Value + aggregatorPer (row, col) cell, aggregate this column
Aggregator dropdownCOUNT / SUM / AVG / MIN / MAX

Algorithm ​

Two nested maps Map<rowKey, Map<colKey, number[]>>: one pass over result.rows; rowKey is the |-joined string of rowFields; colKey is the stringified colField; Number(row[valueField]) lands in the array. NULL is normalized to the literal 'NULL' (grouped together). COUNT uses length; others use numeric aggregation.

Limitations ​

The code comments admit it:

Not supported: multiple value fields, ordered column names (pivot columns are lexicographic), filtering — possibly next release.

So "sort months 1-12 instead of 10, 11, 12, 1, 2..." isn't possible yet. Work around it in SQL by zero-padding ('01' / '02' / ...).

Output ​

This is just a transient table view — no direct export. To persist:

  • Close pivot → back to grid → right-click copy → CSV / Markdown into Excel / Notion
  • Rewrite the pivot in SQL: MySQL GROUP BY x WITH ROLLUP / PG crosstab()

3. Geo scatter (GeoMapDialog) ​

packages/ui/src/components/GeoMapDialog.vue, 138 lines. Trigger: 📐 View → 🗺 Geo.

No leaflet, no basemap — SVG dots drawn straight from (lng, lat). The code comments explain:

Projection: equirectangular (Mercator's visual distortion is small for local data; raw lng/lat is plenty without a heavy CRS). Not done: basemap (no tiles), clustering (dense points smear but pan/zoom helps).

Auto column detection ​

ts
latCol = cols.find(c => /^(lat|latitude|y)$/i.test(c)) ?? cols[0]
lngCol = cols.find(c => /^(lng|lon|long|longitude|x)$/i.test(c)) ?? cols[1]
labelCol = cols.find(c => /^(name|title|label|id)$/i.test(c)) ?? ''

Strict numeric guards (to filter junk data):

ts
if (!Number.isFinite(lat) || !Number.isFinite(lng)) continue
if (lat < -90 || lat > 90 || lng < -180 || lng > 180) continue

Auto-framing ​

Not a full world map — the bounds "just enclose all points with 5% padding":

ts
const dx = Math.max(0.001, (maxX - minX) * 0.05)
return { minX: minX - dx, maxX: maxX + dx, ... }

Corner lng/lat labels appear on the SVG edges. Hovering a point shows lat=... lng=....

Output ​

Visual browse only — no PNG export (possibly next release). For a persistable visualization, add a category column to the SQL and use the scatter chart view.

Data shape ​

Recognized columnsExample
lat, latitude, ylatitude FLOAT
lng, lon, long, longitude, xlng DECIMAL(9,6)
name, title, label, id (label, optional)store_name VARCHAR

Non-standard names work too — pick them manually as long as values are numeric and in range.

4. Timeline (TimelineDialog) ​

packages/ui/src/components/TimelineDialog.vue, 171 lines. Trigger: 📐 View → ⏱ Timeline.

Auto column detection ​

ts
timeCol = cols.find(c => /at$|_time$|date|time|created|updated/i.test(c)) ?? cols[0]
labelCol = cols.find(c => /^(name|title|label|id|user|action)$/i.test(c)) ?? ''
colorCol = ''   // optional: color points by this column

Catches created_at / updated_at / event_time / order_date / login_time etc. by default.

Time parsing (toMs) ​

Handles four formats:

ts
function toMs(v: unknown): number | null {
  if (v instanceof Date) return v.getTime()
  if (typeof v === 'number') return v > 1e12 ? v : v * 1000   // ms or s heuristic
  const ms = Date.parse(String(v))  // ISO / "YYYY-MM-DD HH:MM:SS"
  return Number.isNaN(ms) ? null : ms
}

Numbers below 1e12 (year 2001) are treated as Unix seconds (×1000); above that as ms. Business timestamps work fine; rare pre-1969 timestamps get misclassified — if your data has those, convert to strings with to_char(...) in SQL first.

Rendering ​

Horizontal timeline; points are staggered up/down to avoid overlap (i % 2 === 0 ? -16 : +16); the X axis has 5 evenly spaced date labels.

If a color column is set, distinct values use an 8-color palette in order (#7c6cff / #4caf50 / #e0a020 / #e04050 / #3aa1ff / #b48cff / #67c23a / #ff9966) and a legend appears below. Hovering a point shows time · label in the info bar.

Data shape ​

At minimum one time column (any Date / ISO / Unix seconds or ms). Label / Color are optional.

5. Self-referencing FK tree (TreeViewDialog) ​

packages/ui/src/components/TreeViewDialog.vue, 130 lines. Trigger: 📐 View → 🌳 Tree.

Good for self-referencing FKs or hierarchical data: nested comments (comments.parent_id → comments.id), org departments (departments.parent_dept_id → id), administrative regions (regions.parent_id).

Three axes ​

SelectorInference rule
idMatch /^id$/i first, else the first column
parentMatch /parent[_-]?id|pid/i, blank by default
labelMatch /^(name|title|label)$/i, else fall back to id

Algorithm ​

Two passes: first builds an id index (byId: Map<id, node>), second wires children to parents. A parent id missing from the index (including NULL) means root. parent === self is treated as root (defending against rogue rows like WHERE id=1 AND parent_id=1).

Cycle detection ​

walk(n, depth) does DFS with a Set<string> of visited ids; if it revisits one it sets n.cycle = true and stops. A yellow ⚠ appears next to the node and a "cycle" tooltip shows on hover. Common after ops mistakes (what should be parent-child becomes a loop).

Rendering ​

Flattened, each node indented by depth * 18px, displayed as ▸ <label> #<id>. Hovering the label shows the full JSON via title="{json}" for quick visual inspection.

Data shape ​

At minimum id + parent. A single SELECT id, parent_id, name FROM comments WHERE post_id = 1234 retrieves the whole tree at once and the view renders it.

6. Row history (RowHistoryDialog) ​

packages/ui/src/components/RowHistoryDialog.vue, 123 lines.

The pitch: single-row version traceback — given a row's PK in some table, find all versions in audit / *_history / *_log shadow tables.

Auto shadow-table discovery ​

On open, runs SELECT table_name FROM information_schema.tables WHERE table_name LIKE '{base}_%' OR table_name = 'audit_{base}' OR table_name = '{base}_history'. Candidates fill a <datalist> dropdown — pick one or type your own.

History query ​

Once a shadow table is set, queries by PK: SELECT * FROM {shadow} WHERE {pk}=... ORDER BY changed_at, updated_at, created_at, version, revision DESC LIMIT 200. ORDER BY lists five candidate columns at once — the database uses whichever exists (MySQL is tolerant, PG is strict; most audit tables have at least one). Result shown as a compact mini-table, each cell truncated to 80 chars.

Data shape ​

A business table + a *_history / *_audit / *_log shadow table (PK + duplicated business columns + a changed_at / version field). Standard audit-trigger implementations satisfy this.

Implementation note: the dialog is already wired up (Workspace.vue has rowHistOpen state and the modal mount), but there isn't a right-click entry from the result grid yet — it's reserved for a future menu extension.

7. Data lineage (LineageDialog) — heuristic version ​

packages/ui/src/components/LineageDialog.vue, 98 lines.

The code comment is upfront:

Column lineage (heuristic version): no real SQL parser yet. Crude heuristic — scan historical SQL text for {table}.{column} or bare {column} (when the SQL FROMs {table}) and call those related. Limited accuracy: misses (aliases / subqueries) and false positives (same-name columns). The UI clearly labels this "heuristic" — we'll replace it with a real lineage analyzer when a SQL parser ships.

Algorithm ​

Pulls the most recent 500 history SQL entries for this connection and matches each with \b{table}\b + \b{column}\b word-boundary regexes. On match, looks at the leading keyword: INSERT / UPDATE → sinks (write); SELECT / WITH → sources (read).

Rendering ​

Two columns:

  • ← Sinks — SQL that writes to this column (INSERT / UPDATE)
  • → Sources — SQL that reads from this column (SELECT / WITH)

Each row shows execution time + the first 120 chars of the SQL. A yellow banner at the top warns "heuristic results, not for audit".

Data shape ​

Depends on the query history (client.connections.history). If you've never run a related query in SkylerX, the lineage window shows "No hits".

Implementation note: same as RowHistoryDialog — wired up in Workspace.vue, needs an external trigger (lineageOpen.value = {...}), no dedicated UI entry yet — reserved API.

Capability matrix ​

ViewAuto column detectionData capStatic exportRe-runs SQLBest for
Charts (7 kinds)Numeric column sniff50 / 200 rowsPNG (2× HiDPI)NoMagnitude / trend / composition
PivotFirst / second / third columnBrowser memoryCopy as CSVNoTwo-axis cross aggregation
Geo scatterlat / lng / x / y aliasesNoneNoNoLat/lng direct draw
Timelineat$ / time / date / created suffixesNoneNoNoEvent stream + categorical color
Treeid / parent_id / nameNoneNoNoSelf-FK hierarchy
Row historyHeuristic *_history / *_audit200 rows (SQL LIMIT)No✓ (audit table)Single-row traceback
Lineage—500 history entriesNoNoColumn read/write (heuristic)

Trigger summary ​

ViewEntryNotes
ChartResult toolbar 📊 ChartOpens to bar by default
Pivot / Tree / Geo / TimelineResult toolbar 📐 View → popup menuSame modal, shared altView state
Row historyrowHistOpen.value = { conn, table, pk }Reserved, awaits right-click menu
LineagelineageOpen.value = { conn, table, column }Reserved, awaits right-click menu

Closing a modal returns to the grid — pagination / sort state is preserved. Alternate views are a magnifying glass on top of the grid, not a replacement.

A tiny decision tree ​

Looking at magnitude / ranking / trend / composition? → Chart

  • Magnitude vs time → Line / Area
  • Categorical ranking → Bar
  • Composition → Pie / Donut
  • Multi-dimensional → Radar

Looking at a two-axis cross (e.g. "channel × month")? → Pivot

Got (lat, lng)? → Geo scatter

Got a time column:

  • Continuous time series (daily DAU) → Line
  • Discrete events (deploys, releases, alerts) → Timeline

Self-FK data → Tree

Want one row's history? → Row history

Want who reads / writes this column? → Lineage (heuristic, use with caution)

That covers every alternate view at the result-set layer. If your data shape doesn't fit any of these, 90% of the time a quick SQL rewrite makes it fit — failing that, drop back to the grid, copy to Excel / Numbers / Notion, and continue there.

To look at the SQL itself (slow log, EXPLAIN, index recommendations), see Advanced features; for import/export and migration, see Database support.

Apache License 2.0 · Wuhan Skyler Network Technology Co., Ltd.