ai generated braille-based chart
This commit is contained in:
parent
18827be200
commit
3b66e38152
1 changed files with 159 additions and 49 deletions
208
src/tui/main.zig
208
src/tui/main.zig
|
|
@ -68,6 +68,11 @@ const StyledLine = struct {
|
||||||
alt_style: ?vaxis.Style = null,
|
alt_style: ?vaxis.Style = null,
|
||||||
alt_start: usize = 0,
|
alt_start: usize = 0,
|
||||||
alt_end: usize = 0,
|
alt_end: usize = 0,
|
||||||
|
// Optional pre-encoded grapheme array for multi-byte Unicode (e.g. braille charts).
|
||||||
|
// When set, each element is a grapheme string for one column position.
|
||||||
|
graphemes: ?[]const []const u8 = null,
|
||||||
|
// Optional per-cell style array (same length as graphemes). Enables color gradients.
|
||||||
|
cell_styles: ?[]const vaxis.Style = null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const OptionsRowKind = enum { expiration, calls_header, puts_header, call, put };
|
const OptionsRowKind = enum { expiration, calls_header, puts_header, call, put };
|
||||||
|
|
@ -1168,13 +1173,22 @@ const App = struct {
|
||||||
for (0..width) |ci| {
|
for (0..width) |ci| {
|
||||||
buf[row * width + ci] = .{ .char = .{ .grapheme = " " }, .style = line.style };
|
buf[row * width + ci] = .{ .char = .{ .grapheme = " " }, .style = line.style };
|
||||||
}
|
}
|
||||||
for (0..@min(line.text.len, width)) |ci| {
|
// Grapheme-based rendering (for braille / multi-byte Unicode lines)
|
||||||
var s = line.style;
|
if (line.graphemes) |graphemes| {
|
||||||
// Apply alt_style for the gain/loss column range
|
const cell_styles = line.cell_styles;
|
||||||
if (line.alt_style) |alt| {
|
for (0..@min(graphemes.len, width)) |ci| {
|
||||||
if (ci >= line.alt_start and ci < line.alt_end) s = alt;
|
const s = if (cell_styles) |cs| cs[ci] else line.style;
|
||||||
|
buf[row * width + ci] = .{ .char = .{ .grapheme = graphemes[ci] }, .style = s };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (0..@min(line.text.len, width)) |ci| {
|
||||||
|
var s = line.style;
|
||||||
|
// Apply alt_style for the gain/loss column range
|
||||||
|
if (line.alt_style) |alt| {
|
||||||
|
if (ci >= line.alt_start and ci < line.alt_end) s = alt;
|
||||||
|
}
|
||||||
|
buf[row * width + ci] = .{ .char = .{ .grapheme = glyph(line.text[ci]) }, .style = s };
|
||||||
}
|
}
|
||||||
buf[row * width + ci] = .{ .char = .{ .grapheme = glyph(line.text[ci]) }, .style = s };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1512,7 +1526,7 @@ const App = struct {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Braille chart of recent 60 trading days
|
// Braille sparkline chart of recent 60 trading days
|
||||||
try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
|
try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
|
||||||
const chart_days: usize = @min(c.len, 60);
|
const chart_days: usize = @min(c.len, 60);
|
||||||
const chart_data = c[c.len - chart_days ..];
|
const chart_data = c[c.len - chart_days ..];
|
||||||
|
|
@ -2088,12 +2102,56 @@ fn fmtContractLine(arena: std.mem.Allocator, prefix: []const u8, c: zfin.OptionC
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Braille dot patterns for the 2x4 matrix within each character cell.
|
||||||
|
/// Layout: [0][3] Bit mapping: dot0=0x01, dot3=0x08
|
||||||
|
/// [1][4] dot1=0x02, dot4=0x10
|
||||||
|
/// [2][5] dot2=0x04, dot5=0x20
|
||||||
|
/// [6][7] dot6=0x40, dot7=0x80
|
||||||
|
const braille_dots = [4][2]u8{
|
||||||
|
.{ 0x01, 0x08 }, // row 0 (top)
|
||||||
|
.{ 0x02, 0x10 }, // row 1
|
||||||
|
.{ 0x04, 0x20 }, // row 2
|
||||||
|
.{ 0x40, 0x80 }, // row 3 (bottom)
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Comptime table of braille character UTF-8 encodings (U+2800..U+28FF).
|
||||||
|
/// Each braille codepoint is 3 bytes in UTF-8: 0xE2 0xA0+hi 0x80+lo.
|
||||||
|
const braille_utf8 = blk: {
|
||||||
|
var table: [256][3]u8 = undefined;
|
||||||
|
for (0..256) |i| {
|
||||||
|
const cp: u21 = 0x2800 + @as(u21, @intCast(i));
|
||||||
|
table[i] = .{
|
||||||
|
@as(u8, 0xE0 | @as(u8, @truncate(cp >> 12))),
|
||||||
|
@as(u8, 0x80 | @as(u8, @truncate((cp >> 6) & 0x3F))),
|
||||||
|
@as(u8, 0x80 | @as(u8, @truncate(cp & 0x3F))),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
break :blk table;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Return a static-lifetime grapheme slice for a braille pattern byte.
|
||||||
|
fn brailleGlyph(pattern: u8) []const u8 {
|
||||||
|
return &braille_utf8[pattern];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Interpolate color between two RGB values. t in [0.0, 1.0].
|
||||||
|
fn lerpColor(a: [3]u8, b: [3]u8, t: f64) [3]u8 {
|
||||||
|
return .{
|
||||||
|
@intFromFloat(@as(f64, @floatFromInt(a[0])) * (1.0 - t) + @as(f64, @floatFromInt(b[0])) * t),
|
||||||
|
@intFromFloat(@as(f64, @floatFromInt(a[1])) * (1.0 - t) + @as(f64, @floatFromInt(b[1])) * t),
|
||||||
|
@intFromFloat(@as(f64, @floatFromInt(a[2])) * (1.0 - t) + @as(f64, @floatFromInt(b[2])) * t),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// Build a braille sparkline chart from candle close prices.
|
/// Build a braille sparkline chart from candle close prices.
|
||||||
/// Uses Unicode braille characters (U+2800..U+28FF) for 2x4 dot matrix per character.
|
/// Uses Unicode braille characters (U+2800..U+28FF) for 2-wide x 4-tall dot matrix per cell.
|
||||||
|
/// Each terminal row provides 4 sub-rows of resolution; each column maps to one data point.
|
||||||
fn buildBrailleChart(arena: std.mem.Allocator, lines: *std.ArrayList(StyledLine), data: []const zfin.Candle, th: theme_mod.Theme) !void {
|
fn buildBrailleChart(arena: std.mem.Allocator, lines: *std.ArrayList(StyledLine), data: []const zfin.Candle, th: theme_mod.Theme) !void {
|
||||||
if (data.len < 2) return;
|
if (data.len < 2) return;
|
||||||
|
|
||||||
const chart_width: usize = 60;
|
const chart_width: usize = 60;
|
||||||
|
const chart_height: usize = 10; // terminal rows
|
||||||
|
const dot_rows: usize = chart_height * 4; // vertical dot resolution
|
||||||
|
|
||||||
// Find min/max close prices
|
// Find min/max close prices
|
||||||
var min_price: f64 = data[0].close;
|
var min_price: f64 = data[0].close;
|
||||||
|
|
@ -2111,64 +2169,116 @@ fn buildBrailleChart(arena: std.mem.Allocator, lines: *std.ArrayList(StyledLine)
|
||||||
const max_label = std.fmt.bufPrint(&max_buf, "${d:.0}", .{max_price}) catch "";
|
const max_label = std.fmt.bufPrint(&max_buf, "${d:.0}", .{max_price}) catch "";
|
||||||
const min_label = std.fmt.bufPrint(&min_buf, "${d:.0}", .{min_price}) catch "";
|
const min_label = std.fmt.bufPrint(&min_buf, "${d:.0}", .{min_price}) catch "";
|
||||||
|
|
||||||
// ASCII block chart (compatible with glyph() which is ASCII-only)
|
// Map each data column to a dot-row position (0 = top, dot_rows-1 = bottom)
|
||||||
const chart_height: usize = 10;
|
const n_cols = @min(data.len, chart_width);
|
||||||
for (0..chart_height) |row| {
|
const dot_y = try arena.alloc(usize, n_cols);
|
||||||
var line_chars: [72]u8 = undefined;
|
const col_color = try arena.alloc([3]u8, n_cols);
|
||||||
var lpos: usize = 0;
|
|
||||||
line_chars[0] = ' ';
|
|
||||||
line_chars[1] = ' ';
|
|
||||||
lpos = 2;
|
|
||||||
|
|
||||||
for (0..chart_width) |col| {
|
for (0..n_cols) |col| {
|
||||||
// Map column to data index
|
const data_idx_f: f64 = @as(f64, @floatFromInt(col)) * @as(f64, @floatFromInt(data.len - 1)) / @as(f64, @floatFromInt(n_cols - 1));
|
||||||
const data_idx_f: f64 = @as(f64, @floatFromInt(col)) * @as(f64, @floatFromInt(data.len - 1)) / @as(f64, @floatFromInt(chart_width - 1));
|
const data_idx: usize = @min(@as(usize, @intFromFloat(data_idx_f)), data.len - 1);
|
||||||
const data_idx: usize = @min(@as(usize, @intFromFloat(data_idx_f)), data.len - 1);
|
const close = data[data_idx].close;
|
||||||
const close = data[data_idx].close;
|
const norm = (close - min_price) / price_range; // 0 = min, 1 = max
|
||||||
|
// Inverted: 0 = top dot row, dot_rows-1 = bottom
|
||||||
|
const y_f = (1.0 - norm) * @as(f64, @floatFromInt(dot_rows - 1));
|
||||||
|
dot_y[col] = @min(@as(usize, @intFromFloat(y_f)), dot_rows - 1);
|
||||||
|
|
||||||
// Map price to row (inverted)
|
// Color: gradient from negative (bottom) to positive (top)
|
||||||
const norm = (close - min_price) / price_range;
|
col_color[col] = lerpColor(th.negative, th.positive, norm);
|
||||||
const fill_row: usize = @intFromFloat((1.0 - norm) * @as(f64, @floatFromInt(chart_height - 1)));
|
}
|
||||||
|
|
||||||
if (row == fill_row) {
|
// Build the braille grid: each cell is a pattern byte
|
||||||
if (lpos < line_chars.len) {
|
// Grid is [chart_height][padded_width] where padded_width includes 2 leading spaces
|
||||||
line_chars[lpos] = '#';
|
const padded_width: usize = n_cols + 2; // 2 leading spaces
|
||||||
lpos += 1;
|
|
||||||
}
|
// Allocate pattern grid
|
||||||
} else if (row > fill_row) {
|
const patterns = try arena.alloc(u8, chart_height * padded_width);
|
||||||
if (lpos < line_chars.len) {
|
@memset(patterns, 0);
|
||||||
line_chars[lpos] = ':';
|
|
||||||
lpos += 1;
|
// For each column, draw a filled area from dot_y[col] down to the bottom,
|
||||||
}
|
// plus interpolate a line between adjacent points for smooth contour.
|
||||||
} else {
|
for (0..n_cols) |col| {
|
||||||
if (lpos < line_chars.len) {
|
const target_y = dot_y[col];
|
||||||
line_chars[lpos] = ' ';
|
// Fill from target_y to bottom
|
||||||
lpos += 1;
|
for (target_y..dot_rows) |dy| {
|
||||||
}
|
const term_row = dy / 4;
|
||||||
}
|
const sub_row = dy % 4;
|
||||||
|
// Left column of the braille cell (we use col 0 of each 2-wide cell)
|
||||||
|
const grid_col = col + 2; // offset for 2 leading spaces
|
||||||
|
patterns[term_row * padded_width + grid_col] |= braille_dots[sub_row][0];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Right label
|
// Interpolate between this point and the next for the line contour
|
||||||
|
if (col + 1 < n_cols) {
|
||||||
|
const y0 = dot_y[col];
|
||||||
|
const y1 = dot_y[col + 1];
|
||||||
|
const min_y = @min(y0, y1);
|
||||||
|
const max_y = @max(y0, y1);
|
||||||
|
// Fill intermediate dots on this column for vertical segments
|
||||||
|
for (min_y..max_y + 1) |dy| {
|
||||||
|
const term_row = dy / 4;
|
||||||
|
const sub_row = dy % 4;
|
||||||
|
const grid_col = col + 2;
|
||||||
|
patterns[term_row * padded_width + grid_col] |= braille_dots[sub_row][0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render each terminal row as a StyledLine with graphemes
|
||||||
|
const bg = th.bg;
|
||||||
|
for (0..chart_height) |row| {
|
||||||
|
const graphemes = try arena.alloc([]const u8, padded_width + 10); // extra for label
|
||||||
|
const styles = try arena.alloc(vaxis.Style, padded_width + 10);
|
||||||
|
var gpos: usize = 0;
|
||||||
|
|
||||||
|
// 2 leading spaces
|
||||||
|
graphemes[gpos] = " ";
|
||||||
|
styles[gpos] = .{ .fg = theme_mod.Theme.vcolor(th.text_muted), .bg = theme_mod.Theme.vcolor(bg) };
|
||||||
|
gpos += 1;
|
||||||
|
graphemes[gpos] = " ";
|
||||||
|
styles[gpos] = styles[0];
|
||||||
|
gpos += 1;
|
||||||
|
|
||||||
|
// Chart columns
|
||||||
|
for (0..n_cols) |col| {
|
||||||
|
const pattern = patterns[row * padded_width + col + 2];
|
||||||
|
graphemes[gpos] = brailleGlyph(pattern);
|
||||||
|
// Use the column's color for non-empty cells, dim for empty
|
||||||
|
if (pattern != 0) {
|
||||||
|
styles[gpos] = .{ .fg = theme_mod.Theme.vcolor(col_color[col]), .bg = theme_mod.Theme.vcolor(bg) };
|
||||||
|
} else {
|
||||||
|
styles[gpos] = .{ .fg = theme_mod.Theme.vcolor(bg), .bg = theme_mod.Theme.vcolor(bg) };
|
||||||
|
}
|
||||||
|
gpos += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right-side price labels
|
||||||
if (row == 0) {
|
if (row == 0) {
|
||||||
const lbl = try std.fmt.allocPrint(arena, " {s}", .{max_label});
|
const lbl = try std.fmt.allocPrint(arena, " {s}", .{max_label});
|
||||||
for (lbl) |ch| {
|
for (lbl) |ch| {
|
||||||
if (lpos < line_chars.len) {
|
if (gpos < graphemes.len) {
|
||||||
line_chars[lpos] = ch;
|
graphemes[gpos] = glyph(ch);
|
||||||
lpos += 1;
|
styles[gpos] = .{ .fg = theme_mod.Theme.vcolor(th.text_muted), .bg = theme_mod.Theme.vcolor(bg) };
|
||||||
|
gpos += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (row == chart_height - 1) {
|
} else if (row == chart_height - 1) {
|
||||||
const lbl = try std.fmt.allocPrint(arena, " {s}", .{min_label});
|
const lbl = try std.fmt.allocPrint(arena, " {s}", .{min_label});
|
||||||
for (lbl) |ch| {
|
for (lbl) |ch| {
|
||||||
if (lpos < line_chars.len) {
|
if (gpos < graphemes.len) {
|
||||||
line_chars[lpos] = ch;
|
graphemes[gpos] = glyph(ch);
|
||||||
lpos += 1;
|
styles[gpos] = .{ .fg = theme_mod.Theme.vcolor(th.text_muted), .bg = theme_mod.Theme.vcolor(bg) };
|
||||||
|
gpos += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const chart_style: vaxis.Style = .{ .fg = theme_mod.Theme.vcolor(th.accent), .bg = theme_mod.Theme.vcolor(th.bg) };
|
try lines.append(arena, .{
|
||||||
try lines.append(arena, .{ .text = try arena.dupe(u8, line_chars[0..lpos]), .style = chart_style });
|
.text = "",
|
||||||
|
.style = .{ .fg = theme_mod.Theme.vcolor(th.text), .bg = theme_mod.Theme.vcolor(bg) },
|
||||||
|
.graphemes = graphemes[0..gpos],
|
||||||
|
.cell_styles = styles[0..gpos],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue