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 PNGXMLSerializer 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
Lineage500 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.