Skip to content

Vistas alternativas para el resultado ​

Tras ejecutar un SQL obtienes un resultado y, por defecto, lo ves como una cuadrícula (ver Cuadrícula de resultados). Pero a menudo la cuadrícula no es la mejor forma de mirarlo: cien filas de (month, revenue) se entienden mil veces mejor como gráfico de líneas. SkylerX añade a la barra de resultados un conjunto de vistas alternativas: no se ejecuta de nuevo el SQL, simplemente se reformatea el resultado en memoria.

Esta página explica: cuándo cambiar de vista, cómo se calcula cada una, qué forma de datos requiere y qué se puede guardar como salida.

Cuándo conviene cambiar de vista ​

Forma del datoVista recomendadaEscenario típico
Una columna de categoría + una numéricaBarras / pastel / donaVentas por ciudad, errores por endpoint
Una columna de tiempo + una numérica (continua)Línea / áreaTendencia de DAU, uso de CPU
Dos columnas numéricas (correlación)DispersiónActividad vs retención de usuarios
Tres columnas categóricas / numéricasTabla dinámicaCanal × mes = ingresos
Dos columnas (lat, lng)Dispersión geográficaDistribución de tiendas, mapa de usuarios
Una de tiempo + una de etiquetaLínea temporalEventos de despliegue, ciclo de un pedido
(id, parent_id, ...)Árbol con FK auto-referencialComentarios anidados, departamentos
Varios cambios históricos de la misma filaHistorial de filaAuditoría de un registro

Disparador desde la barra inferior (packages/ui/src/components/ResultGrid.vue:1202-1215):

vue
<button :disabled="!result?.rows.length" @click="chartOpen = true">📊 图表</button>
<div class="menu-box">
  <button @click="showViewMenu = !showViewMenu">📐 视图</button>
  <!-- 弹出菜单 -->
  <button @click="altView = 'pivot'">⊞ 透视</button>
  <button @click="altView = 'tree'">🌳 树形</button>
  <button @click="altView = 'geo'">🗺 地理</button>
  <button @click="altView = 'timeline'">⏱ 时间轴</button>
</div>

Todas estas vistas se abren como modal; al cerrarlas vuelves a la cuadrícula: son "lupas" para la cuadrícula, no la reemplazan.

1. Vista de gráficos (barras / línea / pastel + 4 variantes) ​

packages/ui/src/components/ChartDialog.vue, 630 líneas, botón: 📊 Gráficos.

Decisiones de diseño ​

Los comentarios del código son sinceros:

No usamos ECharts; dibujamos SVG a mano (cada uno de barras / línea / pastel ronda las cien líneas) por:

  • El tamaño de la app de escritorio importa; los gráficos son solo una herramienta auxiliar del result grid, no el protagonista
  • Tres tipos cubren el 90% de las inspecciones puntuales; ya habrá tiempo de incorporar ECharts si hace falta más vistosidad
  • El SVG es fácil de exportar a PNG (toDataURL vía <canvas>)

Los 7 gráficos están escritos a mano en SVG puro:

GráficoPara quéLímiteNotas
📊 Barras (bar)Comparar valores por categoríaPrimeras 50 filasRedondeo automático del eje Y
📈 Línea (line)Tendencia / serie temporalPrimeras 200 filasPath M / L
🥧 Pastel (pie)ProporcionesPrimeras 50 filasEtiquetas automáticas de porcentaje
⛰ Área (area)Tendencia + magnitudPrimeras 200 filasLínea cerrada a la baseline
·· Dispersión (scatter)Nube de puntos discretaPrimeras 200 filasPuntos circulares
⭕ Dona (donut)Variante de proporcionesPrimeras 50 filasAnillo externo r * 1.0, hueco interno r * 0.55
📡 Radar (radar)Comparativa multidimensionalPrimeras 50 filas, mínimo 3 puntosUn eje por fila

Selección de columnas ​

Tres selectores en la barra superior: Label (cualquier columna, .toString()), Value (auto-detecta columnas numéricas; si no lo es, se etiqueta como (?)), Tipo. isNumericColumn toma las primeras 20 filas y prueba Number.isFinite(Number(v)); por defecto Y = primera columna numérica. Al cambiar de result, watch resetea las selecciones.

Reglas de datos: las filas cuyo Number(v) sea NaN se descartan; si la cantidad supera el límite, solo se usan las primeras N (barras / pastel 50; línea / área / dispersión 200; radar 50).

Eje Y ​

Para que la escala se vea "redonda", el máximo se calcula como Math.ceil(m / 10^floor(log10(m))) * 10^floor(log10(m)). Las etiquetas se formatean en B / M / k (cuando son mayores que 1e9 / 1e6 / 1e4).

