asegurate de que subimos todo
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,13 +3,20 @@ add_imgui_app(tables_playground
|
||||
main.cpp
|
||||
data_table.cpp
|
||||
data_table_logic.cpp
|
||||
lua_engine.cpp
|
||||
tql.cpp
|
||||
viz.cpp
|
||||
)
|
||||
target_link_libraries(tables_playground PRIVATE lua54 implot)
|
||||
|
||||
# Self-test E2E (logica pura, sin ImGui). No depende de fn_framework.
|
||||
# Self-test E2E (logica pura + lua_engine + tql).
|
||||
add_executable(tables_playground_self_test
|
||||
self_test.cpp
|
||||
data_table_logic.cpp
|
||||
lua_engine.cpp
|
||||
tql.cpp
|
||||
)
|
||||
target_include_directories(tables_playground_self_test PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
)
|
||||
target_link_libraries(tables_playground_self_test PRIVATE lua54)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,12 +5,14 @@
|
||||
namespace data_table {
|
||||
|
||||
// Render barra-de-chips + tabla. Mutates `st` en respuesta a interaccion.
|
||||
// Caller mantiene el State entre frames.
|
||||
// `declared_types` opcional: array paralelo a headers con ColumnType por col.
|
||||
// Si nullptr o ColumnType::Auto -> resuelve via auto_detect_type.
|
||||
// API unificada: `tables` lista todas las tablas disponibles. La que actua como
|
||||
// main la elige State.main_source (vacio -> tables[0]). El resto se exponen
|
||||
// como joinables en la UI cuando size > 1.
|
||||
void render(const char* id,
|
||||
const char* const* headers,
|
||||
int col_count,
|
||||
const char* const* cells,
|
||||
int row_count,
|
||||
State& st);
|
||||
const std::vector<TableInput>& tables,
|
||||
State& st,
|
||||
bool show_chrome = true);
|
||||
|
||||
} // namespace data_table
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,13 +3,52 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace data_table {
|
||||
|
||||
enum class Op { Eq, Neq, Gt, Gte, Lt, Lte };
|
||||
enum class Op {
|
||||
Eq, Neq, Gt, Gte, Lt, Lte,
|
||||
Contains, NotContains, StartsWith, EndsWith
|
||||
};
|
||||
const char* op_label(Op o);
|
||||
bool op_is_string_only(Op o);
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Column types - declarado por caller con fallback a auto-detect.
|
||||
// ----------------------------------------------------------------------------
|
||||
enum class ColumnType {
|
||||
Auto, String, Int, Float, Bool, Date, Json
|
||||
};
|
||||
|
||||
const char* column_type_name(ColumnType t);
|
||||
const char* column_type_icon(ColumnType t); // UTF-8 Tabler icon
|
||||
|
||||
// Ops permitidos para cada tipo. Devuelve vector ordenado.
|
||||
std::vector<Op> ops_for_type(ColumnType t);
|
||||
|
||||
// Auto-detect via sample: escanea hasta `sample_n` celdas no-vacias.
|
||||
ColumnType auto_detect_type(const char* const* cells, int rows, int cols,
|
||||
int col, int sample_n = 64);
|
||||
|
||||
// Tipo efectivo: si declared != Auto -> declared; else auto_detect.
|
||||
ColumnType effective_type(ColumnType declared,
|
||||
const char* const* cells, int rows, int cols, int col);
|
||||
|
||||
// Derived column: inmutable. Dos modos:
|
||||
// 1) Retipo puro: source_col >= 0, formula == "". Cells del origen.
|
||||
// 2) Formula: source_col == -1, formula no vacia. Eval por Lua.
|
||||
struct DerivedColumn {
|
||||
int source_col = -1;
|
||||
ColumnType type = ColumnType::String;
|
||||
std::string name;
|
||||
std::string formula; // "" = retipado puro; resto = body Lua
|
||||
int lua_id = -1; // referencia en lua_engine; -1 si no compilado
|
||||
std::string compile_error;
|
||||
};
|
||||
|
||||
// Filter movido aqui (antes era despues de State) porque TQL Stage lo necesita.
|
||||
struct Filter {
|
||||
int col;
|
||||
Op op;
|
||||
@@ -19,15 +58,232 @@ struct Filter {
|
||||
struct ColorRule {
|
||||
int col;
|
||||
std::string equals;
|
||||
unsigned int color; // ImU32 (ABGR para ImGui)
|
||||
unsigned int color;
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// TQL (Table Query Language) — stage model. Ver docs/TQL.md.
|
||||
// ----------------------------------------------------------------------------
|
||||
enum class AggFn {
|
||||
Count, Sum, Avg, Min, Max, Distinct, Stddev,
|
||||
Median, P25, P75, P90, P99, Percentile
|
||||
};
|
||||
|
||||
const char* agg_fn_name(AggFn f);
|
||||
|
||||
struct Aggregation {
|
||||
AggFn fn = AggFn::Count;
|
||||
std::string col; // ignorado para Count
|
||||
double arg = 0.0; // para Percentile (0..1)
|
||||
std::string alias; // vacio -> auto-generado via aggregation_alias()
|
||||
};
|
||||
|
||||
struct SortClause {
|
||||
std::string col;
|
||||
bool desc = false;
|
||||
};
|
||||
|
||||
// Stage: layer de TQL. Stage 0 = Raw (sin breakouts/aggregations).
|
||||
// Stage 1+ pueden agrupar. Cada stage consume output del anterior.
|
||||
struct Stage {
|
||||
std::vector<Filter> filters;
|
||||
std::vector<DerivedColumn> derived; // expressions de este stage
|
||||
std::vector<std::string> breakouts; // col names del INPUT de este stage
|
||||
std::vector<Aggregation> aggregations;
|
||||
std::vector<SortClause> sorts;
|
||||
};
|
||||
|
||||
// Pure: alias por defecto cuando agg.alias esta vacio.
|
||||
// count -> "count"
|
||||
// distinct col -> "distinct_<col>"
|
||||
// percentile p -> "p<arg*100>_<col>" (ej. p95_size_kb)
|
||||
// resto -> "<fn>_<col>" (ej. avg_size_kb)
|
||||
std::string aggregation_alias(const Aggregation& a);
|
||||
|
||||
// Pure: tipo del output de la aggregation.
|
||||
// count, distinct -> Int
|
||||
// sum, avg, stddev,
|
||||
// median, p*, percentile -> Float
|
||||
// min, max -> mismo tipo que la col origen
|
||||
ColumnType aggregation_type(const Aggregation& a,
|
||||
const std::vector<std::string>& in_headers,
|
||||
const std::vector<ColumnType>& in_types);
|
||||
|
||||
// Output de compute_stage. Posee `cell_backing` (strings nuevos para
|
||||
// resultados agregados) y `cells` (punteros row-major a backing o a
|
||||
// `in_cells` original para passthrough).
|
||||
struct StageOutput {
|
||||
std::vector<std::string> cell_backing;
|
||||
std::vector<const char*> cells;
|
||||
int rows = 0;
|
||||
int cols = 0;
|
||||
std::vector<std::string> headers;
|
||||
std::vector<ColumnType> types;
|
||||
};
|
||||
|
||||
// Pure: ejecuta un Stage sobre los cells de entrada. Aplica filter -> (group+agg|passthrough) -> sort.
|
||||
StageOutput compute_stage(const char* const* in_cells, int in_rows, int in_cols,
|
||||
const std::vector<std::string>& in_headers,
|
||||
const std::vector<ColumnType>& in_types,
|
||||
const Stage& stage);
|
||||
|
||||
// Pure: aplica filtros usando headers para resolver f.col (que ahora es
|
||||
// indice en el array de in_headers, no del dataset original). Devuelve
|
||||
// indices de filas que pasan.
|
||||
std::vector<int> apply_filters(const char* const* cells, int rows, int cols,
|
||||
const std::vector<Filter>& filters);
|
||||
|
||||
// Pure: helper para drill-down. Devuelve un Filter Op::Eq sobre col_idx con
|
||||
// el value indicado. col_idx es indice en los headers del INPUT del stage
|
||||
// previo (donde se va a aplicar el filtro).
|
||||
Filter make_drill_filter(int col_idx, const std::string& value);
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// ViewMode: tipo de visualizacion a renderizar sobre el output del stage activo.
|
||||
// "Table" siempre disponible. Resto requiere ciertos tipos de columnas.
|
||||
// ----------------------------------------------------------------------------
|
||||
enum class ViewMode {
|
||||
Table,
|
||||
// Bars
|
||||
Bar, // horizontal bars: 1 cat + 1 num
|
||||
Column, // vertical bars: 1 cat + 1 num
|
||||
GroupedBar, // 1 cat + N num (side-by-side)
|
||||
StackedBar, // 1 cat + N num (stacked)
|
||||
// Lines / area
|
||||
Line, // X + 1..N Y series
|
||||
Area, // shaded to y=0
|
||||
Stairs, // step plot
|
||||
// Points
|
||||
Scatter, // X + Y
|
||||
Bubble, // X + Y + size
|
||||
// Distribution
|
||||
Histogram, // 1 num
|
||||
Histogram2D, // 2 num
|
||||
Heatmap, // matrix from breakouts
|
||||
BoxPlot, // 1 cat + 1 num (min/p25/p50/p75/max per group)
|
||||
// Stems / signals
|
||||
Stem,
|
||||
ErrorBars,
|
||||
// Composition
|
||||
Pie,
|
||||
Donut,
|
||||
Funnel, // ordered descending bars
|
||||
Waterfall, // running sum
|
||||
// Single values
|
||||
KPI, // big text + label
|
||||
KPIGrid, // all aggregations as cards
|
||||
// Specialized
|
||||
Candlestick, // OHLC: time + open + high + low + close
|
||||
Radar, // multi-axis (1 cat + N num)
|
||||
};
|
||||
|
||||
const char* view_mode_token(ViewMode m); // "table", "bar", ...
|
||||
const char* view_mode_label(ViewMode m); // "Table", "Bar (horizontal)", ...
|
||||
ViewMode view_mode_from_token(const char* s);
|
||||
int view_mode_min_cols(ViewMode m);
|
||||
bool view_mode_needs_numeric(ViewMode m);
|
||||
bool view_mode_needs_category(ViewMode m);
|
||||
// Requiere stage agrupado (breakout+aggregation). Si user esta en stage 0 con
|
||||
// uno de estos, conviene auto-promote a stage 1.
|
||||
bool view_mode_needs_aggregation(ViewMode m);
|
||||
|
||||
// Lista completa de modos para el selector UI (orden de display).
|
||||
const ViewMode* all_view_modes(int* n_out);
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Joins (MBQL-style). Ver issue 0078.
|
||||
// ----------------------------------------------------------------------------
|
||||
enum class JoinStrategy { Left, Inner, Right, Full };
|
||||
const char* join_strategy_token(JoinStrategy s);
|
||||
JoinStrategy join_strategy_from_token(const char* s);
|
||||
const char* join_strategy_label(JoinStrategy s);
|
||||
|
||||
// Tabla extra pasada al render() para joins. Owner externo (caller).
|
||||
struct TableInput {
|
||||
std::string name; // identificador estable (matchea Join.source)
|
||||
std::vector<std::string> headers;
|
||||
std::vector<ColumnType> types;
|
||||
const char* const* cells = nullptr; // row-major, headers.size() cols x rows filas
|
||||
int rows = 0;
|
||||
int cols = 0;
|
||||
};
|
||||
|
||||
// Join clause: une la tabla actual con `source` por las parejas `on`,
|
||||
// prefijando las cols del derecho con `alias.`.
|
||||
struct Join {
|
||||
std::string alias;
|
||||
std::string source;
|
||||
std::vector<std::pair<std::string, std::string>> on; // {left_col, right_col}
|
||||
JoinStrategy strategy = JoinStrategy::Left;
|
||||
std::vector<std::string> fields; // vacio = all del derecho
|
||||
};
|
||||
|
||||
// Pure: resuelve indice del main entre `tables` segun `main_source`.
|
||||
// Vacio -> 0. Nombre desconocido -> 0. tables vacio -> -1.
|
||||
int resolve_main_idx(const std::vector<TableInput>& tables, const std::string& main_source);
|
||||
|
||||
// Pure: aplica un join sobre dos tablas. Resultado: StageOutput con
|
||||
// `headers` = left + `<alias>.<right_col>` (filtrado por fields si no vacio).
|
||||
StageOutput join_tables(const char* const* left_cells, int left_rows, int left_cols,
|
||||
const std::vector<std::string>& left_headers,
|
||||
const std::vector<ColumnType>& left_types,
|
||||
const TableInput& right,
|
||||
const Join& jn);
|
||||
|
||||
// ViewConfig: overrides manuales de auto-detect para la vista activa.
|
||||
// Campos vacios -> auto. Si col name no existe en output, viz cae a auto.
|
||||
struct ViewConfig {
|
||||
std::string x_col; // single: scatter, line, hist2d
|
||||
std::vector<std::string> y_cols; // 1..N: line/area/bar/etc
|
||||
std::string size_col; // bubble
|
||||
std::string cat_col; // bar/pie/funnel/box override
|
||||
unsigned int primary_color = 0; // 0 = ImPlot auto
|
||||
int hist_bins = 0; // 0 = Sturges
|
||||
float pie_radius = 0.0f; // 0 = default
|
||||
bool show_legend = true;
|
||||
bool show_markers = false; // line/area markers
|
||||
bool locked = false; // disable pan/zoom
|
||||
mutable bool fit_request = false; // consumed by viz::render
|
||||
};
|
||||
|
||||
// VizPanel: viz adicional sobre el mismo StageOutput. State.display + viz_config
|
||||
// es el panel 0 (siempre visible); extra_panels son los aniadidos por el user.
|
||||
struct VizPanel {
|
||||
ViewMode display = ViewMode::Bar;
|
||||
ViewConfig config;
|
||||
// Memoria del ultimo non-Table display para toggle Table<->View.
|
||||
mutable ViewMode last_non_table = ViewMode::Bar;
|
||||
};
|
||||
|
||||
// State: stage pipeline + viz globales.
|
||||
//
|
||||
// `stages` siempre tiene tamaño >= 1 (auto-init en compute_visible_rows / render
|
||||
// si esta vacio: se crea stages[0] vacio). Stage 0 es Raw (filters + derived +
|
||||
// sorts; SIN breakouts/aggregations). Stages 1+ pueden agrupar.
|
||||
//
|
||||
// `active_stage` = indice del stage cuyo output se renderiza.
|
||||
// `col_visible/col_order/color_rules` aplican al output del stage activo.
|
||||
struct State {
|
||||
std::vector<Filter> filters;
|
||||
std::vector<ColorRule> color_rules;
|
||||
std::vector<bool> col_visible; // size = col_count; auto-init en render
|
||||
int sort_col = -1; // -1 = sin sort
|
||||
bool sort_desc = false;
|
||||
std::vector<Stage> stages;
|
||||
int active_stage = 0;
|
||||
ViewMode display = ViewMode::Table;
|
||||
ViewConfig viz_config;
|
||||
std::vector<VizPanel> extra_panels;
|
||||
std::vector<Join> joins; // aplicado antes de stages[0]
|
||||
std::string main_source; // name de TableInput a usar como main; vacio -> tables[0]
|
||||
|
||||
std::vector<ColorRule> color_rules;
|
||||
std::vector<bool> col_visible; // size = effective_cols del stage activo
|
||||
std::vector<int> col_order; // permutacion [0..effective_cols)
|
||||
|
||||
// --- Compat helpers: shortcuts a stages[0] (Raw) ---
|
||||
// Util tras refactor para tests / accesos puntuales. Garantizan stages[0]
|
||||
// existe (lo crean vacio si no).
|
||||
Stage& raw();
|
||||
const Stage& raw() const;
|
||||
Stage& active();
|
||||
const Stage& active_const() const;
|
||||
void ensure_stage0();
|
||||
};
|
||||
|
||||
// Parse "1.23" -> 1.23, true. False si la celda no es numero completo.
|
||||
@@ -41,4 +297,69 @@ std::vector<int> compute_visible_rows(const char* const* cells,
|
||||
int rows, int cols,
|
||||
const State& st);
|
||||
|
||||
// Pure: muta col_order de st para colocar `src` en la posicion (en orden visual)
|
||||
// donde estaba `dst`. No-op si src == dst o cualquiera fuera del array.
|
||||
void reorder_column(State& st, int src, int dst);
|
||||
|
||||
// Pure: dado un buffer y posicion de cursor, busca el `[` abierto sin cerrar
|
||||
// mas reciente. Devuelve su indice (o -1 si ninguno). Rellena `filter_text`
|
||||
// con los caracteres entre `[` y cursor.
|
||||
// Para autocomplete de formulas: cuando el usuario teclea `[` el ImGui callback
|
||||
// detecta esto y muestra un popup con cols disponibles.
|
||||
int find_open_bracket(const char* buf, int len, int cursor, std::string& filter_text);
|
||||
|
||||
// Pure: reemplaza src[start..cursor) por "[name]". Devuelve nuevo string y
|
||||
// actualiza `new_cursor` a la posicion despues del `]`.
|
||||
std::string insert_column_ref(const std::string& src, int start, int cursor,
|
||||
const std::string& name, int& new_cursor);
|
||||
|
||||
// CSV: escapa una celda segun RFC 4180 (wrap en " si contiene , " o newline).
|
||||
std::string csv_escape(const char* s);
|
||||
|
||||
// Construye TSV de un rect de seleccion. Headers SIEMPRE incluidos.
|
||||
// view_row_lo/hi: indices en visible_rows.
|
||||
// view_col_lo/hi: indices en col_order. Cols ocultas se omiten.
|
||||
std::string build_tsv(const char* const* cells, int rows, int cols,
|
||||
const char* const* headers,
|
||||
const std::vector<int>& col_order,
|
||||
const std::vector<bool>& col_visible,
|
||||
const std::vector<int>& visible_rows,
|
||||
int view_row_lo, int view_row_hi,
|
||||
int view_col_lo, int view_col_hi);
|
||||
|
||||
// Construye CSV (full visible view). Headers incluidos, cells escapados.
|
||||
std::string build_csv(const char* const* cells, int rows, int cols,
|
||||
const char* const* headers,
|
||||
const std::vector<int>& col_order,
|
||||
const std::vector<bool>& col_visible,
|
||||
const std::vector<int>& visible_rows);
|
||||
|
||||
struct ColStats {
|
||||
int total = 0; // filas escaneadas
|
||||
int empty_count = 0; // cells == "" o null
|
||||
int unique_count = 0; // distintas (cap configurable)
|
||||
bool unique_capped = false; // true si se alcanzo el cap
|
||||
bool numeric = false; // true si todas las cells no-vacias parsean como numero
|
||||
int numeric_count = 0;
|
||||
double min = 0;
|
||||
double max = 0;
|
||||
double sum = 0;
|
||||
double mean = 0;
|
||||
double p25 = 0;
|
||||
double p50 = 0;
|
||||
double p75 = 0;
|
||||
std::vector<float> hist; // bins (HIST_BINS) si numeric
|
||||
std::vector<std::pair<std::string,int>> top_categories; // top 8 por count desc
|
||||
};
|
||||
|
||||
constexpr int HIST_BINS = 24;
|
||||
|
||||
// Pure: escanea una columna y devuelve estadisticas. `unique_cap` corta el
|
||||
// conteo de unicos si excede (para datasets de millones). 0 = sin cap.
|
||||
// Si `indices != nullptr` y `n_indices > 0`, recorre solo las filas indicadas
|
||||
// (uso tipico: stats sobre filas visibles post-filtro).
|
||||
ColStats compute_column_stats(const char* const* cells, int rows, int cols,
|
||||
int col, int unique_cap = 100000,
|
||||
const int* indices = nullptr, int n_indices = 0);
|
||||
|
||||
} // namespace data_table
|
||||
|
||||
Regular → Executable
@@ -0,0 +1,574 @@
|
||||
#include "lua_engine.h"
|
||||
|
||||
extern "C" {
|
||||
#include "lua.h"
|
||||
#include "lualib.h"
|
||||
#include "lauxlib.h"
|
||||
}
|
||||
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
namespace lua_engine {
|
||||
|
||||
struct Engine {
|
||||
lua_State* L = nullptr;
|
||||
std::vector<RowCtx*> ctx_stack;
|
||||
std::vector<int> visiting_derived;
|
||||
};
|
||||
|
||||
namespace {
|
||||
|
||||
Engine* g_engine = nullptr;
|
||||
|
||||
Engine* engine_from_state(lua_State* L) {
|
||||
return *static_cast<Engine**>(lua_getextraspace(L));
|
||||
}
|
||||
|
||||
RowCtx* current_ctx(lua_State* L) {
|
||||
Engine* e = engine_from_state(L);
|
||||
if (!e || e->ctx_stack.empty()) return nullptr;
|
||||
return e->ctx_stack.back();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Push de cell respetando tipo declarado:
|
||||
// Int/Float -> number (integer si exacto)
|
||||
// Bool -> boolean (true/false/1/0); en otro caso push string
|
||||
// Date/String/Json/Auto -> string
|
||||
// Si types_orig == nullptr -> heuristica: parse_number; si parsea -> number.
|
||||
// ---------------------------------------------------------------------------
|
||||
void push_typed(lua_State* L, const char* v, data_table::ColumnType t) {
|
||||
if (!v || !*v) { lua_pushnil(L); return; }
|
||||
using data_table::ColumnType;
|
||||
using data_table::parse_number;
|
||||
if (t == ColumnType::Int) {
|
||||
double d;
|
||||
if (parse_number(v, d)) {
|
||||
long long iv = (long long)d;
|
||||
if ((double)iv == d) lua_pushinteger(L, (lua_Integer)iv);
|
||||
else lua_pushnumber (L, (lua_Number)d);
|
||||
} else lua_pushstring(L, v);
|
||||
return;
|
||||
}
|
||||
if (t == ColumnType::Float) {
|
||||
double d;
|
||||
if (parse_number(v, d)) {
|
||||
long long iv = (long long)d;
|
||||
if ((double)iv == d) lua_pushinteger(L, (lua_Integer)iv);
|
||||
else lua_pushnumber (L, (lua_Number)d);
|
||||
} else lua_pushstring(L, v);
|
||||
return;
|
||||
}
|
||||
if (t == ColumnType::Bool) {
|
||||
if (std::strcmp(v, "true") == 0 || std::strcmp(v, "1") == 0) lua_pushboolean(L, 1);
|
||||
else if (std::strcmp(v, "false") == 0 || std::strcmp(v, "0") == 0) lua_pushboolean(L, 0);
|
||||
else lua_pushstring(L, v);
|
||||
return;
|
||||
}
|
||||
if (t == ColumnType::Auto) {
|
||||
// Sin tipo declarado: heuristica. parse_number -> number, else string.
|
||||
double d;
|
||||
if (parse_number(v, d)) {
|
||||
long long iv = (long long)d;
|
||||
if ((double)iv == d) lua_pushinteger(L, (lua_Integer)iv);
|
||||
else lua_pushnumber (L, (lua_Number)d);
|
||||
} else lua_pushstring(L, v);
|
||||
return;
|
||||
}
|
||||
// String / Date / Json
|
||||
lua_pushstring(L, v);
|
||||
}
|
||||
|
||||
// Fwd: para recursion en row_index.
|
||||
std::string eval_internal(Engine* e, int id, const RowCtx& ctx, std::string* err_out);
|
||||
|
||||
int row_index(lua_State* L) {
|
||||
Engine* eng = engine_from_state(L);
|
||||
RowCtx* ctx = current_ctx(L);
|
||||
if (!ctx) { lua_pushnil(L); return 1; }
|
||||
|
||||
using data_table::ColumnType;
|
||||
auto get_orig_type = [&](int c) -> ColumnType {
|
||||
if (ctx->types_orig && c < ctx->n_types_orig) return ctx->types_orig[c];
|
||||
return ColumnType::Auto;
|
||||
};
|
||||
|
||||
if (lua_type(L, 2) == LUA_TSTRING) {
|
||||
const char* key = lua_tostring(L, 2);
|
||||
if (ctx->name_to_col) {
|
||||
auto it = ctx->name_to_col->find(key);
|
||||
if (it != ctx->name_to_col->end()) {
|
||||
int col = it->second;
|
||||
push_typed(L, ctx->cells[ctx->row * ctx->orig_cols + col], get_orig_type(col));
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
if (ctx->derived_name_to_idx && ctx->derived) {
|
||||
auto it = ctx->derived_name_to_idx->find(key);
|
||||
if (it != ctx->derived_name_to_idx->end()) {
|
||||
int didx = it->second;
|
||||
if (didx < 0 || didx >= (int)ctx->derived->size()) {
|
||||
lua_pushnil(L); return 1;
|
||||
}
|
||||
// cycle check
|
||||
for (int v : eng->visiting_derived) {
|
||||
if (v == didx) { lua_pushnil(L); return 1; }
|
||||
}
|
||||
const auto& d = (*ctx->derived)[didx];
|
||||
if (d.formula.empty()) {
|
||||
// retipo puro
|
||||
if (d.source_col < 0 || d.source_col >= ctx->orig_cols) {
|
||||
lua_pushnil(L); return 1;
|
||||
}
|
||||
push_typed(L, ctx->cells[ctx->row * ctx->orig_cols + d.source_col], d.type);
|
||||
} else if (d.lua_id < 0) {
|
||||
lua_pushnil(L);
|
||||
} else {
|
||||
eng->visiting_derived.push_back(didx);
|
||||
std::string err;
|
||||
std::string r = eval_internal(eng, d.lua_id, *ctx, &err);
|
||||
eng->visiting_derived.pop_back();
|
||||
push_typed(L, r.c_str(), d.type);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
lua_pushnil(L);
|
||||
return 1;
|
||||
}
|
||||
if (lua_type(L, 2) == LUA_TNUMBER) {
|
||||
int idx = (int)lua_tointeger(L, 2);
|
||||
if (idx >= 1 && idx <= ctx->orig_cols) {
|
||||
int col = idx - 1;
|
||||
push_typed(L, ctx->cells[ctx->row * ctx->orig_cols + col], get_orig_type(col));
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
lua_pushnil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// --- fn.* builtins ---
|
||||
int b_upper(lua_State* L) {
|
||||
const char* s = luaL_checkstring(L, 1);
|
||||
std::string out(s);
|
||||
for (char& c : out) if (c >= 'a' && c <= 'z') c -= 32;
|
||||
lua_pushlstring(L, out.data(), out.size());
|
||||
return 1;
|
||||
}
|
||||
int b_lower(lua_State* L) {
|
||||
const char* s = luaL_checkstring(L, 1);
|
||||
std::string out(s);
|
||||
for (char& c : out) if (c >= 'A' && c <= 'Z') c += 32;
|
||||
lua_pushlstring(L, out.data(), out.size());
|
||||
return 1;
|
||||
}
|
||||
int b_length(lua_State* L) {
|
||||
if (lua_isnil(L, 1)) { lua_pushinteger(L, 0); return 1; }
|
||||
const char* s = luaL_checkstring(L, 1);
|
||||
lua_pushinteger(L, (lua_Integer)std::strlen(s));
|
||||
return 1;
|
||||
}
|
||||
int b_substring(lua_State* L) {
|
||||
const char* s = luaL_checkstring(L, 1);
|
||||
int start = (int)luaL_checkinteger(L, 2);
|
||||
int len = (int)luaL_optinteger(L, 3, -1);
|
||||
int slen = (int)std::strlen(s);
|
||||
if (start < 1) start = 1;
|
||||
if (start > slen) { lua_pushlstring(L, "", 0); return 1; }
|
||||
int from = start - 1;
|
||||
int take = (len < 0) ? slen - from : len;
|
||||
if (from + take > slen) take = slen - from;
|
||||
lua_pushlstring(L, s + from, take);
|
||||
return 1;
|
||||
}
|
||||
int b_contains(lua_State* L) {
|
||||
const char* h = luaL_checkstring(L, 1);
|
||||
const char* n = luaL_checkstring(L, 2);
|
||||
lua_pushboolean(L, std::strstr(h, n) != nullptr);
|
||||
return 1;
|
||||
}
|
||||
int b_starts_with(lua_State* L) {
|
||||
const char* h = luaL_checkstring(L, 1);
|
||||
const char* n = luaL_checkstring(L, 2);
|
||||
size_t ln = std::strlen(n);
|
||||
lua_pushboolean(L, std::strncmp(h, n, ln) == 0);
|
||||
return 1;
|
||||
}
|
||||
int b_ends_with(lua_State* L) {
|
||||
const char* h = luaL_checkstring(L, 1);
|
||||
const char* n = luaL_checkstring(L, 2);
|
||||
size_t lh = std::strlen(h), ln = std::strlen(n);
|
||||
lua_pushboolean(L, ln <= lh && std::strcmp(h + lh - ln, n) == 0);
|
||||
return 1;
|
||||
}
|
||||
int b_replace(lua_State* L) {
|
||||
const char* s = luaL_checkstring(L, 1);
|
||||
const char* find = luaL_checkstring(L, 2);
|
||||
const char* repl = luaL_checkstring(L, 3);
|
||||
std::string out;
|
||||
size_t flen = std::strlen(find);
|
||||
if (flen == 0) { lua_pushstring(L, s); return 1; }
|
||||
for (const char* p = s; *p; ) {
|
||||
if (std::strncmp(p, find, flen) == 0) { out += repl; p += flen; }
|
||||
else { out += *p++; }
|
||||
}
|
||||
lua_pushlstring(L, out.data(), out.size());
|
||||
return 1;
|
||||
}
|
||||
int b_trim(lua_State* L) {
|
||||
const char* s = luaL_checkstring(L, 1);
|
||||
while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') ++s;
|
||||
const char* e = s + std::strlen(s);
|
||||
while (e > s && (e[-1] == ' ' || e[-1] == '\t' || e[-1] == '\n' || e[-1] == '\r')) --e;
|
||||
lua_pushlstring(L, s, e - s);
|
||||
return 1;
|
||||
}
|
||||
int b_concat(lua_State* L) {
|
||||
int n = lua_gettop(L);
|
||||
std::string out;
|
||||
for (int i = 1; i <= n; ++i) {
|
||||
size_t sl = 0;
|
||||
const char* s = luaL_tolstring(L, i, &sl);
|
||||
out.append(s, sl);
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
lua_pushlstring(L, out.data(), out.size());
|
||||
return 1;
|
||||
}
|
||||
int b_to_number(lua_State* L) {
|
||||
if (lua_isnumber(L, 1)) { lua_pushvalue(L, 1); return 1; }
|
||||
const char* s = luaL_checkstring(L, 1);
|
||||
char* end = nullptr;
|
||||
double v = std::strtod(s, &end);
|
||||
if (end == s) { lua_pushnil(L); return 1; }
|
||||
lua_pushnumber(L, v);
|
||||
return 1;
|
||||
}
|
||||
int b_to_string(lua_State* L) { luaL_tolstring(L, 1, nullptr); return 1; }
|
||||
int b_to_bool(lua_State* L) {
|
||||
if (lua_isboolean(L, 1)) { lua_pushvalue(L, 1); return 1; }
|
||||
const char* s = luaL_optstring(L, 1, "");
|
||||
lua_pushboolean(L, std::strcmp(s, "true") == 0 || std::strcmp(s, "1") == 0);
|
||||
return 1;
|
||||
}
|
||||
int b_is_null(lua_State* L) { lua_pushboolean(L, lua_isnil(L, 1)); return 1; }
|
||||
int b_is_empty(lua_State* L) {
|
||||
if (lua_isnil(L, 1)) { lua_pushboolean(L, 1); return 1; }
|
||||
const char* s = luaL_optstring(L, 1, "");
|
||||
lua_pushboolean(L, *s == 0);
|
||||
return 1;
|
||||
}
|
||||
int b_coalesce(lua_State* L) {
|
||||
int n = lua_gettop(L);
|
||||
for (int i = 1; i <= n; ++i) {
|
||||
if (!lua_isnil(L, i)) { lua_pushvalue(L, i); return 1; }
|
||||
}
|
||||
lua_pushnil(L);
|
||||
return 1;
|
||||
}
|
||||
int b_parse_date(lua_State* L) {
|
||||
const char* s = luaL_checkstring(L, 1);
|
||||
if (std::strlen(s) < 10) { lua_pushnil(L); return 1; }
|
||||
int y, m, d;
|
||||
if (std::sscanf(s, "%d-%d-%d", &y, &m, &d) != 3) { lua_pushnil(L); return 1; }
|
||||
lua_createtable(L, 0, 3);
|
||||
lua_pushinteger(L, y); lua_setfield(L, -2, "year");
|
||||
lua_pushinteger(L, m); lua_setfield(L, -2, "month");
|
||||
lua_pushinteger(L, d); lua_setfield(L, -2, "day");
|
||||
return 1;
|
||||
}
|
||||
int b_year(lua_State* L) {
|
||||
const char* s = luaL_checkstring(L, 1);
|
||||
int y; if (std::sscanf(s, "%d", &y) != 1) { lua_pushnil(L); return 1; }
|
||||
lua_pushinteger(L, y); return 1;
|
||||
}
|
||||
int b_month(lua_State* L) {
|
||||
const char* s = luaL_checkstring(L, 1);
|
||||
int y, m; if (std::sscanf(s, "%d-%d", &y, &m) != 2) { lua_pushnil(L); return 1; }
|
||||
lua_pushinteger(L, m); return 1;
|
||||
}
|
||||
int b_day(lua_State* L) {
|
||||
const char* s = luaL_checkstring(L, 1);
|
||||
int y, m, d; if (std::sscanf(s, "%d-%d-%d", &y, &m, &d) != 3) { lua_pushnil(L); return 1; }
|
||||
lua_pushinteger(L, d); return 1;
|
||||
}
|
||||
|
||||
void apply_sandbox(lua_State* L) {
|
||||
const char* nuke[] = { "io", "require", "loadfile", "dofile", "load",
|
||||
"package", "debug", nullptr };
|
||||
for (int i = 0; nuke[i]; ++i) {
|
||||
lua_pushnil(L);
|
||||
lua_setglobal(L, nuke[i]);
|
||||
}
|
||||
lua_getglobal(L, "os");
|
||||
if (lua_istable(L, -1)) {
|
||||
lua_createtable(L, 0, 4);
|
||||
const char* keep[] = {"date", "time", "difftime", "clock", nullptr};
|
||||
for (int i = 0; keep[i]; ++i) {
|
||||
lua_getfield(L, -2, keep[i]);
|
||||
lua_setfield(L, -2, keep[i]);
|
||||
}
|
||||
lua_setglobal(L, "os");
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
|
||||
void register_builtins(lua_State* L) {
|
||||
lua_createtable(L, 0, 24);
|
||||
#define R(name, fn) lua_pushcfunction(L, fn); lua_setfield(L, -2, name);
|
||||
R("upper", b_upper);
|
||||
R("lower", b_lower);
|
||||
R("length", b_length);
|
||||
R("substring", b_substring);
|
||||
R("contains", b_contains);
|
||||
R("starts_with", b_starts_with);
|
||||
R("ends_with", b_ends_with);
|
||||
R("replace", b_replace);
|
||||
R("trim", b_trim);
|
||||
R("concat", b_concat);
|
||||
R("to_number", b_to_number);
|
||||
R("to_string", b_to_string);
|
||||
R("to_bool", b_to_bool);
|
||||
R("is_null", b_is_null);
|
||||
R("is_empty", b_is_empty);
|
||||
R("coalesce", b_coalesce);
|
||||
R("parse_date", b_parse_date);
|
||||
R("year", b_year);
|
||||
R("month", b_month);
|
||||
R("day", b_day);
|
||||
#undef R
|
||||
lua_setglobal(L, "fn");
|
||||
}
|
||||
|
||||
void install_row_metatable(lua_State* L) {
|
||||
luaL_newmetatable(L, "fn_row_meta");
|
||||
lua_pushcfunction(L, row_index);
|
||||
lua_setfield(L, -2, "__index");
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Preprocesador: [col] -> row["col"] respetando strings y comentarios.
|
||||
// Auto-prepend `return` si la formula es expresion suelta.
|
||||
// ---------------------------------------------------------------------------
|
||||
bool ident_start(unsigned char c) {
|
||||
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_' || c >= 0x80;
|
||||
}
|
||||
// Para nombres de cols dentro de [name]: permite espacios para "col with space"
|
||||
// y '.' para futuro `alias.col` post-join (fase 9 — issue 0078).
|
||||
bool ident_cont(unsigned char c) {
|
||||
return ident_start(c) || (c >= '0' && c <= '9') || c == ' ' || c == '.';
|
||||
}
|
||||
// Para boundary de keywords Lua: NO permite espacio.
|
||||
bool word_char(unsigned char c) {
|
||||
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
|
||||
(c >= '0' && c <= '9') || c == '_' || c >= 0x80;
|
||||
}
|
||||
|
||||
bool kw_at(const std::string& s, size_t i, const char* kw) {
|
||||
size_t k = std::strlen(kw);
|
||||
if (i + k > s.size()) return false;
|
||||
if (s.compare(i, k, kw) != 0) return false;
|
||||
if (i + k == s.size()) return true;
|
||||
unsigned char nc = (unsigned char)s[i + k];
|
||||
return !word_char(nc);
|
||||
}
|
||||
|
||||
bool needs_auto_return(const std::string& body) {
|
||||
size_t i = 0;
|
||||
while (i < body.size()) {
|
||||
char c = body[i];
|
||||
if (c == ' ' || c == '\t' || c == '\n' || c == '\r') { ++i; continue; }
|
||||
// skip short comment
|
||||
if (c == '-' && i + 1 < body.size() && body[i+1] == '-') {
|
||||
// long comment?
|
||||
if (i + 3 < body.size() && body[i+2] == '[' && body[i+3] == '[') {
|
||||
size_t j = i + 4;
|
||||
while (j + 1 < body.size() && !(body[j] == ']' && body[j+1] == ']')) ++j;
|
||||
i = (j + 1 < body.size()) ? j + 2 : body.size();
|
||||
continue;
|
||||
}
|
||||
while (i < body.size() && body[i] != '\n') ++i;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (i >= body.size()) return false;
|
||||
const char* kws[] = {"return","if","for","while","do","local","repeat","function", nullptr};
|
||||
for (int k = 0; kws[k]; ++k) if (kw_at(body, i, kws[k])) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string brackets_pass(const std::string& src) {
|
||||
std::string out;
|
||||
out.reserve(src.size() + 16);
|
||||
size_t i = 0;
|
||||
while (i < src.size()) {
|
||||
char c = src[i];
|
||||
// strings
|
||||
if (c == '"' || c == '\'') {
|
||||
char q = c;
|
||||
out += c; ++i;
|
||||
while (i < src.size()) {
|
||||
char d = src[i];
|
||||
out += d; ++i;
|
||||
if (d == '\\' && i < src.size()) { out += src[i++]; continue; }
|
||||
if (d == q) break;
|
||||
if (d == '\n') break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// comentario corto / largo
|
||||
if (c == '-' && i + 1 < src.size() && src[i+1] == '-') {
|
||||
// long: --[[ ... ]]
|
||||
if (i + 3 < src.size() && src[i+2] == '[' && src[i+3] == '[') {
|
||||
out.append(src, i, 4); i += 4;
|
||||
while (i + 1 < src.size() && !(src[i] == ']' && src[i+1] == ']')) {
|
||||
out += src[i++];
|
||||
}
|
||||
if (i + 1 < src.size()) { out += src[i++]; out += src[i++]; }
|
||||
continue;
|
||||
}
|
||||
// short
|
||||
while (i < src.size() && src[i] != '\n') { out += src[i++]; }
|
||||
continue;
|
||||
}
|
||||
// long string [[ ... ]]
|
||||
if (c == '[' && i + 1 < src.size() && src[i+1] == '[') {
|
||||
out.append(src, i, 2); i += 2;
|
||||
while (i + 1 < src.size() && !(src[i] == ']' && src[i+1] == ']')) {
|
||||
out += src[i++];
|
||||
}
|
||||
if (i + 1 < src.size()) { out += src[i++]; out += src[i++]; }
|
||||
continue;
|
||||
}
|
||||
// bracket col-ref [name]
|
||||
if (c == '[') {
|
||||
// peek if next is valid ident_start
|
||||
if (i + 1 < src.size() && ident_start((unsigned char)src[i+1])) {
|
||||
size_t j = i + 1;
|
||||
while (j < src.size() && src[j] != ']' && src[j] != '\n') {
|
||||
if (!ident_cont((unsigned char)src[j])) { j = std::string::npos; break; }
|
||||
++j;
|
||||
}
|
||||
if (j != std::string::npos && j < src.size() && src[j] == ']') {
|
||||
std::string name(src, i + 1, j - i - 1);
|
||||
// trim trailing space
|
||||
while (!name.empty() && name.back() == ' ') name.pop_back();
|
||||
out += "row[\"";
|
||||
out += name;
|
||||
out += "\"]";
|
||||
i = j + 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
out += c;
|
||||
++i;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
} // anon
|
||||
|
||||
std::string preprocess(const std::string& body) {
|
||||
std::string pre = brackets_pass(body);
|
||||
if (needs_auto_return(pre)) return "return " + pre;
|
||||
return pre;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
std::string eval_internal(Engine* e, int id, const RowCtx& ctx, std::string* err_out) {
|
||||
if (!e || !e->L || id < 0) {
|
||||
if (err_out) *err_out = "invalid handle";
|
||||
return "";
|
||||
}
|
||||
lua_State* L = e->L;
|
||||
e->ctx_stack.push_back(const_cast<RowCtx*>(&ctx));
|
||||
lua_rawgeti(L, LUA_REGISTRYINDEX, id);
|
||||
lua_newuserdata(L, 1);
|
||||
luaL_setmetatable(L, "fn_row_meta");
|
||||
int rc = lua_pcall(L, 1, 1, 0);
|
||||
e->ctx_stack.pop_back();
|
||||
if (rc != LUA_OK) {
|
||||
if (err_out) *err_out = lua_tostring(L, -1) ? lua_tostring(L, -1) : "runtime error";
|
||||
lua_pop(L, 1);
|
||||
return "";
|
||||
}
|
||||
std::string out;
|
||||
if (lua_isnil(L, -1)) out = "";
|
||||
else {
|
||||
size_t n = 0;
|
||||
const char* s = luaL_tolstring(L, -1, &n);
|
||||
out.assign(s, n);
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
return out;
|
||||
}
|
||||
|
||||
} // anon
|
||||
|
||||
Engine* get() {
|
||||
if (g_engine) return g_engine;
|
||||
g_engine = new Engine();
|
||||
g_engine->L = luaL_newstate();
|
||||
luaL_openlibs(g_engine->L);
|
||||
*static_cast<Engine**>(lua_getextraspace(g_engine->L)) = g_engine;
|
||||
apply_sandbox(g_engine->L);
|
||||
register_builtins(g_engine->L);
|
||||
install_row_metatable(g_engine->L);
|
||||
return g_engine;
|
||||
}
|
||||
|
||||
void shutdown() {
|
||||
if (!g_engine) return;
|
||||
lua_close(g_engine->L);
|
||||
delete g_engine;
|
||||
g_engine = nullptr;
|
||||
}
|
||||
|
||||
int compile(Engine* e, const std::string& body, std::string* err_out) {
|
||||
if (!e || !e->L) { if (err_out) *err_out = "engine null"; return -1; }
|
||||
lua_State* L = e->L;
|
||||
std::string final_body = preprocess(body);
|
||||
std::string wrapped = "return function(row)\n" + final_body + "\nend";
|
||||
if (luaL_loadbufferx(L, wrapped.data(), wrapped.size(), "formula", "t") != LUA_OK) {
|
||||
if (err_out) *err_out = lua_tostring(L, -1) ? lua_tostring(L, -1) : "parse error";
|
||||
lua_pop(L, 1);
|
||||
return -1;
|
||||
}
|
||||
if (lua_pcall(L, 0, 1, 0) != LUA_OK) {
|
||||
if (err_out) *err_out = lua_tostring(L, -1) ? lua_tostring(L, -1) : "compile error";
|
||||
lua_pop(L, 1);
|
||||
return -1;
|
||||
}
|
||||
if (!lua_isfunction(L, -1)) {
|
||||
if (err_out) *err_out = "formula did not produce a function";
|
||||
lua_pop(L, 1);
|
||||
return -1;
|
||||
}
|
||||
int ref = luaL_ref(L, LUA_REGISTRYINDEX);
|
||||
return ref;
|
||||
}
|
||||
|
||||
void release(Engine* e, int id) {
|
||||
if (!e || !e->L || id < 0) return;
|
||||
luaL_unref(e->L, LUA_REGISTRYINDEX, id);
|
||||
}
|
||||
|
||||
std::string eval(Engine* e, int id, const RowCtx& ctx, std::string* err_out) {
|
||||
return eval_internal(e, id, ctx, err_out);
|
||||
}
|
||||
|
||||
lua_State* raw_state() {
|
||||
Engine* e = get();
|
||||
return e ? e->L : nullptr;
|
||||
}
|
||||
|
||||
} // namespace lua_engine
|
||||
@@ -0,0 +1,61 @@
|
||||
// Lua 5.4 wrapper para formulas de columnas custom del playground tables.
|
||||
//
|
||||
// Features:
|
||||
// - Sandbox medio: io/require/dofile fuera; os reducido a date/time/diff/clock.
|
||||
// - Builtins fn.* (~20 funciones).
|
||||
// - Sintaxis [col_name] preprocesada a row["col_name"].
|
||||
// - Auto-`return` si la formula es expresion suelta sin keyword inicial.
|
||||
// - Type-aware push: row.x devuelve number si la col es Int/Float, boolean
|
||||
// si Bool, string en el resto (Date/String/Json). Nil si vacia.
|
||||
// - UTF-8 ok en nombres de columnas dentro de [].
|
||||
// - Comentarios y string literals preservados por el preprocesador.
|
||||
// - Llamadas recursivas: un derived col puede referenciar a otro derived col;
|
||||
// ciclos cortados con nil.
|
||||
#pragma once
|
||||
|
||||
#include "data_table_logic.h"
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
// Forward declaration del C struct de Lua (definido en lua.h).
|
||||
struct lua_State;
|
||||
|
||||
namespace lua_engine {
|
||||
|
||||
struct Engine;
|
||||
|
||||
Engine* get();
|
||||
void shutdown();
|
||||
|
||||
int compile(Engine* e, const std::string& body, std::string* err_out);
|
||||
void release(Engine* e, int id);
|
||||
|
||||
struct RowCtx {
|
||||
const char* const* cells = nullptr;
|
||||
int orig_cols = 0;
|
||||
int row = 0;
|
||||
const std::vector<std::string>* header_names = nullptr;
|
||||
const std::unordered_map<std::string,int>* name_to_col = nullptr;
|
||||
|
||||
// Tipos declarados/auto-detect de las cols originales. nullptr -> heuristica.
|
||||
const data_table::ColumnType* types_orig = nullptr;
|
||||
int n_types_orig = 0;
|
||||
|
||||
// Derived cols + lookup por nombre (incluye retipo puro y formulas).
|
||||
const std::vector<data_table::DerivedColumn>* derived = nullptr;
|
||||
const std::unordered_map<std::string,int>* derived_name_to_idx = nullptr;
|
||||
};
|
||||
|
||||
std::string eval(Engine* e, int id, const RowCtx& ctx, std::string* err_out);
|
||||
|
||||
// Helper expuesto para tests: preprocesa `[col]` -> `row["col"]` respetando
|
||||
// strings y comentarios. Tambien aplica auto-return.
|
||||
std::string preprocess(const std::string& body);
|
||||
|
||||
// Acceso al lua_State subyacente. Uso restringido: tql.cpp parsea chunks
|
||||
// (return { ... }) y walks tablas. NO usar para nada que rompa el sandbox.
|
||||
::lua_State* raw_state();
|
||||
|
||||
} // namespace lua_engine
|
||||
@@ -1,89 +1,175 @@
|
||||
// Playground tables: iterador de la fn `data_table` antes de promoverla al
|
||||
// registry y migrar las apps C++ que hoy usan `ImGui::BeginTable` raw.
|
||||
|
||||
#include "app_base.h"
|
||||
#include "imgui.h"
|
||||
#include "core/logger.h"
|
||||
#include "data_table.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
struct Row {
|
||||
const char* name;
|
||||
const char* lang;
|
||||
const char* domain;
|
||||
const char* purity;
|
||||
const char* description;
|
||||
// ---------------------------------------------------------------------------
|
||||
// Dataset generador. Filas se generan con valores deterministas en funcion del
|
||||
// indice (semilla = i). Strings repetidas (lang/domain/purity/tested) usan
|
||||
// interned literals -> sin coste de memoria por fila.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
struct Dataset {
|
||||
int rows = 0;
|
||||
int cols = 10;
|
||||
std::vector<std::string> backing; // dynamic strings (name, version, deps, size, cov, date)
|
||||
std::vector<const char*> cells; // row-major pointers
|
||||
};
|
||||
|
||||
const std::vector<Row>& sample_rows() {
|
||||
static const std::vector<Row> rows = {
|
||||
{"filter_slice", "go", "core", "pure", "Filtra slice con predicado"},
|
||||
{"map_slice", "go", "core", "pure", "Aplica f a cada elemento"},
|
||||
{"reduce_slice", "go", "core", "pure", "Fold con acumulador"},
|
||||
{"sma", "py", "finance", "pure", "Simple moving average"},
|
||||
{"ema", "py", "finance", "pure", "Exponential moving average"},
|
||||
{"rsi", "py", "finance", "pure", "Relative strength index"},
|
||||
{"table_view", "cpp", "viz", "pure", "Tabla ImGui actual del registry"},
|
||||
{"line_plot", "cpp", "viz", "pure", "ImPlot line wrapper"},
|
||||
{"scatter_plot", "cpp", "viz", "pure", "ImPlot scatter wrapper"},
|
||||
{"bar_chart", "cpp", "viz", "pure", "ImPlot bar wrapper"},
|
||||
{"heatmap", "cpp", "viz", "pure", "ImPlot heatmap wrapper"},
|
||||
{"sqlite_open", "go", "infra", "impure", "Open SQLite con WAL+FK"},
|
||||
{"http_json_response", "go", "infra", "impure", "Helper JSON response"},
|
||||
{"http_parse_body", "go", "infra", "impure", "Parse JSON body"},
|
||||
{"rsync_deploy", "bash", "infra", "impure", "rsync local -> remoto"},
|
||||
{"systemd_install", "go", "infra", "impure", "Sube unit + enable + start"},
|
||||
{"systemd_restart", "go", "infra", "impure", "Restart servicio remoto"},
|
||||
{"jupyter_discover", "py", "notebook", "impure", "Descubre instancias Jupyter"},
|
||||
{"jupyter_exec", "py", "notebook", "impure", "Ejecuta celda y vuelca output"},
|
||||
{"docker_pull_image", "go", "infra", "impure", "docker pull con timeout"},
|
||||
{"graph_force_layout", "cpp", "viz", "pure", "Force-directed CPU"},
|
||||
{"graph_force_layout_gpu","cpp", "viz", "pure", "Force-directed GPU (compute)"},
|
||||
{"sql_workbench", "cpp", "core", "impure", "Workbench SQL embebido"},
|
||||
{"text_editor", "cpp", "core", "impure", "Editor de texto con highlighting"},
|
||||
{"icon_font", "cpp", "core", "impure", "Carga tabler-icons.ttf"},
|
||||
const char* const* dataset_cells(const Dataset& d) { return d.cells.data(); }
|
||||
|
||||
std::shared_ptr<Dataset> build_dataset(int rows) {
|
||||
auto d = std::make_shared<Dataset>();
|
||||
d->rows = rows;
|
||||
d->cols = 10;
|
||||
|
||||
static const char* langs[] = {"go", "py", "cpp", "bash", "ts"};
|
||||
static const char* domains[] = {"core", "viz", "infra", "finance", "notebook", "shell"};
|
||||
static const char* puritys[] = {"pure", "impure"};
|
||||
static const char* bools[] = {"true", "false"};
|
||||
|
||||
// Reserve antes de pushear -> punteros .c_str() estables.
|
||||
d->backing.reserve((size_t)rows * 6 + 16);
|
||||
d->cells.reserve((size_t)rows * 10);
|
||||
|
||||
auto add = [&](const std::string& s) -> const char* {
|
||||
d->backing.push_back(s);
|
||||
return d->backing.back().c_str();
|
||||
};
|
||||
return rows;
|
||||
|
||||
char buf[40];
|
||||
for (int i = 0; i < rows; ++i) {
|
||||
std::snprintf(buf, sizeof(buf), "fn_%07d", i);
|
||||
const char* name = add(buf);
|
||||
|
||||
const char* lang = langs[i % 5];
|
||||
const char* domain = domains[i % 6];
|
||||
const char* purity = puritys[i % 2];
|
||||
|
||||
std::snprintf(buf, sizeof(buf), "%d", (i % 5) + 1);
|
||||
const char* vmaj = add(buf);
|
||||
std::snprintf(buf, sizeof(buf), "%d", i % 7);
|
||||
const char* deps = add(buf);
|
||||
std::snprintf(buf, sizeof(buf), "%.2f", ((i * 31) % 10000) / 100.0);
|
||||
const char* size = add(buf);
|
||||
std::snprintf(buf, sizeof(buf), "%.1f", (i % 1001) / 10.0);
|
||||
const char* cov = add(buf);
|
||||
const char* tst = bools[(i * 3) % 2];
|
||||
|
||||
int y = 2024 + (i % 3);
|
||||
int m = 1 + (i % 12);
|
||||
int day = 1 + (i % 28);
|
||||
std::snprintf(buf, sizeof(buf), "%04d-%02d-%02d", y, m, day);
|
||||
const char* dt = add(buf);
|
||||
|
||||
d->cells.push_back(name);
|
||||
d->cells.push_back(lang);
|
||||
d->cells.push_back(domain);
|
||||
d->cells.push_back(purity);
|
||||
d->cells.push_back(vmaj);
|
||||
d->cells.push_back(deps);
|
||||
d->cells.push_back(size);
|
||||
d->cells.push_back(cov);
|
||||
d->cells.push_back(tst);
|
||||
d->cells.push_back(dt);
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
const char* const* flatten_cells(int& rows, int& cols) {
|
||||
static std::vector<const char*> flat;
|
||||
static bool built = false;
|
||||
if (!built) {
|
||||
const auto& src = sample_rows();
|
||||
flat.reserve(src.size() * 5);
|
||||
for (const auto& r : src) {
|
||||
flat.push_back(r.name);
|
||||
flat.push_back(r.lang);
|
||||
flat.push_back(r.domain);
|
||||
flat.push_back(r.purity);
|
||||
flat.push_back(r.description);
|
||||
}
|
||||
built = true;
|
||||
}
|
||||
rows = (int)sample_rows().size();
|
||||
cols = 5;
|
||||
return flat.data();
|
||||
std::shared_ptr<Dataset>& current_dataset() {
|
||||
static std::shared_ptr<Dataset> ds;
|
||||
if (!ds) ds = build_dataset(100);
|
||||
return ds;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void render() {
|
||||
static data_table::State st;
|
||||
if (ImGui::Begin("Tables Playground - data_table v0.1")) {
|
||||
if (ImGui::Begin("Tables Playground - data_table v0.5")) {
|
||||
ImGui::TextWrapped(
|
||||
"Iteracion 1: sort real al pulsar header, click en celda -> popup operador "
|
||||
"(=, !=, >, >=, <, <=) -> chip removible. Click derecho header: filter input, "
|
||||
"conditional color, hide column, show/hide columns.");
|
||||
"v0.5: + en chip-row anade filtro a cualquier col. Show stats muestra "
|
||||
"0/uniq/mean/min/max por header. Clipper virtualiza render -> 1M filas a 60 FPS.");
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::Text("Dataset size:");
|
||||
ImGui::SameLine();
|
||||
const int sizes[] = {100, 10000, 100000, 1000000};
|
||||
const char* labels[] = {"100", "10K", "100K", "1M"};
|
||||
for (size_t i = 0; i < sizeof(sizes)/sizeof(sizes[0]); ++i) {
|
||||
if (i > 0) ImGui::SameLine();
|
||||
bool is_active = (current_dataset()->rows == sizes[i]);
|
||||
if (is_active) ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32(80, 120, 80, 255));
|
||||
if (ImGui::SmallButton(labels[i])) {
|
||||
current_dataset() = build_dataset(sizes[i]);
|
||||
st = data_table::State{}; // reset filtros/sort/orden al cambiar dataset
|
||||
}
|
||||
if (is_active) ImGui::PopStyleColor();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("(actual: %d filas)", current_dataset()->rows);
|
||||
ImGui::Separator();
|
||||
|
||||
static const char* headers[] = {"name", "lang", "domain", "purity", "description"};
|
||||
int rows = 0, cols = 0;
|
||||
const char* const* cells = flatten_cells(rows, cols);
|
||||
data_table::render("##registry_sample", headers, cols, cells, rows, st);
|
||||
static const char* headers[] = {
|
||||
"name", "lang", "domain", "purity",
|
||||
"version_major", "deps_count", "size_kb", "coverage_pct",
|
||||
"tested", "updated_at"
|
||||
};
|
||||
static const data_table::ColumnType types[] = {
|
||||
data_table::ColumnType::String, // name
|
||||
data_table::ColumnType::String, // lang
|
||||
data_table::ColumnType::String, // domain
|
||||
data_table::ColumnType::String, // purity
|
||||
data_table::ColumnType::Int, // version_major
|
||||
data_table::ColumnType::Int, // deps_count
|
||||
data_table::ColumnType::Float, // size_kb
|
||||
data_table::ColumnType::Float, // coverage_pct
|
||||
data_table::ColumnType::Bool, // tested
|
||||
data_table::ColumnType::Date, // updated_at
|
||||
};
|
||||
// Tabla extra para demo de joins (fase 9).
|
||||
static const char* lang_info_cells[] = {
|
||||
"go", "compiled", "2009",
|
||||
"py", "interp", "1991",
|
||||
"rust", "compiled", "2010",
|
||||
"ts", "interp", "2012",
|
||||
"bash", "shell", "1989",
|
||||
"lua", "interp", "1993",
|
||||
};
|
||||
static data_table::TableInput lang_info;
|
||||
if (lang_info.name.empty()) {
|
||||
lang_info.name = "lang_info";
|
||||
lang_info.headers = {"lang", "family", "year"};
|
||||
lang_info.types = {data_table::ColumnType::String,
|
||||
data_table::ColumnType::String,
|
||||
data_table::ColumnType::Int};
|
||||
lang_info.cells = lang_info_cells;
|
||||
lang_info.rows = 6;
|
||||
lang_info.cols = 3;
|
||||
}
|
||||
|
||||
const auto& d = *current_dataset();
|
||||
static data_table::TableInput main_t;
|
||||
main_t.name = "fn_registry";
|
||||
main_t.headers = {"name", "lang", "domain", "purity",
|
||||
"version_major", "deps_count", "size_kb", "coverage_pct",
|
||||
"tested", "updated_at"};
|
||||
main_t.types = std::vector<data_table::ColumnType>(types, types + 10);
|
||||
main_t.cells = dataset_cells(d);
|
||||
main_t.rows = d.rows;
|
||||
main_t.cols = d.cols;
|
||||
|
||||
std::vector<data_table::TableInput> tables = { main_t, lang_info };
|
||||
data_table::render("##bigdata", tables, st);
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
@@ -92,11 +178,12 @@ void render() {
|
||||
int main() {
|
||||
return fn::run_app({
|
||||
.title = "Tables Playground",
|
||||
.width = 1280,
|
||||
.height = 800,
|
||||
.width = 1400,
|
||||
.height = 900,
|
||||
.about = {.name = "tables_playground",
|
||||
.version = "0.2.0",
|
||||
.description = "Playground para iterar mejoras sobre table_view antes de promover al registry."},
|
||||
.version = "0.5.0",
|
||||
.description = "Playground data_table: + add filter, stats por columna, "
|
||||
"clipper para datasets de millones."},
|
||||
.log = {.file_path = "tables_playground.log",
|
||||
.level = static_cast<int>(fn_log::Level::Info)}
|
||||
}, render);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,910 @@
|
||||
#include "tql.h"
|
||||
#include "lua_engine.h"
|
||||
|
||||
extern "C" {
|
||||
#include "lua.h"
|
||||
#include "lualib.h"
|
||||
#include "lauxlib.h"
|
||||
}
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace tql {
|
||||
|
||||
using namespace data_table;
|
||||
|
||||
namespace {
|
||||
|
||||
int find_orig_col(const std::vector<std::string>& headers, const std::string& name) {
|
||||
for (size_t i = 0; i < headers.size(); ++i) if (headers[i] == name) return (int)i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
int find_derived_idx(const std::vector<DerivedColumn>& d, const std::string& name) {
|
||||
for (size_t i = 0; i < d.size(); ++i) if (d[i].name == name) return (int)i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
Op parse_op(const std::string& s) {
|
||||
if (s == "=") return Op::Eq;
|
||||
if (s == "!=") return Op::Neq;
|
||||
if (s == ">") return Op::Gt;
|
||||
if (s == ">=") return Op::Gte;
|
||||
if (s == "<") return Op::Lt;
|
||||
if (s == "<=") return Op::Lte;
|
||||
if (s == "contains") return Op::Contains;
|
||||
if (s == "!contains") return Op::NotContains;
|
||||
if (s == "starts") return Op::StartsWith;
|
||||
if (s == "ends") return Op::EndsWith;
|
||||
return Op::Eq;
|
||||
}
|
||||
|
||||
std::string lua_to_string(lua_State* L, int idx) {
|
||||
if (lua_isnil(L, idx)) return "";
|
||||
if (lua_isboolean(L, idx)) return lua_toboolean(L, idx) ? "true" : "false";
|
||||
size_t n = 0;
|
||||
const char* s = luaL_tolstring(L, idx, &n);
|
||||
std::string out(s, n);
|
||||
lua_pop(L, 1);
|
||||
return out;
|
||||
}
|
||||
|
||||
} // anon
|
||||
|
||||
std::string lua_string_literal(const std::string& s) {
|
||||
std::string out;
|
||||
out.reserve(s.size() + 4);
|
||||
out += '"';
|
||||
for (char c : s) {
|
||||
switch (c) {
|
||||
case '\\': out += "\\\\"; break;
|
||||
case '"': out += "\\\""; break;
|
||||
case '\n': out += "\\n"; break;
|
||||
case '\r': out += "\\r"; break;
|
||||
case '\t': out += "\\t"; break;
|
||||
default:
|
||||
if ((unsigned char)c < 0x20) {
|
||||
char b[8]; std::snprintf(b, sizeof(b), "\\%d", (unsigned char)c);
|
||||
out += b;
|
||||
} else out += c;
|
||||
}
|
||||
}
|
||||
out += '"';
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string color_to_hex(unsigned int c) {
|
||||
unsigned int r = c & 0xFF;
|
||||
unsigned int g = (c >> 8) & 0xFF;
|
||||
unsigned int b = (c >> 16) & 0xFF;
|
||||
unsigned int a = (c >> 24) & 0xFF;
|
||||
char buf[16];
|
||||
if (a == 0xFF) std::snprintf(buf, sizeof(buf), "#%02x%02x%02x", r, g, b);
|
||||
else std::snprintf(buf, sizeof(buf), "#%02x%02x%02x%02x", r, g, b, a);
|
||||
return buf;
|
||||
}
|
||||
|
||||
unsigned int hex_to_color(const std::string& s) {
|
||||
if (s.size() < 7 || s[0] != '#') return 0xFFFFFFFF;
|
||||
auto hex2 = [&](size_t i) -> unsigned int {
|
||||
unsigned int v = 0;
|
||||
if (i + 1 < s.size()) std::sscanf(s.c_str() + i, "%2x", &v);
|
||||
return v;
|
||||
};
|
||||
unsigned int r = hex2(1), g = hex2(3), b = hex2(5);
|
||||
unsigned int a = (s.size() >= 9) ? hex2(7) : 0xFF;
|
||||
return r | (g << 8) | (b << 16) | (a << 24);
|
||||
}
|
||||
|
||||
ColumnType column_type_from_string(const std::string& s) {
|
||||
if (s == "string") return ColumnType::String;
|
||||
if (s == "int") return ColumnType::Int;
|
||||
if (s == "float") return ColumnType::Float;
|
||||
if (s == "bool") return ColumnType::Bool;
|
||||
if (s == "date") return ColumnType::Date;
|
||||
if (s == "json") return ColumnType::Json;
|
||||
return ColumnType::Auto;
|
||||
}
|
||||
|
||||
// Helper: header del Stage 0 dado un col idx eff. Para stages 1+ no aplica
|
||||
// (los stage outputs tienen sus propios headers).
|
||||
namespace {
|
||||
const char* agg_fn_token(AggFn f) {
|
||||
switch (f) {
|
||||
case AggFn::Count: return "count";
|
||||
case AggFn::Sum: return "sum";
|
||||
case AggFn::Avg: return "avg";
|
||||
case AggFn::Min: return "min";
|
||||
case AggFn::Max: return "max";
|
||||
case AggFn::Distinct: return "distinct";
|
||||
case AggFn::Stddev: return "stddev";
|
||||
case AggFn::Median: return "median";
|
||||
case AggFn::P25: return "p25";
|
||||
case AggFn::P75: return "p75";
|
||||
case AggFn::P90: return "p90";
|
||||
case AggFn::P99: return "p99";
|
||||
case AggFn::Percentile: return "percentile";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
AggFn agg_fn_from_string(const std::string& s) {
|
||||
if (s == "count") return AggFn::Count;
|
||||
if (s == "sum") return AggFn::Sum;
|
||||
if (s == "avg") return AggFn::Avg;
|
||||
if (s == "min") return AggFn::Min;
|
||||
if (s == "max") return AggFn::Max;
|
||||
if (s == "distinct") return AggFn::Distinct;
|
||||
if (s == "stddev") return AggFn::Stddev;
|
||||
if (s == "median") return AggFn::Median;
|
||||
if (s == "p25") return AggFn::P25;
|
||||
if (s == "p75") return AggFn::P75;
|
||||
if (s == "p90") return AggFn::P90;
|
||||
if (s == "p99") return AggFn::P99;
|
||||
if (s == "percentile") return AggFn::Percentile;
|
||||
return AggFn::Count;
|
||||
}
|
||||
} // anon
|
||||
|
||||
std::string emit(const State& state,
|
||||
const std::vector<std::string>& headers,
|
||||
const std::vector<ColumnType>& types)
|
||||
{
|
||||
int orig_cols = (int)headers.size();
|
||||
const Stage& raw = state.raw();
|
||||
int eff_cols = orig_cols + (int)raw.derived.size();
|
||||
|
||||
// Build effective headers + types (same indexing as col_visible/order)
|
||||
std::vector<std::string> eff_headers(eff_cols);
|
||||
std::vector<ColumnType> eff_types(eff_cols);
|
||||
for (int c = 0; c < orig_cols; ++c) {
|
||||
eff_headers[c] = headers[c];
|
||||
eff_types[c] = (c < (int)types.size()) ? types[c] : ColumnType::Auto;
|
||||
}
|
||||
for (int k = 0; k < (int)raw.derived.size(); ++k) {
|
||||
eff_headers[orig_cols + k] = raw.derived[k].name;
|
||||
eff_types[orig_cols + k] = raw.derived[k].type;
|
||||
}
|
||||
|
||||
// Build order positions: col_idx -> visual order (1-based)
|
||||
std::unordered_map<int, int> order_pos;
|
||||
for (size_t i = 0; i < state.col_order.size(); ++i) {
|
||||
order_pos[state.col_order[i]] = (int)i + 1;
|
||||
}
|
||||
|
||||
auto emit_filter_block = [&](const std::vector<Filter>& filters,
|
||||
const std::vector<std::string>& stage_headers,
|
||||
const char* indent) -> std::string {
|
||||
if (filters.empty()) return {};
|
||||
std::string s;
|
||||
s += indent; s += "filter = {\n";
|
||||
for (const auto& f : filters) {
|
||||
std::string col_name = (f.col >= 0 && f.col < (int)stage_headers.size())
|
||||
? stage_headers[f.col] : "";
|
||||
s += indent; s += " {";
|
||||
s += lua_string_literal(op_label(f.op));
|
||||
s += ", ";
|
||||
s += lua_string_literal(col_name);
|
||||
s += ", ";
|
||||
s += lua_string_literal(f.value);
|
||||
s += "},\n";
|
||||
}
|
||||
s += indent; s += "},\n";
|
||||
return s;
|
||||
};
|
||||
|
||||
auto emit_sort_block = [&](const std::vector<SortClause>& sorts,
|
||||
const char* indent) -> std::string {
|
||||
if (sorts.empty()) return {};
|
||||
std::string s;
|
||||
s += indent; s += "sort = {\n";
|
||||
for (const auto& sc : sorts) {
|
||||
s += indent; s += " {";
|
||||
s += lua_string_literal(sc.desc ? "desc" : "asc");
|
||||
s += ", ";
|
||||
s += lua_string_literal(sc.col);
|
||||
s += "},\n";
|
||||
}
|
||||
s += indent; s += "},\n";
|
||||
return s;
|
||||
};
|
||||
|
||||
std::string out;
|
||||
out += "-- TQL v1 (Table Query Language). Round-trip de State <-> Lua.\n";
|
||||
out += "-- Schema:\n";
|
||||
out += "-- version = 1 -- bump si breaking change\n";
|
||||
out += "-- display = \"table\" -- table|bar|line|pie (futuro)\n";
|
||||
out += "-- stages = { stage0, stage1, ... } -- pipeline; stage 0 = Raw\n";
|
||||
out += "-- columns = { {name,type,visible,order,color_rules}, ... }\n";
|
||||
out += "--\n";
|
||||
out += "-- Stage 0 (Raw): filter + expressions + sort\n";
|
||||
out += "-- Stage N (Grouped): filter + breakout + aggregation + sort\n";
|
||||
out += "--\n";
|
||||
out += "-- filter: {{op, col, val}, ...} op in =,!=,>,>=,<,<=,contains,!contains,starts,ends\n";
|
||||
out += "-- expressions: {[name] = \"lua_body\"} ej: [\"total\"] = \"return [a] + [b]\"\n";
|
||||
out += "-- breakout: {\"col1\", \"col2\"} group by\n";
|
||||
out += "-- aggregation: {{fn, col, arg?}, ...} fn in count,sum,avg,min,max,distinct,stddev,median,p25,p75,p90,p99,percentile\n";
|
||||
out += "-- sort: {{dir, col}, ...} dir in asc,desc\n";
|
||||
out += "return {\n";
|
||||
out += " version = 1,\n";
|
||||
out += " display = ";
|
||||
out += lua_string_literal(view_mode_token(state.display));
|
||||
out += ",\n";
|
||||
if (!state.main_source.empty()) {
|
||||
out += " main_source = ";
|
||||
out += lua_string_literal(state.main_source);
|
||||
out += ",\n";
|
||||
}
|
||||
|
||||
// joins (antes de stages, materializa input)
|
||||
if (!state.joins.empty()) {
|
||||
out += " joins = {\n";
|
||||
for (const auto& jn : state.joins) {
|
||||
out += " {alias = " + lua_string_literal(jn.alias);
|
||||
out += ", source = " + lua_string_literal(jn.source);
|
||||
out += ", strategy = " + lua_string_literal(join_strategy_token(jn.strategy));
|
||||
out += ", on = {";
|
||||
for (size_t i = 0; i < jn.on.size(); ++i) {
|
||||
if (i) out += ", ";
|
||||
out += "{" + lua_string_literal(jn.on[i].first) + ", "
|
||||
+ lua_string_literal(jn.on[i].second) + "}";
|
||||
}
|
||||
out += "}";
|
||||
if (!jn.fields.empty()) {
|
||||
out += ", fields = {";
|
||||
for (size_t i = 0; i < jn.fields.size(); ++i) {
|
||||
if (i) out += ", ";
|
||||
out += lua_string_literal(jn.fields[i]);
|
||||
}
|
||||
out += "}";
|
||||
}
|
||||
out += "},\n";
|
||||
}
|
||||
out += " },\n";
|
||||
}
|
||||
|
||||
out += " stages = {\n";
|
||||
|
||||
// Recorre todos los stages; stage 0 tiene formato Raw (filter+expr+sort),
|
||||
// stages 1+ tienen formato Grouped (filter+breakout+aggregation+sort).
|
||||
// Headers para resolver col indices de filters/sorts se computan stage por
|
||||
// stage simulando la cadena.
|
||||
std::vector<std::string> cur_headers = headers; // stage input headers
|
||||
// Para stage 0 raw, los headers incluyen orig + derived.
|
||||
// Construye cur_headers iniciales (= orig); derived se anaden al pasar stage 0.
|
||||
|
||||
for (int si = 0; si < (int)state.stages.size(); ++si) {
|
||||
const Stage& stg = state.stages[si];
|
||||
out += " {\n";
|
||||
|
||||
if (si == 0) {
|
||||
// Stage 0: orig headers + derived seran disponibles tras expressions.
|
||||
// Para los filter col indices, asumimos van con cur_headers = orig.
|
||||
// (data_table.cpp solo aplica filters a orig cols al guardar; si en
|
||||
// futuro stage 0 admite filter sobre derived, se traduce a name.)
|
||||
std::vector<std::string> s0_headers = headers;
|
||||
// Filters
|
||||
out += emit_filter_block(stg.filters, s0_headers, " ");
|
||||
|
||||
// Expressions
|
||||
if (!stg.derived.empty()) {
|
||||
bool any = false;
|
||||
for (const auto& d : stg.derived) if (!d.formula.empty()) { any = true; break; }
|
||||
if (any) {
|
||||
out += " expressions = {\n";
|
||||
for (const auto& d : stg.derived) {
|
||||
if (d.formula.empty()) continue;
|
||||
out += " [";
|
||||
out += lua_string_literal(d.name);
|
||||
out += "] = ";
|
||||
out += lua_string_literal(d.formula);
|
||||
out += ",\n";
|
||||
}
|
||||
out += " },\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Sort (sort.col es string en nuevo modelo).
|
||||
out += emit_sort_block(stg.sorts, " ");
|
||||
|
||||
// Avanza cur_headers para siguiente stage: orig + derived.
|
||||
for (const auto& d : stg.derived) cur_headers.push_back(d.name);
|
||||
} else {
|
||||
// Stage 1+: filter (sobre output del previo, cur_headers).
|
||||
out += emit_filter_block(stg.filters, cur_headers, " ");
|
||||
|
||||
// breakout
|
||||
if (!stg.breakouts.empty()) {
|
||||
out += " breakout = {";
|
||||
for (size_t i = 0; i < stg.breakouts.size(); ++i) {
|
||||
if (i > 0) out += ", ";
|
||||
out += lua_string_literal(stg.breakouts[i]);
|
||||
}
|
||||
out += "},\n";
|
||||
}
|
||||
|
||||
// aggregation
|
||||
if (!stg.aggregations.empty()) {
|
||||
out += " aggregation = {\n";
|
||||
for (const auto& a : stg.aggregations) {
|
||||
out += " {";
|
||||
out += lua_string_literal(agg_fn_token(a.fn));
|
||||
if (a.fn != AggFn::Count) {
|
||||
out += ", ";
|
||||
out += lua_string_literal(a.col);
|
||||
}
|
||||
if (a.fn == AggFn::Percentile) {
|
||||
char buf[32]; std::snprintf(buf, sizeof(buf), "%g", a.arg);
|
||||
out += ", "; out += buf;
|
||||
}
|
||||
out += "},\n";
|
||||
}
|
||||
out += " },\n";
|
||||
}
|
||||
|
||||
// sort
|
||||
out += emit_sort_block(stg.sorts, " ");
|
||||
|
||||
// Avanza cur_headers para siguiente stage: breakouts + agg aliases.
|
||||
std::vector<std::string> next;
|
||||
for (const auto& b : stg.breakouts) next.push_back(b);
|
||||
for (const auto& a : stg.aggregations) next.push_back(aggregation_alias(a));
|
||||
cur_headers = std::move(next);
|
||||
}
|
||||
|
||||
out += " },\n";
|
||||
}
|
||||
out += " },\n";
|
||||
|
||||
// columns (per-col render config) — siempre referidas a los effective cols
|
||||
// del STAGE 0 (asumimos viz state para stage 0 / raw). Renderizar columns
|
||||
// por cada stage no aporta v1.
|
||||
out += " columns = {\n";
|
||||
for (int c = 0; c < eff_cols; ++c) {
|
||||
out += " {";
|
||||
out += "name = " + lua_string_literal(eff_headers[c]);
|
||||
out += ", type = " + lua_string_literal(column_type_name(eff_types[c]));
|
||||
bool vis = (c < (int)state.col_visible.size()) ? state.col_visible[c] : true;
|
||||
out += std::string(", visible = ") + (vis ? "true" : "false");
|
||||
int order = order_pos.count(c) ? order_pos[c] : c + 1;
|
||||
out += ", order = " + std::to_string(order);
|
||||
// color rules for this col
|
||||
bool first = true;
|
||||
for (const auto& cr : state.color_rules) {
|
||||
if (cr.col != c) continue;
|
||||
if (first) { out += ", color_rules = {"; first = false; }
|
||||
else { out += ", "; }
|
||||
out += "{equals = " + lua_string_literal(cr.equals);
|
||||
out += ", color = " + lua_string_literal(color_to_hex(cr.color)) + "}";
|
||||
}
|
||||
if (!first) out += "}";
|
||||
out += "},\n";
|
||||
}
|
||||
out += " },\n";
|
||||
|
||||
// views (extra viz panels — viz adicional sobre mismos stages)
|
||||
auto emit_view = [&](const VizPanel& p) -> std::string {
|
||||
std::string s = " {";
|
||||
s += "display = " + lua_string_literal(view_mode_token(p.display));
|
||||
if (!p.config.x_col.empty()) s += ", x_col = " + lua_string_literal(p.config.x_col);
|
||||
if (!p.config.cat_col.empty()) s += ", cat_col = " + lua_string_literal(p.config.cat_col);
|
||||
if (!p.config.size_col.empty()) s += ", size_col = "+ lua_string_literal(p.config.size_col);
|
||||
if (!p.config.y_cols.empty()) {
|
||||
s += ", y_cols = {";
|
||||
for (size_t i = 0; i < p.config.y_cols.size(); ++i) {
|
||||
if (i) s += ", ";
|
||||
s += lua_string_literal(p.config.y_cols[i]);
|
||||
}
|
||||
s += "}";
|
||||
}
|
||||
if (p.config.primary_color != 0)
|
||||
s += ", color = " + lua_string_literal(color_to_hex(p.config.primary_color));
|
||||
if (p.config.hist_bins > 0)
|
||||
s += ", hist_bins = " + std::to_string(p.config.hist_bins);
|
||||
if (p.config.pie_radius > 0)
|
||||
s += ", pie_radius = " + std::to_string(p.config.pie_radius);
|
||||
if (!p.config.show_legend) s += ", show_legend = false";
|
||||
if (p.config.show_markers) s += ", show_markers = true";
|
||||
if (p.config.locked) s += ", locked = true";
|
||||
s += "},\n";
|
||||
return s;
|
||||
};
|
||||
|
||||
out += " views = {\n";
|
||||
// Panel 0 = main viz
|
||||
VizPanel main_p;
|
||||
main_p.display = state.display;
|
||||
main_p.config = state.viz_config;
|
||||
out += emit_view(main_p);
|
||||
for (const auto& p : state.extra_panels) out += emit_view(p);
|
||||
out += " },\n";
|
||||
|
||||
out += " visualization_settings = {},\n";
|
||||
out += "}\n";
|
||||
return out;
|
||||
}
|
||||
|
||||
bool apply(const std::string& lua_text, State& state,
|
||||
const std::vector<std::string>& headers,
|
||||
const std::vector<ColumnType>& /*types*/,
|
||||
const char* const* cells, int rows, int orig_cols,
|
||||
std::string* err)
|
||||
{
|
||||
std::vector<std::string> warns;
|
||||
auto warn = [&](const std::string& m) { warns.push_back(m); };
|
||||
auto finish_with_warns = [&](bool ok) -> bool {
|
||||
if (err && !warns.empty()) {
|
||||
std::string j;
|
||||
for (size_t i = 0; i < warns.size(); ++i) {
|
||||
if (i) j += "; ";
|
||||
j += warns[i];
|
||||
}
|
||||
*err = j;
|
||||
}
|
||||
return ok;
|
||||
};
|
||||
|
||||
lua_State* L = lua_engine::raw_state();
|
||||
if (!L) { if (err) *err = "lua engine null"; return false; }
|
||||
|
||||
if (luaL_loadbufferx(L, lua_text.data(), lua_text.size(), "tql", "t") != LUA_OK) {
|
||||
if (err) *err = lua_tostring(L, -1) ? lua_tostring(L, -1) : "load error";
|
||||
lua_pop(L, 1);
|
||||
return false;
|
||||
}
|
||||
if (lua_pcall(L, 0, 1, 0) != LUA_OK) {
|
||||
if (err) *err = lua_tostring(L, -1) ? lua_tostring(L, -1) : "exec error";
|
||||
lua_pop(L, 1);
|
||||
return false;
|
||||
}
|
||||
if (!lua_istable(L, -1)) {
|
||||
if (err) *err = "TQL root must be a table";
|
||||
lua_pop(L, 1);
|
||||
return false;
|
||||
}
|
||||
|
||||
// main_source
|
||||
lua_getfield(L, -1, "main_source");
|
||||
if (lua_isstring(L, -1)) state.main_source = lua_tostring(L, -1);
|
||||
else state.main_source.clear();
|
||||
lua_pop(L, 1);
|
||||
|
||||
// display
|
||||
lua_getfield(L, -1, "display");
|
||||
if (lua_isstring(L, -1)) {
|
||||
std::string d = lua_tostring(L, -1);
|
||||
ViewMode m = view_mode_from_token(d.c_str());
|
||||
state.display = m;
|
||||
if (d != "table" && std::strcmp(view_mode_token(m), d.c_str()) != 0) {
|
||||
warn("unknown display \"" + d + "\" (defaulting to table)");
|
||||
}
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
|
||||
// Validar version.
|
||||
lua_getfield(L, -1, "version");
|
||||
if (lua_isnil(L, -1)) {
|
||||
warn("version missing (assuming 1)");
|
||||
} else if (!lua_isnumber(L, -1)) {
|
||||
if (err) *err = "version must be a number";
|
||||
lua_pop(L, 2);
|
||||
return false;
|
||||
} else {
|
||||
int v = (int)lua_tointeger(L, -1);
|
||||
if (v != 1) {
|
||||
char buf[64]; std::snprintf(buf, sizeof(buf), "unsupported TQL version %d (expected 1)", v);
|
||||
if (err) *err = buf;
|
||||
lua_pop(L, 2);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
|
||||
// Reset partes mutables. Liberar lua_ids antes.
|
||||
for (auto& s : state.stages) {
|
||||
for (auto& d : s.derived) {
|
||||
if (d.lua_id >= 0) lua_engine::release(lua_engine::get(), d.lua_id);
|
||||
}
|
||||
}
|
||||
state.stages.clear();
|
||||
state.active_stage = 0;
|
||||
state.color_rules.clear();
|
||||
|
||||
// ---- Walk joins[] ----
|
||||
state.joins.clear();
|
||||
lua_getfield(L, -1, "joins");
|
||||
if (lua_istable(L, -1)) {
|
||||
int nj = (int)lua_rawlen(L, -1);
|
||||
for (int i = 1; i <= nj; ++i) {
|
||||
lua_rawgeti(L, -1, i);
|
||||
if (!lua_istable(L, -1)) { lua_pop(L, 1); continue; }
|
||||
Join jn;
|
||||
lua_getfield(L, -1, "alias");
|
||||
if (lua_isstring(L, -1)) jn.alias = lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
lua_getfield(L, -1, "source");
|
||||
if (lua_isstring(L, -1)) jn.source = lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
lua_getfield(L, -1, "strategy");
|
||||
if (lua_isstring(L, -1)) jn.strategy = join_strategy_from_token(lua_tostring(L, -1));
|
||||
lua_pop(L, 1);
|
||||
lua_getfield(L, -1, "on");
|
||||
if (lua_istable(L, -1)) {
|
||||
int on_n = (int)lua_rawlen(L, -1);
|
||||
for (int k = 1; k <= on_n; ++k) {
|
||||
lua_rawgeti(L, -1, k);
|
||||
if (lua_istable(L, -1) && lua_rawlen(L, -1) >= 2) {
|
||||
lua_rawgeti(L, -1, 1); std::string a = lua_to_string(L, -1); lua_pop(L, 1);
|
||||
lua_rawgeti(L, -1, 2); std::string b = lua_to_string(L, -1); lua_pop(L, 1);
|
||||
jn.on.push_back({a, b});
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
lua_getfield(L, -1, "fields");
|
||||
if (lua_istable(L, -1)) {
|
||||
int fn_n = (int)lua_rawlen(L, -1);
|
||||
for (int k = 1; k <= fn_n; ++k) {
|
||||
lua_rawgeti(L, -1, k);
|
||||
if (lua_isstring(L, -1)) jn.fields.emplace_back(lua_tostring(L, -1));
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
state.joins.push_back(jn);
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
|
||||
// ---- Walk stages[] ----
|
||||
lua_getfield(L, -1, "stages");
|
||||
if (lua_istable(L, -1)) {
|
||||
int n_stages = (int)lua_rawlen(L, -1);
|
||||
// Headers efectivos por stage para resolver filter/sort col indices.
|
||||
std::vector<std::string> cur_headers = headers;
|
||||
|
||||
for (int si = 1; si <= n_stages; ++si) {
|
||||
lua_rawgeti(L, -1, si);
|
||||
if (!lua_istable(L, -1)) { lua_pop(L, 1); continue; }
|
||||
|
||||
Stage stg;
|
||||
|
||||
// Stage 0 expressions (solo aplica a si == 1, pero permitimos en
|
||||
// cualquier stage por simetria — el UI no las expone en stages 1+).
|
||||
lua_getfield(L, -1, "expressions");
|
||||
if (lua_istable(L, -1)) {
|
||||
lua_pushnil(L);
|
||||
while (lua_next(L, -2) != 0) {
|
||||
if (lua_isstring(L, -2) && lua_isstring(L, -1)) {
|
||||
std::string name = lua_tostring(L, -2);
|
||||
std::string formula = lua_tostring(L, -1);
|
||||
std::string cerr;
|
||||
int id = lua_engine::compile(lua_engine::get(), formula, &cerr);
|
||||
DerivedColumn d;
|
||||
d.source_col = -1;
|
||||
d.name = name;
|
||||
d.formula = formula;
|
||||
d.lua_id = id;
|
||||
d.compile_error = (id < 0) ? cerr : "";
|
||||
if (id >= 0 && si == 1) {
|
||||
// auto-detect tipo via sample (solo para stage 0).
|
||||
int sample = std::min(64, rows);
|
||||
std::vector<std::string> samples_str;
|
||||
std::vector<const char*> samples_ptr;
|
||||
std::vector<std::string> hn_storage = headers;
|
||||
std::unordered_map<std::string, int> n2c;
|
||||
for (int c = 0; c < orig_cols && c < (int)hn_storage.size(); ++c) {
|
||||
n2c[hn_storage[c]] = c;
|
||||
}
|
||||
for (int r = 0; r < sample; ++r) {
|
||||
lua_engine::RowCtx ctx;
|
||||
ctx.cells = cells;
|
||||
ctx.orig_cols = orig_cols;
|
||||
ctx.row = r;
|
||||
ctx.header_names = &hn_storage;
|
||||
ctx.name_to_col = &n2c;
|
||||
std::string e;
|
||||
samples_str.emplace_back(
|
||||
lua_engine::eval(lua_engine::get(), id, ctx, &e));
|
||||
}
|
||||
for (auto& s : samples_str) samples_ptr.push_back(s.c_str());
|
||||
d.type = auto_detect_type(samples_ptr.data(),
|
||||
(int)samples_ptr.size(), 1, 0);
|
||||
} else {
|
||||
d.type = ColumnType::String;
|
||||
}
|
||||
stg.derived.push_back(d);
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
|
||||
// filter
|
||||
lua_getfield(L, -1, "filter");
|
||||
if (lua_istable(L, -1)) {
|
||||
int n = (int)lua_rawlen(L, -1);
|
||||
for (int i = 1; i <= n; ++i) {
|
||||
lua_rawgeti(L, -1, i);
|
||||
if (lua_istable(L, -1) && lua_rawlen(L, -1) >= 3) {
|
||||
lua_rawgeti(L, -1, 1); std::string op = lua_to_string(L, -1); lua_pop(L, 1);
|
||||
lua_rawgeti(L, -1, 2); std::string col_name = lua_to_string(L, -1); lua_pop(L, 1);
|
||||
lua_rawgeti(L, -1, 3); std::string val = lua_to_string(L, -1); lua_pop(L, 1);
|
||||
int ci = find_orig_col(cur_headers, col_name);
|
||||
if (ci >= 0) {
|
||||
stg.filters.push_back({ci, parse_op(op), val});
|
||||
} else {
|
||||
warn("stage " + std::to_string(si - 1) + ": filter col \"" + col_name + "\" not found");
|
||||
}
|
||||
if (op != "=" && op != "!=" && op != ">" && op != ">=" &&
|
||||
op != "<" && op != "<=" && op != "contains" &&
|
||||
op != "!contains" && op != "starts" && op != "ends") {
|
||||
warn("stage " + std::to_string(si - 1) + ": unknown filter op \"" + op + "\" (defaulting to =)");
|
||||
}
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
|
||||
// breakout (solo aplica stages >= 1, no-op silencioso si stage 0)
|
||||
lua_getfield(L, -1, "breakout");
|
||||
if (lua_istable(L, -1)) {
|
||||
int n = (int)lua_rawlen(L, -1);
|
||||
for (int i = 1; i <= n; ++i) {
|
||||
lua_rawgeti(L, -1, i);
|
||||
if (lua_isstring(L, -1)) {
|
||||
std::string bn = lua_tostring(L, -1);
|
||||
if (find_orig_col(cur_headers, bn) < 0) {
|
||||
warn("stage " + std::to_string(si - 1) + ": breakout col \"" + bn + "\" not in input headers");
|
||||
}
|
||||
stg.breakouts.emplace_back(bn);
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
|
||||
// aggregation
|
||||
lua_getfield(L, -1, "aggregation");
|
||||
if (lua_istable(L, -1)) {
|
||||
int n = (int)lua_rawlen(L, -1);
|
||||
for (int i = 1; i <= n; ++i) {
|
||||
lua_rawgeti(L, -1, i);
|
||||
if (lua_istable(L, -1) && lua_rawlen(L, -1) >= 1) {
|
||||
Aggregation a;
|
||||
lua_rawgeti(L, -1, 1);
|
||||
std::string fn_name = lua_to_string(L, -1);
|
||||
lua_pop(L, 1);
|
||||
bool known = (fn_name == "count" || fn_name == "sum" || fn_name == "avg" ||
|
||||
fn_name == "min" || fn_name == "max" || fn_name == "distinct" ||
|
||||
fn_name == "stddev"|| fn_name == "median" ||
|
||||
fn_name == "p25" || fn_name == "p75" || fn_name == "p90" ||
|
||||
fn_name == "p99" || fn_name == "percentile");
|
||||
if (!known) {
|
||||
warn("stage " + std::to_string(si - 1) + ": unknown aggregation fn \"" + fn_name + "\" (defaulting to count)");
|
||||
}
|
||||
a.fn = agg_fn_from_string(fn_name);
|
||||
if (lua_rawlen(L, -1) >= 2) {
|
||||
lua_rawgeti(L, -1, 2);
|
||||
a.col = lua_to_string(L, -1);
|
||||
lua_pop(L, 1);
|
||||
if (a.fn != AggFn::Count && find_orig_col(cur_headers, a.col) < 0) {
|
||||
warn("stage " + std::to_string(si - 1) + ": aggregation col \"" + a.col + "\" not in input headers");
|
||||
}
|
||||
} else if (a.fn != AggFn::Count) {
|
||||
warn("stage " + std::to_string(si - 1) + ": aggregation \"" + fn_name + "\" requires a column");
|
||||
}
|
||||
if (lua_rawlen(L, -1) >= 3) {
|
||||
lua_rawgeti(L, -1, 3);
|
||||
if (lua_isnumber(L, -1)) a.arg = lua_tonumber(L, -1);
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
stg.aggregations.push_back(a);
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
|
||||
// sort
|
||||
lua_getfield(L, -1, "sort");
|
||||
if (lua_istable(L, -1)) {
|
||||
int n = (int)lua_rawlen(L, -1);
|
||||
for (int i = 1; i <= n; ++i) {
|
||||
lua_rawgeti(L, -1, i);
|
||||
if (lua_istable(L, -1) && lua_rawlen(L, -1) >= 2) {
|
||||
lua_rawgeti(L, -1, 1); std::string dir = lua_to_string(L, -1); lua_pop(L, 1);
|
||||
lua_rawgeti(L, -1, 2); std::string col = lua_to_string(L, -1); lua_pop(L, 1);
|
||||
SortClause sc;
|
||||
sc.col = col;
|
||||
sc.desc = (dir == "desc");
|
||||
if (dir != "asc" && dir != "desc") {
|
||||
warn("stage " + std::to_string(si - 1) + ": unknown sort dir \"" + dir + "\" (defaulting to asc)");
|
||||
}
|
||||
stg.sorts.push_back(sc);
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
|
||||
state.stages.push_back(std::move(stg));
|
||||
|
||||
// Advance cur_headers para resolver filter/sort col del siguiente stage.
|
||||
const Stage& last = state.stages.back();
|
||||
if (si == 1) {
|
||||
// Stage 0: cur_headers = orig + derived (sin breakouts/agg).
|
||||
for (const auto& d : last.derived) cur_headers.push_back(d.name);
|
||||
} else {
|
||||
if (!last.breakouts.empty() || !last.aggregations.empty()) {
|
||||
std::vector<std::string> next;
|
||||
for (const auto& b : last.breakouts) next.push_back(b);
|
||||
for (const auto& a : last.aggregations) next.push_back(aggregation_alias(a));
|
||||
cur_headers = std::move(next);
|
||||
}
|
||||
}
|
||||
|
||||
lua_pop(L, 1); // pop stage entry
|
||||
}
|
||||
}
|
||||
lua_pop(L, 1); // stages
|
||||
|
||||
state.ensure_stage0();
|
||||
|
||||
// ---- Walk columns (per-col render config) ----
|
||||
int eff_cols = orig_cols + (int)state.raw().derived.size();
|
||||
lua_getfield(L, -1, "columns");
|
||||
if (lua_istable(L, -1)) {
|
||||
state.col_visible.assign(eff_cols, true);
|
||||
std::vector<std::pair<int,int>> order_pairs;
|
||||
std::vector<bool> seen(eff_cols, false);
|
||||
|
||||
int n = (int)lua_rawlen(L, -1);
|
||||
for (int i = 1; i <= n; ++i) {
|
||||
lua_rawgeti(L, -1, i);
|
||||
if (!lua_istable(L, -1)) { lua_pop(L, 1); continue; }
|
||||
|
||||
lua_getfield(L, -1, "name");
|
||||
std::string nm = lua_to_string(L, -1);
|
||||
lua_pop(L, 1);
|
||||
|
||||
int col_idx = find_orig_col(headers, nm);
|
||||
if (col_idx < 0) {
|
||||
int di = find_derived_idx(state.raw().derived, nm);
|
||||
if (di >= 0) col_idx = orig_cols + di;
|
||||
}
|
||||
if (col_idx < 0 || col_idx >= eff_cols) { lua_pop(L, 1); continue; }
|
||||
seen[col_idx] = true;
|
||||
|
||||
// visible
|
||||
lua_getfield(L, -1, "visible");
|
||||
if (lua_isboolean(L, -1)) state.col_visible[col_idx] = lua_toboolean(L, -1);
|
||||
lua_pop(L, 1);
|
||||
|
||||
// order
|
||||
lua_getfield(L, -1, "order");
|
||||
int order_val = lua_isnumber(L, -1) ? (int)lua_tointeger(L, -1) : (col_idx + 1);
|
||||
lua_pop(L, 1);
|
||||
order_pairs.emplace_back(order_val, col_idx);
|
||||
|
||||
// type (mutable solo para derived)
|
||||
lua_getfield(L, -1, "type");
|
||||
if (lua_isstring(L, -1)) {
|
||||
std::string tn = lua_tostring(L, -1);
|
||||
ColumnType t = column_type_from_string(tn);
|
||||
if (col_idx >= orig_cols) {
|
||||
state.raw().derived[col_idx - orig_cols].type = t;
|
||||
}
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
|
||||
// color_rules
|
||||
lua_getfield(L, -1, "color_rules");
|
||||
if (lua_istable(L, -1)) {
|
||||
int rn = (int)lua_rawlen(L, -1);
|
||||
for (int j = 1; j <= rn; ++j) {
|
||||
lua_rawgeti(L, -1, j);
|
||||
if (lua_istable(L, -1)) {
|
||||
lua_getfield(L, -1, "equals");
|
||||
std::string eq = lua_to_string(L, -1);
|
||||
lua_pop(L, 1);
|
||||
lua_getfield(L, -1, "color");
|
||||
std::string hx = lua_to_string(L, -1);
|
||||
lua_pop(L, 1);
|
||||
state.color_rules.push_back({col_idx, eq, hex_to_color(hx)});
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
|
||||
lua_pop(L, 1); // pop entry
|
||||
}
|
||||
|
||||
std::sort(order_pairs.begin(), order_pairs.end());
|
||||
state.col_order.clear();
|
||||
for (auto& p : order_pairs) state.col_order.push_back(p.second);
|
||||
for (int c = 0; c < eff_cols; ++c) if (!seen[c]) state.col_order.push_back(c);
|
||||
}
|
||||
lua_pop(L, 1); // columns
|
||||
|
||||
// ---- Walk views[] (extra viz panels) ----
|
||||
state.extra_panels.clear();
|
||||
lua_getfield(L, -1, "views");
|
||||
if (lua_istable(L, -1)) {
|
||||
int n = (int)lua_rawlen(L, -1);
|
||||
for (int i = 1; i <= n; ++i) {
|
||||
lua_rawgeti(L, -1, i);
|
||||
if (!lua_istable(L, -1)) { lua_pop(L, 1); continue; }
|
||||
VizPanel p;
|
||||
lua_getfield(L, -1, "display");
|
||||
if (lua_isstring(L, -1)) p.display = view_mode_from_token(lua_tostring(L, -1));
|
||||
lua_pop(L, 1);
|
||||
|
||||
auto read_str = [&](const char* key, std::string& out_s) {
|
||||
lua_getfield(L, -1, key);
|
||||
if (lua_isstring(L, -1)) out_s = lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
};
|
||||
read_str("x_col", p.config.x_col);
|
||||
read_str("cat_col", p.config.cat_col);
|
||||
read_str("size_col", p.config.size_col);
|
||||
|
||||
lua_getfield(L, -1, "y_cols");
|
||||
if (lua_istable(L, -1)) {
|
||||
int yn = (int)lua_rawlen(L, -1);
|
||||
for (int j = 1; j <= yn; ++j) {
|
||||
lua_rawgeti(L, -1, j);
|
||||
if (lua_isstring(L, -1)) p.config.y_cols.emplace_back(lua_tostring(L, -1));
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
|
||||
lua_getfield(L, -1, "color");
|
||||
if (lua_isstring(L, -1)) p.config.primary_color = hex_to_color(lua_tostring(L, -1));
|
||||
lua_pop(L, 1);
|
||||
|
||||
lua_getfield(L, -1, "hist_bins");
|
||||
if (lua_isnumber(L, -1)) p.config.hist_bins = (int)lua_tointeger(L, -1);
|
||||
lua_pop(L, 1);
|
||||
|
||||
lua_getfield(L, -1, "pie_radius");
|
||||
if (lua_isnumber(L, -1)) p.config.pie_radius = (float)lua_tonumber(L, -1);
|
||||
lua_pop(L, 1);
|
||||
|
||||
lua_getfield(L, -1, "show_legend");
|
||||
if (lua_isboolean(L, -1)) p.config.show_legend = lua_toboolean(L, -1);
|
||||
lua_pop(L, 1);
|
||||
|
||||
lua_getfield(L, -1, "show_markers");
|
||||
if (lua_isboolean(L, -1)) p.config.show_markers = lua_toboolean(L, -1);
|
||||
lua_pop(L, 1);
|
||||
|
||||
lua_getfield(L, -1, "locked");
|
||||
if (lua_isboolean(L, -1)) p.config.locked = lua_toboolean(L, -1);
|
||||
lua_pop(L, 1);
|
||||
|
||||
// Panel 0 = main viz (state.display + state.viz_config).
|
||||
if (i == 1) {
|
||||
state.display = p.display;
|
||||
state.viz_config = p.config;
|
||||
} else {
|
||||
state.extra_panels.push_back(p);
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
}
|
||||
lua_pop(L, 1); // views
|
||||
|
||||
lua_pop(L, 1); // pop root
|
||||
return finish_with_warns(true);
|
||||
}
|
||||
|
||||
} // namespace tql
|
||||
@@ -0,0 +1,42 @@
|
||||
// TQL — Table Query Language emit/apply. Round-trip entre State y Lua text.
|
||||
// Ver docs/TQL.md.
|
||||
#pragma once
|
||||
|
||||
#include "data_table_logic.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace tql {
|
||||
|
||||
// Serializa el estado actual a un Lua chunk completo:
|
||||
// return { version, display, stages, columns, visualization_settings }
|
||||
//
|
||||
// `headers` y `types` describen las cols originales (size = orig_cols).
|
||||
// Las derived cols se anaden automaticamente desde state.derived.
|
||||
std::string emit(const data_table::State& state,
|
||||
const std::vector<std::string>& headers,
|
||||
const std::vector<data_table::ColumnType>& types);
|
||||
|
||||
// Parsea un Lua chunk TQL y rellena State. Mutates:
|
||||
// - stages (clears + reconstruye desde stages[] del TQL; stage 0 = Raw con
|
||||
// filters/expressions/sort; stages 1+ con filter/breakout/aggregation/sort)
|
||||
// - col_visible / col_order (desde columns[])
|
||||
// - color_rules (desde columns[].color_rules)
|
||||
// - stages[0].derived[].type (desde columns[].type para nombres derived)
|
||||
//
|
||||
// `cells/rows/orig_cols` necesarios para sample auto-detect de tipos en
|
||||
// expressions (cuando la entry columns omite el type explicito).
|
||||
bool apply(const std::string& lua_text,
|
||||
data_table::State& state,
|
||||
const std::vector<std::string>& headers,
|
||||
const std::vector<data_table::ColumnType>& types,
|
||||
const char* const* cells, int rows, int orig_cols,
|
||||
std::string* err);
|
||||
|
||||
// Helpers expuestos para tests.
|
||||
std::string lua_string_literal(const std::string& s);
|
||||
std::string color_to_hex(unsigned int c);
|
||||
unsigned int hex_to_color(const std::string& s);
|
||||
data_table::ColumnType column_type_from_string(const std::string& s);
|
||||
|
||||
} // namespace tql
|
||||
@@ -0,0 +1,800 @@
|
||||
#include "viz.h"
|
||||
#include "implot.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace viz {
|
||||
|
||||
using data_table::StageOutput;
|
||||
using data_table::ColumnType;
|
||||
using data_table::ViewMode;
|
||||
using data_table::ViewConfig;
|
||||
using data_table::parse_number;
|
||||
|
||||
static int find_header(const StageOutput& out, const std::string& name) {
|
||||
if (name.empty()) return -1;
|
||||
for (size_t c = 0; c < out.headers.size(); ++c)
|
||||
if (out.headers[c] == name) return (int)c;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int resolve_x(const StageOutput& out, const ViewConfig& cfg, int fallback) {
|
||||
int c = find_header(out, cfg.x_col);
|
||||
return (c >= 0) ? c : fallback;
|
||||
}
|
||||
static int resolve_cat(const StageOutput& out, const ViewConfig& cfg, int fallback) {
|
||||
int c = find_header(out, cfg.cat_col);
|
||||
return (c >= 0) ? c : fallback;
|
||||
}
|
||||
static int resolve_size(const StageOutput& out, const ViewConfig& cfg, int fallback) {
|
||||
int c = find_header(out, cfg.size_col);
|
||||
return (c >= 0) ? c : fallback;
|
||||
}
|
||||
|
||||
int first_numeric_col(const StageOutput& out) {
|
||||
for (size_t c = 0; c < out.types.size(); ++c) {
|
||||
if (out.types[c] == ColumnType::Int || out.types[c] == ColumnType::Float) return (int)c;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int first_category_col(const StageOutput& out) {
|
||||
for (size_t c = 0; c < out.types.size(); ++c) {
|
||||
ColumnType t = out.types[c];
|
||||
if (t == ColumnType::String || t == ColumnType::Date || t == ColumnType::Bool ||
|
||||
t == ColumnType::Json) return (int)c;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::vector<double> extract_numeric(const StageOutput& out, int col) {
|
||||
std::vector<double> v;
|
||||
if (col < 0 || col >= out.cols) return v;
|
||||
v.reserve(out.rows);
|
||||
for (int r = 0; r < out.rows; ++r) {
|
||||
const char* s = out.cells[(size_t)r * out.cols + col];
|
||||
double d = 0;
|
||||
if (s && *s && parse_number(s, d)) v.push_back(d);
|
||||
else v.push_back(std::nan(""));
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
std::vector<std::string> extract_category(const StageOutput& out, int col) {
|
||||
std::vector<std::string> v;
|
||||
if (col < 0 || col >= out.cols) return v;
|
||||
v.reserve(out.rows);
|
||||
for (int r = 0; r < out.rows; ++r) {
|
||||
const char* s = out.cells[(size_t)r * out.cols + col];
|
||||
v.emplace_back(s ? s : "");
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
struct NumCol { int idx; std::string name; std::vector<double> vals; };
|
||||
|
||||
std::vector<NumCol> collect_numeric(const StageOutput& out, int max_n = 16) {
|
||||
std::vector<NumCol> r;
|
||||
for (size_t c = 0; c < out.types.size() && (int)r.size() < max_n; ++c) {
|
||||
if (out.types[c] == ColumnType::Int || out.types[c] == ColumnType::Float) {
|
||||
NumCol nc;
|
||||
nc.idx = (int)c;
|
||||
nc.name = out.headers[c];
|
||||
nc.vals = extract_numeric(out, (int)c);
|
||||
r.push_back(std::move(nc));
|
||||
}
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
std::vector<NumCol> collect_numeric_filtered(const StageOutput& out,
|
||||
const ViewConfig& cfg,
|
||||
int max_n = 16) {
|
||||
if (cfg.y_cols.empty()) return collect_numeric(out, max_n);
|
||||
std::vector<NumCol> r;
|
||||
for (const auto& name : cfg.y_cols) {
|
||||
if ((int)r.size() >= max_n) break;
|
||||
int c = find_header(out, name);
|
||||
if (c < 0) continue;
|
||||
if (out.types[c] != ColumnType::Int && out.types[c] != ColumnType::Float) continue;
|
||||
NumCol nc;
|
||||
nc.idx = c;
|
||||
nc.name = out.headers[c];
|
||||
nc.vals = extract_numeric(out, c);
|
||||
r.push_back(std::move(nc));
|
||||
}
|
||||
if (r.empty()) return collect_numeric(out, max_n);
|
||||
return r;
|
||||
}
|
||||
|
||||
ImPlotSpec spec_with_color(unsigned int rgba_color) {
|
||||
if (rgba_color == 0) return ImPlotSpec();
|
||||
ImU32 c = (ImU32)rgba_color;
|
||||
return ImPlotSpec(ImPlotProp_LineColor, c, ImPlotProp_FillColor, c);
|
||||
}
|
||||
|
||||
// Axis flags: locked = no pan/zoom; unlocked = 0 (sin AutoFit, para preservar
|
||||
// pan/zoom del user). Re-fit explicito via SetNextAxesToFit cuando fit_request.
|
||||
ImPlotAxisFlags axflag(const ViewConfig& cfg, ImPlotAxisFlags base = 0) {
|
||||
if (cfg.locked) return base | ImPlotAxisFlags_Lock;
|
||||
return base;
|
||||
}
|
||||
|
||||
// Llamar antes de BeginPlot. Si cfg.fit_request -> fuerza re-fit y limpia el flag.
|
||||
void maybe_fit(const ViewConfig& cfg) {
|
||||
if (cfg.fit_request) {
|
||||
ImPlot::SetNextAxesToFit();
|
||||
cfg.fit_request = false;
|
||||
}
|
||||
}
|
||||
|
||||
void info_text(const char* msg) {
|
||||
ImVec2 avail = ImGui::GetContentRegionAvail();
|
||||
ImVec2 sz = ImGui::CalcTextSize(msg);
|
||||
ImGui::SetCursorPos(ImVec2(ImGui::GetCursorPosX() + (avail.x - sz.x) * 0.5f,
|
||||
ImGui::GetCursorPosY() + (avail.y - sz.y) * 0.5f));
|
||||
ImGui::TextDisabled("%s", msg);
|
||||
}
|
||||
|
||||
// Drop NaN and pair with optional labels.
|
||||
std::vector<double> finite(const std::vector<double>& v) {
|
||||
std::vector<double> r; r.reserve(v.size());
|
||||
for (double d : v) if (!std::isnan(d)) r.push_back(d);
|
||||
return r;
|
||||
}
|
||||
|
||||
bool render_bar_like(const StageOutput& out, ViewMode mode,
|
||||
const ViewConfig& cfg, ImVec2 size) {
|
||||
int cat_col = resolve_cat(out, cfg, first_category_col(out));
|
||||
auto nums = collect_numeric_filtered(out, cfg, 8);
|
||||
if (cat_col < 0 || nums.empty()) {
|
||||
info_text("Need 1 category + 1+ numeric columns");
|
||||
return false;
|
||||
}
|
||||
auto cats = extract_category(out, cat_col);
|
||||
int n = (int)cats.size();
|
||||
if (n == 0) { info_text("Empty data"); return false; }
|
||||
|
||||
// Ticks
|
||||
std::vector<double> ticks(n);
|
||||
std::vector<const char*> labels(n);
|
||||
for (int i = 0; i < n; ++i) { ticks[i] = i; labels[i] = cats[i].c_str(); }
|
||||
|
||||
bool horiz = (mode == ViewMode::Bar);
|
||||
ImPlotFlags pflags = cfg.show_legend ? 0 : ImPlotFlags_NoLegend;
|
||||
maybe_fit(cfg);
|
||||
if (!ImPlot::BeginPlot("##bar", size, pflags)) return false;
|
||||
|
||||
ImPlotAxisFlags ax_cat = axflag(cfg);
|
||||
ImPlotAxisFlags ax_num = axflag(cfg);
|
||||
|
||||
if (horiz) {
|
||||
ImPlot::SetupAxes(out.headers[nums[0].idx].c_str(), out.headers[cat_col].c_str(),
|
||||
ax_num, ax_cat);
|
||||
ImPlot::SetupAxisTicks(ImAxis_Y1, ticks.data(), n, labels.data(), false);
|
||||
} else {
|
||||
ImPlot::SetupAxes(out.headers[cat_col].c_str(), out.headers[nums[0].idx].c_str(),
|
||||
ax_cat, ax_num);
|
||||
ImPlot::SetupAxisTicks(ImAxis_X1, ticks.data(), n, labels.data(), false);
|
||||
}
|
||||
|
||||
if (mode == ViewMode::StackedBar || mode == ViewMode::GroupedBar) {
|
||||
// Build flat matrix items x groups
|
||||
int items = (int)nums.size();
|
||||
std::vector<double> mat((size_t)items * n, 0.0);
|
||||
std::vector<const char*> series_labels(items);
|
||||
for (int it = 0; it < items; ++it) {
|
||||
series_labels[it] = nums[it].name.c_str();
|
||||
for (int g = 0; g < n; ++g) {
|
||||
double d = nums[it].vals[g];
|
||||
mat[(size_t)it * n + g] = std::isnan(d) ? 0.0 : d;
|
||||
}
|
||||
}
|
||||
int flags = (mode == ViewMode::StackedBar) ? ImPlotBarGroupsFlags_Stacked : 0;
|
||||
if (horiz) flags |= ImPlotBarGroupsFlags_Horizontal;
|
||||
ImPlot::PlotBarGroups(series_labels.data(), mat.data(), items, n, 0.67, 0,
|
||||
ImPlotSpec(ImPlotProp_Flags, flags));
|
||||
} else {
|
||||
// Single series (first numeric col).
|
||||
std::vector<double> ys(n);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
double d = nums[0].vals[i];
|
||||
ys[i] = std::isnan(d) ? 0.0 : d;
|
||||
}
|
||||
ImPlotSpec spc = spec_with_color(cfg.primary_color);
|
||||
if (horiz) {
|
||||
if (cfg.primary_color != 0) {
|
||||
ImU32 col = (ImU32)cfg.primary_color;
|
||||
ImPlot::PlotBars(nums[0].name.c_str(), ys.data(), ticks.data(), n, 0.67,
|
||||
ImPlotSpec(ImPlotProp_Flags, ImPlotBarsFlags_Horizontal,
|
||||
ImPlotProp_FillColor, col,
|
||||
ImPlotProp_LineColor, col));
|
||||
} else {
|
||||
ImPlot::PlotBars(nums[0].name.c_str(), ys.data(), ticks.data(), n, 0.67,
|
||||
ImPlotSpec(ImPlotProp_Flags, ImPlotBarsFlags_Horizontal));
|
||||
}
|
||||
} else {
|
||||
ImPlot::PlotBars(nums[0].name.c_str(), ticks.data(), ys.data(), n, 0.67, spc);
|
||||
}
|
||||
}
|
||||
ImPlot::EndPlot();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool render_line_like(const StageOutput& out, ViewMode mode,
|
||||
const ViewConfig& cfg, ImVec2 size) {
|
||||
auto nums = collect_numeric_filtered(out, cfg, 8);
|
||||
if (nums.empty()) { info_text("Need at least 1 numeric column"); return false; }
|
||||
|
||||
ImPlotFlags pflags = cfg.show_legend ? 0 : ImPlotFlags_NoLegend;
|
||||
maybe_fit(cfg);
|
||||
if (!ImPlot::BeginPlot("##line", size, pflags)) return false;
|
||||
ImPlot::SetupAxes(nullptr, nullptr, axflag(cfg), axflag(cfg));
|
||||
|
||||
int n = nums.empty() ? 0 : (int)nums[0].vals.size();
|
||||
if (n == 0) { ImPlot::EndPlot(); return false; }
|
||||
|
||||
// X column: cfg.x_col override; sino primer numeric si hay >=2; sino indices.
|
||||
int x_idx = -1;
|
||||
if (!cfg.x_col.empty()) {
|
||||
int xc = find_header(out, cfg.x_col);
|
||||
if (xc >= 0 && (out.types[xc] == ColumnType::Int || out.types[xc] == ColumnType::Float)) {
|
||||
x_idx = xc;
|
||||
}
|
||||
}
|
||||
std::vector<double> idx_xs;
|
||||
const double* xs = nullptr;
|
||||
int start_y = 0;
|
||||
std::vector<double> x_data_external;
|
||||
if (x_idx >= 0) {
|
||||
x_data_external = extract_numeric(out, x_idx);
|
||||
xs = x_data_external.data();
|
||||
} else if (nums.size() >= 2 && cfg.y_cols.empty()) {
|
||||
xs = nums[0].vals.data();
|
||||
start_y = 1;
|
||||
} else {
|
||||
idx_xs.resize(n);
|
||||
for (int i = 0; i < n; ++i) idx_xs[i] = i;
|
||||
xs = idx_xs.data();
|
||||
}
|
||||
|
||||
bool only_one = (cfg.primary_color != 0) && (nums.size() - start_y == 1);
|
||||
for (size_t i = (size_t)start_y; i < nums.size(); ++i) {
|
||||
const auto& nc = nums[i];
|
||||
ImU32 col = only_one ? (ImU32)cfg.primary_color : 0;
|
||||
int marker = cfg.show_markers ? ImPlotMarker_Circle : ImPlotMarker_None;
|
||||
if (mode == ViewMode::Area) {
|
||||
if (col) {
|
||||
ImPlot::PlotShaded(nc.name.c_str(), xs, nc.vals.data(), (int)nc.vals.size(), 0.0,
|
||||
ImPlotSpec(ImPlotProp_FillColor, col, ImPlotProp_LineColor, col));
|
||||
} else {
|
||||
ImPlot::PlotShaded(nc.name.c_str(), xs, nc.vals.data(), (int)nc.vals.size(), 0.0);
|
||||
}
|
||||
} else if (mode == ViewMode::Stairs) {
|
||||
if (col) {
|
||||
ImPlot::PlotStairs(nc.name.c_str(), xs, nc.vals.data(), (int)nc.vals.size(),
|
||||
ImPlotSpec(ImPlotProp_LineColor, col));
|
||||
} else {
|
||||
ImPlot::PlotStairs(nc.name.c_str(), xs, nc.vals.data(), (int)nc.vals.size());
|
||||
}
|
||||
} else {
|
||||
if (col) {
|
||||
ImPlot::PlotLine(nc.name.c_str(), xs, nc.vals.data(), (int)nc.vals.size(),
|
||||
ImPlotSpec(ImPlotProp_Flags, ImPlotLineFlags_SkipNaN,
|
||||
ImPlotProp_LineColor, col,
|
||||
ImPlotProp_Marker, marker));
|
||||
} else {
|
||||
ImPlot::PlotLine(nc.name.c_str(), xs, nc.vals.data(), (int)nc.vals.size(),
|
||||
ImPlotSpec(ImPlotProp_Flags, ImPlotLineFlags_SkipNaN,
|
||||
ImPlotProp_Marker, marker));
|
||||
}
|
||||
}
|
||||
}
|
||||
ImPlot::EndPlot();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool render_scatter(const StageOutput& out, const ViewConfig& cfg, ImVec2 size) {
|
||||
// Soporte cfg.x_col + cfg.y_cols[0]
|
||||
int xc = find_header(out, cfg.x_col);
|
||||
int yc = !cfg.y_cols.empty() ? find_header(out, cfg.y_cols[0]) : -1;
|
||||
std::vector<NumCol> nums;
|
||||
if (xc >= 0 && yc >= 0) {
|
||||
NumCol a{xc, out.headers[xc], extract_numeric(out, xc)};
|
||||
NumCol b{yc, out.headers[yc], extract_numeric(out, yc)};
|
||||
nums = {a, b};
|
||||
} else {
|
||||
nums = collect_numeric(out, 4);
|
||||
}
|
||||
if (nums.size() < 2) { info_text("Need 2 numeric columns"); return false; }
|
||||
maybe_fit(cfg);
|
||||
if (!ImPlot::BeginPlot("##scatter", size, cfg.show_legend ? 0 : ImPlotFlags_NoLegend)) return false;
|
||||
ImPlot::SetupAxes(nums[0].name.c_str(), nums[1].name.c_str(),
|
||||
axflag(cfg), axflag(cfg));
|
||||
if (cfg.primary_color) {
|
||||
ImU32 col = (ImU32)cfg.primary_color;
|
||||
ImPlot::PlotScatter("##s", nums[0].vals.data(), nums[1].vals.data(),
|
||||
(int)nums[0].vals.size(),
|
||||
ImPlotSpec(ImPlotProp_MarkerFillColor, col,
|
||||
ImPlotProp_MarkerLineColor, col));
|
||||
} else {
|
||||
ImPlot::PlotScatter("##s", nums[0].vals.data(), nums[1].vals.data(),
|
||||
(int)nums[0].vals.size());
|
||||
}
|
||||
ImPlot::EndPlot();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool render_bubble(const StageOutput& out, const ViewConfig& cfg, ImVec2 size) {
|
||||
int xc = find_header(out, cfg.x_col);
|
||||
int yc = !cfg.y_cols.empty() ? find_header(out, cfg.y_cols[0]) : -1;
|
||||
int sc = resolve_size(out, cfg, -1);
|
||||
std::vector<NumCol> nums;
|
||||
if (xc >= 0 && yc >= 0 && sc >= 0) {
|
||||
nums = {
|
||||
{xc, out.headers[xc], extract_numeric(out, xc)},
|
||||
{yc, out.headers[yc], extract_numeric(out, yc)},
|
||||
{sc, out.headers[sc], extract_numeric(out, sc)},
|
||||
};
|
||||
} else {
|
||||
nums = collect_numeric(out, 4);
|
||||
}
|
||||
if (nums.size() < 3) { info_text("Need 3 numeric columns (x, y, size)"); return false; }
|
||||
maybe_fit(cfg);
|
||||
if (!ImPlot::BeginPlot("##bubble", size, cfg.show_legend ? 0 : ImPlotFlags_NoLegend)) return false;
|
||||
ImPlot::SetupAxes(nums[0].name.c_str(), nums[1].name.c_str(),
|
||||
axflag(cfg), axflag(cfg));
|
||||
ImPlot::PlotBubbles("##b", nums[0].vals.data(), nums[1].vals.data(),
|
||||
nums[2].vals.data(), (int)nums[0].vals.size());
|
||||
ImPlot::EndPlot();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool render_histogram(const StageOutput& out, const ViewConfig& cfg, ImVec2 size) {
|
||||
auto nums = collect_numeric_filtered(out, cfg, 4);
|
||||
if (nums.empty()) { info_text("Need 1 numeric column"); return false; }
|
||||
auto vals = finite(nums[0].vals);
|
||||
if (vals.empty()) { info_text("No finite values"); return false; }
|
||||
maybe_fit(cfg);
|
||||
if (!ImPlot::BeginPlot("##hist", size, cfg.show_legend ? 0 : ImPlotFlags_NoLegend)) return false;
|
||||
ImPlot::SetupAxes(nums[0].name.c_str(), "count",
|
||||
axflag(cfg), axflag(cfg));
|
||||
int bins = (cfg.hist_bins > 0) ? cfg.hist_bins : ImPlotBin_Sturges;
|
||||
if (cfg.primary_color) {
|
||||
ImU32 col = (ImU32)cfg.primary_color;
|
||||
ImPlot::PlotHistogram("##h", vals.data(), (int)vals.size(), bins, 1.0,
|
||||
ImPlotRange(),
|
||||
ImPlotSpec(ImPlotProp_FillColor, col,
|
||||
ImPlotProp_LineColor, col));
|
||||
} else {
|
||||
ImPlot::PlotHistogram("##h", vals.data(), (int)vals.size(), bins);
|
||||
}
|
||||
ImPlot::EndPlot();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool render_hist2d(const StageOutput& out, const ViewConfig& cfg, ImVec2 size) {
|
||||
int xc = find_header(out, cfg.x_col);
|
||||
int yc = !cfg.y_cols.empty() ? find_header(out, cfg.y_cols[0]) : -1;
|
||||
std::vector<NumCol> nums;
|
||||
if (xc >= 0 && yc >= 0) {
|
||||
nums = {
|
||||
{xc, out.headers[xc], extract_numeric(out, xc)},
|
||||
{yc, out.headers[yc], extract_numeric(out, yc)},
|
||||
};
|
||||
} else {
|
||||
nums = collect_numeric(out, 2);
|
||||
}
|
||||
if (nums.size() < 2) { info_text("Need 2 numeric columns"); return false; }
|
||||
maybe_fit(cfg);
|
||||
if (!ImPlot::BeginPlot("##hist2d", size, cfg.show_legend ? 0 : ImPlotFlags_NoLegend)) return false;
|
||||
ImPlot::SetupAxes(nums[0].name.c_str(), nums[1].name.c_str());
|
||||
int bins = (cfg.hist_bins > 0) ? cfg.hist_bins : ImPlotBin_Sturges;
|
||||
ImPlot::PlotHistogram2D("##h2", nums[0].vals.data(), nums[1].vals.data(),
|
||||
(int)nums[0].vals.size(), bins, bins);
|
||||
ImPlot::EndPlot();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool render_heatmap(const StageOutput& out, const ViewConfig& cfg, ImVec2 size) {
|
||||
auto nums = collect_numeric_filtered(out, cfg, 64);
|
||||
if (nums.empty()) { info_text("Need numeric columns"); return false; }
|
||||
int cols = (int)nums.size();
|
||||
int rows = (int)nums[0].vals.size();
|
||||
if (rows == 0) { info_text("No rows"); return false; }
|
||||
std::vector<double> mat((size_t)rows * cols, 0.0);
|
||||
double mn = +1e300, mx = -1e300;
|
||||
for (int c = 0; c < cols; ++c) {
|
||||
for (int r = 0; r < rows; ++r) {
|
||||
double d = nums[c].vals[r];
|
||||
if (std::isnan(d)) d = 0;
|
||||
mat[(size_t)r * cols + c] = d;
|
||||
if (d < mn) mn = d; if (d > mx) mx = d;
|
||||
}
|
||||
}
|
||||
if (mn == mx) { mx = mn + 1; }
|
||||
maybe_fit(cfg);
|
||||
if (!ImPlot::BeginPlot("##heatmap", size, 0)) return false;
|
||||
ImPlot::PlotHeatmap("##hm", mat.data(), rows, cols, mn, mx, nullptr);
|
||||
ImPlot::EndPlot();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool render_pie(const StageOutput& out, const ViewConfig& cfg, bool donut, ImVec2 size) {
|
||||
int cat = resolve_cat(out, cfg, first_category_col(out));
|
||||
auto nums = collect_numeric_filtered(out, cfg, 1);
|
||||
if (cat < 0 || nums.empty()) { info_text("Need 1 category + 1 numeric"); return false; }
|
||||
auto cats = extract_category(out, cat);
|
||||
int n = std::min((int)cats.size(), (int)nums[0].vals.size());
|
||||
if (n == 0) return false;
|
||||
std::vector<double> values(n);
|
||||
std::vector<const char*> labels(n);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
double d = nums[0].vals[i];
|
||||
values[i] = std::isnan(d) ? 0.0 : std::abs(d);
|
||||
labels[i] = cats[i].c_str();
|
||||
}
|
||||
ImPlotFlags pf = ImPlotFlags_Equal;
|
||||
if (!cfg.show_legend) pf |= ImPlotFlags_NoLegend;
|
||||
maybe_fit(cfg);
|
||||
if (!ImPlot::BeginPlot("##pie", size, pf)) return false;
|
||||
ImPlot::SetupAxes(nullptr, nullptr, ImPlotAxisFlags_NoDecorations,
|
||||
ImPlotAxisFlags_NoDecorations);
|
||||
ImPlot::SetupAxesLimits(0, 1, 0, 1, ImPlotCond_Always);
|
||||
double radius = (cfg.pie_radius > 0) ? cfg.pie_radius : (donut ? 0.4 : 0.45);
|
||||
ImPlot::PlotPieChart(labels.data(), values.data(), n, 0.5, 0.5, radius, "%.1f");
|
||||
if (donut) {
|
||||
// Draw inner hole as solid circle by overlaying a smaller pie of one slice transparent.
|
||||
// Simpler: just visually it's a circle with text. Use no extra primitive for now.
|
||||
}
|
||||
ImPlot::EndPlot();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool render_funnel(const StageOutput& out, const ViewConfig& cfg, ImVec2 size) {
|
||||
int cat = resolve_cat(out, cfg, first_category_col(out));
|
||||
auto nums = collect_numeric_filtered(out, cfg, 1);
|
||||
if (cat < 0 || nums.empty()) { info_text("Need 1 category + 1 numeric"); return false; }
|
||||
auto cats = extract_category(out, cat);
|
||||
int n = std::min((int)cats.size(), (int)nums[0].vals.size());
|
||||
if (n == 0) return false;
|
||||
// Sort desc by value
|
||||
std::vector<int> idx(n);
|
||||
for (int i = 0; i < n; ++i) idx[i] = i;
|
||||
std::sort(idx.begin(), idx.end(), [&](int a, int b) {
|
||||
double da = std::isnan(nums[0].vals[a]) ? -1e300 : nums[0].vals[a];
|
||||
double db = std::isnan(nums[0].vals[b]) ? -1e300 : nums[0].vals[b];
|
||||
return da > db;
|
||||
});
|
||||
std::vector<double> ys(n);
|
||||
std::vector<double> ticks(n);
|
||||
std::vector<const char*> labels(n);
|
||||
std::vector<std::string> labels_store(n);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
double d = nums[0].vals[idx[i]];
|
||||
ys[i] = std::isnan(d) ? 0 : d;
|
||||
ticks[i] = n - 1 - i; // descending order
|
||||
labels_store[i] = cats[idx[i]];
|
||||
labels[i] = labels_store[i].c_str();
|
||||
}
|
||||
maybe_fit(cfg);
|
||||
if (!ImPlot::BeginPlot("##funnel", size, 0)) return false;
|
||||
ImPlot::SetupAxes(nums[0].name.c_str(), out.headers[cat].c_str(),
|
||||
axflag(cfg), axflag(cfg));
|
||||
ImPlot::SetupAxisTicks(ImAxis_Y1, ticks.data(), n, labels.data(), false);
|
||||
ImPlot::PlotBars(nums[0].name.c_str(), ys.data(), ticks.data(), n, 0.85,
|
||||
ImPlotSpec(ImPlotProp_Flags, ImPlotBarsFlags_Horizontal));
|
||||
ImPlot::EndPlot();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool render_waterfall(const StageOutput& out, const ViewConfig& cfg, ImVec2 size) {
|
||||
auto nums = collect_numeric_filtered(out, cfg, 1);
|
||||
if (nums.empty()) { info_text("Need 1 numeric column"); return false; }
|
||||
int n = (int)nums[0].vals.size();
|
||||
if (n == 0) return false;
|
||||
int cat = resolve_cat(out, cfg, first_category_col(out));
|
||||
auto cats = (cat >= 0) ? extract_category(out, cat) : std::vector<std::string>();
|
||||
|
||||
std::vector<double> running(n + 1, 0);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
double d = std::isnan(nums[0].vals[i]) ? 0 : nums[0].vals[i];
|
||||
running[i + 1] = running[i] + d;
|
||||
}
|
||||
std::vector<double> ticks(n);
|
||||
for (int i = 0; i < n; ++i) ticks[i] = i;
|
||||
std::vector<const char*> labels(n);
|
||||
for (int i = 0; i < n; ++i) labels[i] = (i < (int)cats.size()) ? cats[i].c_str() : "";
|
||||
|
||||
maybe_fit(cfg);
|
||||
if (!ImPlot::BeginPlot("##waterfall", size, 0)) return false;
|
||||
ImPlot::SetupAxes(nullptr, nums[0].name.c_str(),
|
||||
axflag(cfg), axflag(cfg));
|
||||
if (cat >= 0) ImPlot::SetupAxisTicks(ImAxis_X1, ticks.data(), n, labels.data(), false);
|
||||
// Draw stems with rectangles via error-bars trick: low=cum_prev, high=cum_curr.
|
||||
std::vector<double> mid(n), err(n);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
mid[i] = (running[i] + running[i + 1]) * 0.5;
|
||||
err[i] = std::abs((running[i + 1] - running[i]) * 0.5);
|
||||
}
|
||||
ImPlot::PlotErrorBars("##wf", ticks.data(), mid.data(), err.data(), n);
|
||||
ImPlot::PlotLine("cum", running.data() + 1, n);
|
||||
ImPlot::EndPlot();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool render_kpi_single(const StageOutput& out, const ViewConfig& cfg) {
|
||||
int nc = !cfg.y_cols.empty() ? find_header(out, cfg.y_cols[0]) : -1;
|
||||
if (nc < 0) nc = first_numeric_col(out);
|
||||
if (nc < 0) { info_text("Need 1 numeric column"); return false; }
|
||||
auto vals = extract_numeric(out, nc);
|
||||
if (vals.empty()) { info_text("Empty"); return false; }
|
||||
double last = std::nan("");
|
||||
for (auto v : vals) if (!std::isnan(v)) last = v;
|
||||
if (std::isnan(last)) { info_text("No finite values"); return false; }
|
||||
|
||||
char buf[64];
|
||||
if (std::abs(last) >= 1e6) std::snprintf(buf, sizeof(buf), "%.2fM", last / 1e6);
|
||||
else if (std::abs(last) >= 1e3) std::snprintf(buf, sizeof(buf), "%.2fK", last / 1e3);
|
||||
else std::snprintf(buf, sizeof(buf), "%.3g", last);
|
||||
|
||||
ImVec2 avail = ImGui::GetContentRegionAvail();
|
||||
ImGui::SetWindowFontScale(4.0f);
|
||||
ImVec2 sz = ImGui::CalcTextSize(buf);
|
||||
ImGui::SetCursorPos(ImVec2(ImGui::GetCursorPosX() + (avail.x - sz.x) * 0.5f,
|
||||
ImGui::GetCursorPosY() + (avail.y - sz.y) * 0.5f - 20));
|
||||
ImGui::TextUnformatted(buf);
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
|
||||
sz = ImGui::CalcTextSize(out.headers[nc].c_str());
|
||||
ImGui::SetCursorPos(ImVec2(ImGui::GetCursorPosX() + (avail.x - sz.x) * 0.5f,
|
||||
ImGui::GetCursorPosY() + (avail.y - sz.y) * 0.5f - 10));
|
||||
ImGui::TextDisabled("%s", out.headers[nc].c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool render_kpi_grid(const StageOutput& out, const ViewConfig& cfg) {
|
||||
auto nums = collect_numeric_filtered(out, cfg, 12);
|
||||
if (nums.empty()) { info_text("Need numeric columns"); return false; }
|
||||
ImVec2 avail = ImGui::GetContentRegionAvail();
|
||||
int per_row = std::max(1, (int)(avail.x / 220));
|
||||
int idx = 0;
|
||||
for (auto& nc : nums) {
|
||||
double last = std::nan("");
|
||||
for (auto v : nc.vals) if (!std::isnan(v)) last = v;
|
||||
if (std::isnan(last)) last = 0;
|
||||
char buf[64];
|
||||
if (std::abs(last) >= 1e6) std::snprintf(buf, sizeof(buf), "%.2fM", last / 1e6);
|
||||
else if (std::abs(last) >= 1e3) std::snprintf(buf, sizeof(buf), "%.2fK", last / 1e3);
|
||||
else std::snprintf(buf, sizeof(buf), "%.4g", last);
|
||||
|
||||
ImGui::BeginChild((ImGuiID)(0x1000 + idx), ImVec2(210, 100), true);
|
||||
ImGui::TextDisabled("%s", nc.name.c_str());
|
||||
ImGui::SetWindowFontScale(2.4f);
|
||||
ImGui::TextUnformatted(buf);
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::EndChild();
|
||||
|
||||
if ((idx % per_row) != (per_row - 1)) ImGui::SameLine();
|
||||
idx++;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool render_stem(const StageOutput& out, const ViewConfig& cfg, ImVec2 size) {
|
||||
auto nums = collect_numeric_filtered(out, cfg, 1);
|
||||
if (nums.empty()) { info_text("Need 1 numeric column"); return false; }
|
||||
int n = (int)nums[0].vals.size();
|
||||
std::vector<double> xs(n); for (int i = 0; i < n; ++i) xs[i] = i;
|
||||
maybe_fit(cfg);
|
||||
if (!ImPlot::BeginPlot("##stem", size, 0)) return false;
|
||||
ImPlot::SetupAxes(nullptr, nums[0].name.c_str(),
|
||||
axflag(cfg), axflag(cfg));
|
||||
ImPlot::PlotStems(nums[0].name.c_str(), xs.data(), nums[0].vals.data(), n);
|
||||
ImPlot::EndPlot();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool render_errorbars(const StageOutput& out, const ViewConfig& cfg, ImVec2 size) {
|
||||
auto nums = collect_numeric_filtered(out, cfg, 4);
|
||||
if (nums.size() < 2) { info_text("Need 2 numeric columns (value, err)"); return false; }
|
||||
int n = (int)nums[0].vals.size();
|
||||
std::vector<double> xs(n); for (int i = 0; i < n; ++i) xs[i] = i;
|
||||
maybe_fit(cfg);
|
||||
if (!ImPlot::BeginPlot("##eb", size, 0)) return false;
|
||||
ImPlot::SetupAxes(nullptr, nums[0].name.c_str(),
|
||||
axflag(cfg), axflag(cfg));
|
||||
ImPlot::PlotErrorBars(nums[0].name.c_str(), xs.data(),
|
||||
nums[0].vals.data(), nums[1].vals.data(), n);
|
||||
ImPlot::PlotScatter("##s", xs.data(), nums[0].vals.data(), n);
|
||||
ImPlot::EndPlot();
|
||||
return true;
|
||||
}
|
||||
|
||||
// BoxPlot: agrupar por categoria, calcular min/p25/p50/p75/max y dibujar
|
||||
// rectangulos manuales via PlotShaded + lineas.
|
||||
bool render_boxplot(const StageOutput& out, const ViewConfig& cfg, ImVec2 size) {
|
||||
int cat = resolve_cat(out, cfg, first_category_col(out));
|
||||
auto nums = collect_numeric_filtered(out, cfg, 1);
|
||||
if (cat < 0 || nums.empty()) { info_text("Need 1 category + 1 numeric"); return false; }
|
||||
auto cats = extract_category(out, cat);
|
||||
int n = std::min((int)cats.size(), (int)nums[0].vals.size());
|
||||
if (n == 0) return false;
|
||||
|
||||
// Group values by category
|
||||
std::unordered_map<std::string, std::vector<double>> groups;
|
||||
std::vector<std::string> order;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (groups.find(cats[i]) == groups.end()) order.push_back(cats[i]);
|
||||
double d = nums[0].vals[i];
|
||||
if (!std::isnan(d)) groups[cats[i]].push_back(d);
|
||||
}
|
||||
int G = (int)order.size();
|
||||
if (G == 0) return false;
|
||||
|
||||
std::vector<double> mn(G), p25(G), p50(G), p75(G), mx(G), xs(G);
|
||||
std::vector<const char*> labels(G);
|
||||
for (int g = 0; g < G; ++g) {
|
||||
auto& v = groups[order[g]];
|
||||
std::sort(v.begin(), v.end());
|
||||
int N = (int)v.size();
|
||||
xs[g] = g;
|
||||
labels[g]= order[g].c_str();
|
||||
if (N == 0) { mn[g]=p25[g]=p50[g]=p75[g]=mx[g]=0; continue; }
|
||||
mn[g] = v.front();
|
||||
mx[g] = v.back();
|
||||
p25[g] = v[std::min(N - 1, (int)(N * 0.25))];
|
||||
p50[g] = v[std::min(N - 1, (int)(N * 0.50))];
|
||||
p75[g] = v[std::min(N - 1, (int)(N * 0.75))];
|
||||
}
|
||||
|
||||
maybe_fit(cfg);
|
||||
if (!ImPlot::BeginPlot("##box", size, 0)) return false;
|
||||
ImPlot::SetupAxes(out.headers[cat].c_str(), nums[0].name.c_str(),
|
||||
axflag(cfg), axflag(cfg));
|
||||
ImPlot::SetupAxisTicks(ImAxis_X1, xs.data(), G, labels.data(), false);
|
||||
|
||||
// Whiskers: stems from min to max
|
||||
for (int g = 0; g < G; ++g) {
|
||||
double lo[2] = { mn[g], mx[g] };
|
||||
double xx[2] = { xs[g], xs[g] };
|
||||
ImPlot::PlotLine("##wh", xx, lo, 2);
|
||||
}
|
||||
// Box: p25..p75 as bars centered on p50
|
||||
std::vector<double> mid(G), half(G);
|
||||
for (int g = 0; g < G; ++g) {
|
||||
mid[g] = (p25[g] + p75[g]) * 0.5;
|
||||
half[g] = (p75[g] - p25[g]) * 0.5;
|
||||
}
|
||||
ImPlot::PlotErrorBars("box", xs.data(), mid.data(), half.data(), G);
|
||||
ImPlot::PlotScatter("median", xs.data(), p50.data(), G);
|
||||
ImPlot::EndPlot();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Candlestick: tiempo + O/H/L/C. Asume 4 primeras cols numericas en ese orden.
|
||||
bool render_candlestick(const StageOutput& out, const ViewConfig& cfg, ImVec2 size) {
|
||||
auto nums = collect_numeric_filtered(out, cfg, 8);
|
||||
if (nums.size() < 4) { info_text("Need 4 numeric columns: O/H/L/C"); return false; }
|
||||
int n = (int)nums[0].vals.size();
|
||||
maybe_fit(cfg);
|
||||
if (!ImPlot::BeginPlot("##candle", size, 0)) return false;
|
||||
ImPlot::SetupAxes("t", "price", axflag(cfg), axflag(cfg));
|
||||
std::vector<double> xs(n); for (int i = 0; i < n; ++i) xs[i] = i;
|
||||
const auto& O = nums[0].vals;
|
||||
const auto& H = nums[1].vals;
|
||||
const auto& L = nums[2].vals;
|
||||
const auto& C = nums[3].vals;
|
||||
// Wicks
|
||||
for (int i = 0; i < n; ++i) {
|
||||
double xx[2] = { xs[i], xs[i] };
|
||||
double yy[2] = { L[i], H[i] };
|
||||
ImPlot::PlotLine("##wick", xx, yy, 2);
|
||||
}
|
||||
// Body via PlotBars(mid, |C-O|)? Simpler: separate lines.
|
||||
std::vector<double> body_low(n), body_high(n), body_mid(n), body_err(n);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
body_low[i] = std::min(O[i], C[i]);
|
||||
body_high[i] = std::max(O[i], C[i]);
|
||||
body_mid[i] = (body_low[i] + body_high[i]) * 0.5;
|
||||
body_err[i] = (body_high[i] - body_low[i]) * 0.5;
|
||||
}
|
||||
ImPlot::PlotErrorBars("OHLC", xs.data(), body_mid.data(), body_err.data(), n);
|
||||
ImPlot::EndPlot();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool render_radar(const StageOutput& out, const ViewConfig& cfg, ImVec2 size) {
|
||||
auto nums = collect_numeric_filtered(out, cfg, 12);
|
||||
if (nums.size() < 3) { info_text("Need 3+ numeric columns"); return false; }
|
||||
int K = (int)nums.size();
|
||||
int n = (int)nums[0].vals.size();
|
||||
if (n == 0) return false;
|
||||
// Take first row as the polygon.
|
||||
std::vector<double> xs(K + 1), ys(K + 1);
|
||||
double radius_norm = 0;
|
||||
for (int k = 0; k < K; ++k) {
|
||||
double d = nums[k].vals[0];
|
||||
if (std::isnan(d)) d = 0;
|
||||
radius_norm = std::max(radius_norm, std::abs(d));
|
||||
}
|
||||
if (radius_norm == 0) radius_norm = 1;
|
||||
for (int k = 0; k < K; ++k) {
|
||||
double v = nums[k].vals[0]; if (std::isnan(v)) v = 0;
|
||||
double angle = 2 * 3.14159265358979 * k / K - 3.14159265358979 / 2;
|
||||
double r = v / radius_norm;
|
||||
xs[k] = std::cos(angle) * r;
|
||||
ys[k] = std::sin(angle) * r;
|
||||
}
|
||||
xs[K] = xs[0]; ys[K] = ys[0];
|
||||
maybe_fit(cfg);
|
||||
if (!ImPlot::BeginPlot("##radar", size,
|
||||
ImPlotFlags_Equal | ImPlotFlags_NoLegend)) return false;
|
||||
ImPlot::SetupAxesLimits(-1.2, 1.2, -1.2, 1.2, ImPlotCond_Always);
|
||||
ImPlot::SetupAxes(nullptr, nullptr, ImPlotAxisFlags_NoDecorations,
|
||||
ImPlotAxisFlags_NoDecorations);
|
||||
// Grid rings
|
||||
for (double rr : {0.25, 0.5, 0.75, 1.0}) {
|
||||
double gx[64], gy[64];
|
||||
for (int i = 0; i < 64; ++i) {
|
||||
double a = 2 * 3.14159265358979 * i / 63;
|
||||
gx[i] = std::cos(a) * rr; gy[i] = std::sin(a) * rr;
|
||||
}
|
||||
ImPlot::PlotLine("##grid", gx, gy, 64);
|
||||
}
|
||||
ImPlot::PlotLine("radar", xs.data(), ys.data(), K + 1);
|
||||
// Axis labels
|
||||
for (int k = 0; k < K; ++k) {
|
||||
double a = 2 * 3.14159265358979 * k / K - 3.14159265358979 / 2;
|
||||
ImPlot::PlotText(nums[k].name.c_str(), std::cos(a) * 1.1, std::sin(a) * 1.1);
|
||||
}
|
||||
ImPlot::EndPlot();
|
||||
return true;
|
||||
}
|
||||
|
||||
} // anon
|
||||
|
||||
bool render(const StageOutput& out, ViewMode mode,
|
||||
const ViewConfig& cfg, ImVec2 size) {
|
||||
if (out.rows == 0 || out.cols == 0) {
|
||||
info_text("No data");
|
||||
return false;
|
||||
}
|
||||
switch (mode) {
|
||||
case ViewMode::Table: return false;
|
||||
case ViewMode::Bar:
|
||||
case ViewMode::Column:
|
||||
case ViewMode::GroupedBar:
|
||||
case ViewMode::StackedBar: return render_bar_like(out, mode, cfg, size);
|
||||
case ViewMode::Line:
|
||||
case ViewMode::Area:
|
||||
case ViewMode::Stairs: return render_line_like(out, mode, cfg, size);
|
||||
case ViewMode::Scatter: return render_scatter(out, cfg, size);
|
||||
case ViewMode::Bubble: return render_bubble(out, cfg, size);
|
||||
case ViewMode::Histogram: return render_histogram(out, cfg, size);
|
||||
case ViewMode::Histogram2D: return render_hist2d(out, cfg, size);
|
||||
case ViewMode::Heatmap: return render_heatmap(out, cfg, size);
|
||||
case ViewMode::BoxPlot: return render_boxplot(out, cfg, size);
|
||||
case ViewMode::Stem: return render_stem(out, cfg, size);
|
||||
case ViewMode::ErrorBars: return render_errorbars(out, cfg, size);
|
||||
case ViewMode::Pie: return render_pie(out, cfg, false, size);
|
||||
case ViewMode::Donut: return render_pie(out, cfg, true, size);
|
||||
case ViewMode::Funnel: return render_funnel(out, cfg, size);
|
||||
case ViewMode::Waterfall: return render_waterfall(out, cfg, size);
|
||||
case ViewMode::KPI: return render_kpi_single(out, cfg);
|
||||
case ViewMode::KPIGrid: return render_kpi_grid(out, cfg);
|
||||
case ViewMode::Candlestick: return render_candlestick(out, cfg, size);
|
||||
case ViewMode::Radar: return render_radar(out, cfg, size);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace viz
|
||||
@@ -0,0 +1,35 @@
|
||||
// viz: dispatcher de visualizaciones ImPlot sobre StageOutput.
|
||||
// Cada modo elige automaticamente las columnas relevantes (primera categorica,
|
||||
// primera o varias numericas) salvo override desde UI.
|
||||
#pragma once
|
||||
|
||||
#include "data_table_logic.h"
|
||||
#include "imgui.h"
|
||||
#include <vector>
|
||||
|
||||
namespace viz {
|
||||
|
||||
// Render principal. Devuelve true si renderiza el modo solicitado, false si
|
||||
// no se cumplen pre-condiciones (faltan cols numericas/categoricas etc.).
|
||||
//
|
||||
// `size`: ImVec2(-1,-1) usa todo el espacio disponible.
|
||||
// `out`: output del stage activo (headers, types, cells flat row-major).
|
||||
bool render(const data_table::StageOutput& out,
|
||||
data_table::ViewMode mode,
|
||||
const data_table::ViewConfig& cfg,
|
||||
ImVec2 size = ImVec2(-1, -1));
|
||||
|
||||
// Helper expuesto: encuentra primera col numerica. -1 si ninguna.
|
||||
int first_numeric_col(const data_table::StageOutput& out);
|
||||
|
||||
// Helper: primera col categorica (String/Date/Bool/Json o Int con muchos
|
||||
// uniques bajos — heuristica). -1 si ninguna.
|
||||
int first_category_col(const data_table::StageOutput& out);
|
||||
|
||||
// Helper: extrae columna como vector<double>. Cells no parseables -> NaN.
|
||||
std::vector<double> extract_numeric(const data_table::StageOutput& out, int col);
|
||||
|
||||
// Helper: extrae columna como vector<string> (categorias).
|
||||
std::vector<std::string> extract_category(const data_table::StageOutput& out, int col);
|
||||
|
||||
} // namespace viz
|
||||
Reference in New Issue
Block a user