Salida: exportar PNG ​

Botón a la derecha ⬇ Exportar PNG → XMLSerializer serializa el SVG → un <canvas> lo redibuja a 2× HiDPI (fondo oscuro #1d1e22) → canvas.toBlob('image/png') → pasa por el SaveFileDialog propio. Nombre chart-{kind}-{ts}.png, resolución 1440×720, perfecto para pegar en Feishu / Slack.

2. Tabla dinámica (PivotDialog) ​

packages/ui/src/components/PivotDialog.vue, 162 líneas. Disparador: 📐 Vista → ⊞ Pivot.

Objetivo: aplicar pivot en memoria al resultado actual, sin volver a ejecutar el SQL. El algoritmo no tiene misterio: agrupa filas por (rowFields...) → dentro de cada grupo, distribuye por colField → en cada cubo aplica agg.

Tres ejes + una función de agregación ​

ControlComportamiento
Filas (chips multiselección)Agrupa por estas columnas; la clave se forma con '|'
Columna (desplegable)Los valores distintos de esa columna se despliegan como cabeceras (orden lexicográfico)
Valor + agregaciónAplica la agregación a esa columna en cada celda (row, col)
Desplegable de agregaciónCOUNT / SUM / AVG / MIN / MAX

Algoritmo ​

Dos niveles anidados Map<rowKey, Map<colKey, number[]>>: se recorre result.rows; rowKey es la concatenación con | de las columnas de rowFields; colKey es el valor en string de colField; Number(row[valueField]) entra al array. NULL se unifica como literal 'NULL' (entra al mismo grupo). COUNT usa length; el resto, agregaciones numéricas.

Limitaciones ​

El propio código las admite:

No se soporta: varios value field, orden personalizado de las columnas (van en lexicográfico), filtros; quedan pendientes para la siguiente versión.

Es decir, si quieres "ordenar por mes 1-12 en lugar de 10, 11, 12, 1, 2...", aún no se puede; antes hay que pasar la columna a string con cero a la izquierda ('01' / '02' / ...) en el propio SQL.

Salida ​

Es solo una vista temporal de tabla, no se exporta. Para persistir los datos te sugerimos:

  • Cerrar el pivot, volver a la cuadrícula → clic derecho copiar → elegir CSV / Markdown y pegar en Excel / Notion
  • Reescribir el pivot en SQL: GROUP BY x WITH ROLLUP en MySQL / crosstab() en PG

3. Dispersión geográfica (GeoMapDialog) ​

packages/ui/src/components/GeoMapDialog.vue, 138 líneas. Disparador: 📐 Vista → 🗺 Geo.

No usamos leaflet ni mapa base, dibujamos los (lng, lat) directamente como puntos en SVG. El propio código lo explica:

Proyección: equidistante (Mercator deforma poco visualmente y, para datos locales, dibujar coordenadas directamente basta; nada de sistemas complejos). Lo que no se hace: mapa base (no traemos tiles) y clustering (con demasiados puntos se emborrona, pero se resuelve con zoom).

Detección automática de columnas ​

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)) ?? ''

Filtrado por rango numérico (para descartar basura):

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

Encuadre automático ​

No es el mapa mundial completo; los bounds se calculan para "contener exactamente los puntos + 5% de margen":

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

En los bordes del SVG aparecen las coordenadas de las cuatro esquinas; al pasar el ratón por un punto se muestra lat=... lng=....

Salida ​

Solo navegación visual; no exporta PNG (puede que en la próxima versión). Para una visualización persistente, añade en el SQL una columna de categoría y usa la vista de gráfico (dispersión) para capturarla.

Requisitos de los datos ​

Nombres aceptadosEjemplo
lat, latitude, ylatitude FLOAT
lng, lon, long, longitude, xlng DECIMAL(9,6)
name, title, label, id (etiqueta, opcional)store_name VARCHAR

Si no coinciden con los nombres estándar también vale: selecciónalos manualmente en los desplegables, siempre que el valor sea numérico y esté dentro del rango.

4. Línea temporal (TimelineDialog) ​

packages/ui/src/components/TimelineDialog.vue, 171 líneas. Disparador: 📐 Vista → ⏱ Línea temporal.

Detección automática ​

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 = ''   // opcional: clasifica por esta columna y aplica color

Suele acertar columnas como created_at / updated_at / event_time / order_date / login_time.

Parseo del valor temporal (toMs) ​

Admite cuatro formatos:

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 启发式
  const ms = Date.parse(String(v))  // ISO / "YYYY-MM-DD HH:MM:SS"
  return Number.isNaN(ms) ? null : ms
}

Los valores por debajo de 1e12 (año 2001) se interpretan como segundos Unix y se multiplican por 1000; por encima, como ms. Para datos de negocio normales sobra; los timestamps anteriores a 1969 pueden malinterpretarse: en ese caso conviértelos en string con to_char(...) en el propio SQL.

Renderizado ​

Línea temporal horizontal; los eventos alternan arriba/abajo para evitar solapamientos (i % 2 === 0 ? -16 : +16); el eje X muestra cinco marcas equidistantes con fechas.

Si especificas la columna color, los valores distintos toman 8 colores de la paleta (#7c6cff / #4caf50 / #e0a020 / #e04050 / #3aa1ff / #b48cff / #67c23a / #ff9966), y aparece una leyenda abajo. Al pasar el ratón por un punto se muestra tiempo · label en la barra inferior de información.

Requisitos de los datos ​

Al menos una columna de tiempo (Date / ISO / segundos o milisegundos Unix). Label y Color son opcionales.

5. Árbol con FK auto-referencial (TreeViewDialog) ​

packages/ui/src/components/TreeViewDialog.vue, 130 líneas. Disparador: 📐 Vista → 🌳 Árbol.

Ideal para claves foráneas auto-referenciales o datos jerárquicos: comentarios anidados (comments.parent_id → comments.id), departamentos (departments.parent_dept_id → id), regiones administrativas (regions.parent_id).

Tres ejes ​

SelectorRegla de inferencia
idPrimero /^id$/i; si no, la primera columna
parent/parent[_-]?id|pid/i; por defecto vacío
label/^(name|title|label)$/i; si no, fallback al id

Algoritmo ​

Dos pasadas: la primera construye el índice por id (byId: Map<id, node>); la segunda cuelga los hijos del padre; si el parent id no está en el índice (incluido NULL) la fila se considera raíz. parent === self se trata como raíz (para evitar pseudo-registros del tipo WHERE id=1 AND parent_id=1).

Detección de ciclos ​

walk(n, depth) DFS con un Set<string> de visitados; si vuelve a aparecer el mismo id, marca n.cycle = true y se detiene. Junto al nodo aparece un ⚠ amarillo y el tooltip avisa de "ciclo". Suele ocurrir cuando un cambio manual rompió la jerarquía (lo que debía ser una relación padre-hijo se volvió un ciclo).

Renderizado ​

Tras aplanar el árbol, sangra depth * 18px; cada nodo muestra ▸ <label> #<id>. Al pasar el ratón sobre la etiqueta, title="{json}" muestra el registro completo (inspección rápida a ojo).

Requisitos de los datos ​

Mínimo dos columnas: id + parent; un único SELECT id, parent_id, name FROM comments WHERE post_id = 1234 te trae el árbol entero y la vista lo renderiza por niveles.

6. Historial de cambios de fila (RowHistoryDialog) ​

packages/ui/src/components/RowHistoryDialog.vue, 123 líneas.

Objetivo: trazabilidad de versiones de una fila — dada la clave primaria de un registro en una tabla, encontrar todas sus versiones en las tablas espejo audit / *_history / *_log.

Descubrimiento automático de la tabla espejo ​

Al abrir, ejecuta SELECT table_name FROM information_schema.tables WHERE table_name LIKE '{base}_%' OR table_name = 'audit_{base}' OR table_name = '{base}_history', y carga los candidatos en un <datalist>; el usuario elige o teclea.

Consulta del historial ​

Tras fijar la tabla espejo, consulta por la PK: SELECT * FROM {shadow} WHERE {pk}=... ORDER BY changed_at, updated_at, created_at, version, revision DESC LIMIT 200. El ORDER BY enumera cinco columnas candidatas; la BD usa la que exista (MySQL es tolerante, PG estricto; las tablas de audit habituales tienen al menos una). El resultado se muestra como una mini tabla compacta con cada celda truncada a 80 caracteres.

Requisitos de los datos ​

Necesitas la tabla de negocio + una tabla espejo *_history / *_audit / *_log (PK + columnas de negocio repetidas + campos changed_at / version). La mayoría de implementaciones con triggers de audit cumplen esta convención.

Nota de implementación: el diálogo ya está en el repositorio (Workspace.vue monta el modal y tiene el estado rowHistOpen), pero aún no hay entrada directa desde el menú contextual de la cuadrícula: queda reservada para una próxima integración.

7. Linaje de datos (LineageDialog) — versión heurística ​

packages/ui/src/components/LineageDialog.vue, 98 líneas.

El propio código lo aclara desde el principio:

Linaje de columna (versión heurística): aún no hay un parser SQL real, así que arrancamos con la heurística más simple: si en el texto de un SQL del historial aparece «{table}.{column}» o el nombre desnudo {column} (siempre que el SQL haga FROM de {table}), se considera relacionada. Precisión limitada: hay falsos negativos (alias / subconsultas) y falsos positivos (columnas con el mismo nombre). Avisamos claramente al usuario de que es la versión "heurística"; cuando exista el parser SQL, lo sustituiremos por un análisis real.

Algoritmo ​

Carga los últimos 500 SQL del historial de la conexión y aplica dos regex con word boundary \b{table}\b + \b{column}\b sobre cada texto. Si hay match, mira el comienzo: INSERT / UPDATE → entra a sinks (escritura); SELECT / WITH → entra a sources (lectura).

Renderizado ​

Doble columna:

  • ← Sinks — SQLs que escriben en esta columna (INSERT / UPDATE)
  • → Sources — SQLs que leen de esta columna (SELECT / WITH)

Cada fila muestra la hora de ejecución + los primeros 120 caracteres del SQL. Arriba aparece una banda amarilla que recuerda "este es un resultado heurístico, no apto para auditoría".

Requisitos de los datos ​

Depende del historial de consultas (client.connections.history). Si nunca has ejecutado consultas relacionadas en SkylerX, el panel mostrará "No hits".

Nota de implementación: igual que RowHistoryDialog, está montado en Workspace.vue y debe activarse desde fuera (lineageOpen.value = {...}); hoy no tiene UI dedicada, queda como API reservada.

Matriz de soporte ​

VistaDetección automáticaLímite de datosExportación estáticaRe-ejecuta SQLRecomendada para
Gráficos (7 tipos)Sondeo de columnas numéricas50 / 200 filasPNG (2× HiDPI)NoVer magnitud / tendencia / proporción
Tabla dinámicaPrimera / segunda / tercera columnaSegún memoria del navegadorCopiar como CSVNoAgregación cruzada de dos ejes
Dispersión geográficaAlias lat / lng / x / ySin límiteNoNoDibujo directo de lat/long
Línea temporalSufijos at$ / time / date / createdSin límiteNoNoFlujo de eventos + color por categoría
Árbolid / parent_id / nameSin límiteNoNoJerarquías con FK auto-referencial
Historial de filaHeurística por nombre *_history / *_audit200 filas (LIMIT del SQL)NoSí (consulta la tabla audit)Trazabilidad de versiones de una fila
Linaje de datos—Últimos 500 del historialNoNoRelación lectura/escritura por columna (heurística)

Resumen de disparadores ​

VistaEntradaNotas
GráficosBarra de resultados 📊 GráficosAbre directamente en barras por defecto
Pivot / Árbol / Geo / Línea temporalBarra de resultados 📐 Vista → menúComparten el mismo modal vía altView
Historial de filaDisparado vía rowHistOpen.value = { conn, table, pk }Reservado, a la espera de un menú contextual
Linaje de datosDisparado vía lineageOpen.value = { conn, table, column }Reservado, a la espera de un menú contextual

Al cerrar los modales se vuelve a la cuadrícula original sin perder paginación ni ordenación: solo añaden una "lupa" sobre la cuadrícula, no la sustituyen.

Mini árbol de decisión ​

¿Quieres ver magnitud / ranking / tendencia / proporción? → Gráfico

  • Magnitud vs tiempo → línea / área
  • Ranking por categoría → barras
  • Proporción → pastel / dona
  • Multidimensional → radar

¿Necesitas un cruce bidimensional (p. ej. "canal × mes")? → Pivot

Datos con (lat, lng) → dispersión geográfica

Datos con una columna de tiempo:

  • Serie continua (DAU diario) → línea
  • Eventos discretos (despliegues, alarmas) → línea temporal

Datos con FK auto-referencial → árbol

Quieres ver el historial de una fila → historial de fila

Quieres saber quién lee y quién escribe una columna → linaje (heurístico, úsalo con cautela)

Con esto cubrimos todas las vistas alternativas sobre el resultado. Si tu dato no encaja en ninguna, en el 90% de los casos basta reescribir el SQL para meterlo en alguna vista; si no, vuelve a la cuadrícula y usa Copiar para pegar en Excel / Numbers / Notion.

Para ver el comportamiento del SQL en sí (slow log, EXPLAIN, recomendación de índices), ve a Avanzado e ingeniería; para importar/exportar datos, ve a Migración de datos.

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