termcast 1.3.48 → 1.3.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +12 -0
- package/dist/build.js.map +1 -1
- package/dist/cli.js +5 -40
- package/dist/cli.js.map +1 -1
- package/dist/colors.d.ts +7 -7
- package/dist/colors.js +7 -7
- package/dist/compile.d.ts +6 -1
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +45 -26
- package/dist/compile.js.map +1 -1
- package/dist/components/actions.js +1 -1
- package/dist/components/actions.js.map +1 -1
- package/dist/components/bar-chart.d.ts +38 -0
- package/dist/components/bar-chart.d.ts.map +1 -0
- package/dist/components/bar-chart.js +158 -0
- package/dist/components/bar-chart.js.map +1 -0
- package/dist/components/bar-graph.d.ts +41 -0
- package/dist/components/bar-graph.d.ts.map +1 -0
- package/dist/components/bar-graph.js +95 -0
- package/dist/components/bar-graph.js.map +1 -0
- package/dist/components/detail.d.ts.map +1 -1
- package/dist/components/detail.js +5 -7
- package/dist/components/detail.js.map +1 -1
- package/dist/components/footer.d.ts.map +1 -1
- package/dist/components/footer.js +8 -9
- package/dist/components/footer.js.map +1 -1
- package/dist/components/form/date-picker.d.ts.map +1 -1
- package/dist/components/form/date-picker.js +7 -1
- package/dist/components/form/date-picker.js.map +1 -1
- package/dist/components/form/dropdown.d.ts.map +1 -1
- package/dist/components/form/dropdown.js +10 -2
- package/dist/components/form/dropdown.js.map +1 -1
- package/dist/components/form/index.d.ts.map +1 -1
- package/dist/components/form/index.js +4 -5
- package/dist/components/form/index.js.map +1 -1
- package/dist/components/form/use-form-navigation.d.ts.map +1 -1
- package/dist/components/form/use-form-navigation.js +6 -0
- package/dist/components/form/use-form-navigation.js.map +1 -1
- package/dist/components/graph.d.ts +111 -0
- package/dist/components/graph.d.ts.map +1 -0
- package/dist/components/graph.js +392 -0
- package/dist/components/graph.js.map +1 -0
- package/dist/components/icon.js +5 -5
- package/dist/components/icon.js.map +1 -1
- package/dist/components/list.d.ts +53 -5
- package/dist/components/list.d.ts.map +1 -1
- package/dist/components/list.js +125 -71
- package/dist/components/list.js.map +1 -1
- package/dist/components/loading-bar.js +3 -3
- package/dist/components/loading-bar.js.map +1 -1
- package/dist/components/loading-text.d.ts +1 -1
- package/dist/components/loading-text.d.ts.map +1 -1
- package/dist/components/loading-text.js +3 -1
- package/dist/components/loading-text.js.map +1 -1
- package/dist/components/metadata.js +2 -2
- package/dist/components/metadata.js.map +1 -1
- package/dist/components/row.d.ts +10 -0
- package/dist/components/row.d.ts.map +1 -0
- package/dist/components/row.js +12 -0
- package/dist/components/row.js.map +1 -0
- package/dist/components/table.d.ts +57 -0
- package/dist/components/table.d.ts.map +1 -0
- package/dist/components/table.js +365 -0
- package/dist/components/table.js.map +1 -0
- package/dist/descendants.js +13 -13
- package/dist/descendants.js.map +1 -1
- package/dist/examples/bar-graph-weekly.d.ts +2 -0
- package/dist/examples/bar-graph-weekly.d.ts.map +1 -0
- package/dist/examples/bar-graph-weekly.js +95 -0
- package/dist/examples/bar-graph-weekly.js.map +1 -0
- package/dist/examples/components-weird-places.d.ts +2 -0
- package/dist/examples/components-weird-places.d.ts.map +1 -0
- package/dist/examples/components-weird-places.js +46 -0
- package/dist/examples/components-weird-places.js.map +1 -0
- package/dist/examples/graph-bar-chart.d.ts +2 -0
- package/dist/examples/graph-bar-chart.d.ts.map +1 -0
- package/dist/examples/graph-bar-chart.js +270 -0
- package/dist/examples/graph-bar-chart.js.map +1 -0
- package/dist/examples/graph-multi-series.d.ts +2 -0
- package/dist/examples/graph-multi-series.d.ts.map +1 -0
- package/dist/examples/graph-multi-series.js +23 -0
- package/dist/examples/graph-multi-series.js.map +1 -0
- package/dist/examples/graph-polymarket.d.ts +2 -0
- package/dist/examples/graph-polymarket.d.ts.map +1 -0
- package/dist/examples/graph-polymarket.js +109 -0
- package/dist/examples/graph-polymarket.js.map +1 -0
- package/dist/examples/graph-row.d.ts +2 -0
- package/dist/examples/graph-row.d.ts.map +1 -0
- package/dist/examples/graph-row.js +226 -0
- package/dist/examples/graph-row.js.map +1 -0
- package/dist/examples/graph-styles.d.ts +2 -0
- package/dist/examples/graph-styles.d.ts.map +1 -0
- package/dist/examples/graph-styles.js +316 -0
- package/dist/examples/graph-styles.js.map +1 -0
- package/dist/examples/list-accessory-table.d.ts +2 -0
- package/dist/examples/list-accessory-table.d.ts.map +1 -0
- package/dist/examples/list-accessory-table.js +46 -0
- package/dist/examples/list-accessory-table.js.map +1 -0
- package/dist/examples/list-item-accessories.d.ts +2 -0
- package/dist/examples/list-item-accessories.d.ts.map +1 -0
- package/dist/examples/list-item-accessories.js +27 -0
- package/dist/examples/list-item-accessories.js.map +1 -0
- package/dist/examples/list-no-actions.d.ts +2 -0
- package/dist/examples/list-no-actions.d.ts.map +1 -0
- package/dist/examples/list-no-actions.js +7 -0
- package/dist/examples/list-no-actions.js.map +1 -0
- package/dist/examples/simple-detail-table.d.ts +2 -0
- package/dist/examples/simple-detail-table.d.ts.map +1 -0
- package/dist/examples/simple-detail-table.js +45 -0
- package/dist/examples/simple-detail-table.js.map +1 -0
- package/dist/examples/simple-graph.d.ts +2 -0
- package/dist/examples/simple-graph.d.ts.map +1 -0
- package/dist/examples/simple-graph.js +32 -0
- package/dist/examples/simple-graph.js.map +1 -0
- package/dist/examples/simple-table-wrap.d.ts +2 -0
- package/dist/examples/simple-table-wrap.d.ts.map +1 -0
- package/dist/examples/simple-table-wrap.js +37 -0
- package/dist/examples/simple-table-wrap.js.map +1 -0
- package/dist/examples/table-edge-cases.d.ts +2 -0
- package/dist/examples/table-edge-cases.d.ts.map +1 -0
- package/dist/examples/table-edge-cases.js +70 -0
- package/dist/examples/table-edge-cases.js.map +1 -0
- package/dist/examples/table-flex-grow.d.ts +2 -0
- package/dist/examples/table-flex-grow.d.ts.map +1 -0
- package/dist/examples/table-flex-grow.js +18 -0
- package/dist/examples/table-flex-grow.js.map +1 -0
- package/dist/extensions/dev.d.ts.map +1 -1
- package/dist/extensions/dev.js +5 -1
- package/dist/extensions/dev.js.map +1 -1
- package/dist/globals.d.ts +1 -0
- package/dist/globals.d.ts.map +1 -1
- package/dist/globals.js +2 -0
- package/dist/globals.js.map +1 -1
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/date-picker-widget.d.ts.map +1 -1
- package/dist/internal/date-picker-widget.js +4 -0
- package/dist/internal/date-picker-widget.js.map +1 -1
- package/dist/internal/providers.d.ts.map +1 -1
- package/dist/internal/providers.js +1 -3
- package/dist/internal/providers.js.map +1 -1
- package/dist/markdown-utils.d.ts +22 -1
- package/dist/markdown-utils.d.ts.map +1 -1
- package/dist/markdown-utils.js +66 -1
- package/dist/markdown-utils.js.map +1 -1
- package/dist/opentui.d.ts +4 -0
- package/dist/opentui.d.ts.map +1 -0
- package/dist/opentui.js +3 -0
- package/dist/opentui.js.map +1 -0
- package/dist/release.d.ts +2 -1
- package/dist/release.d.ts.map +1 -1
- package/dist/release.js +2 -1
- package/dist/release.js.map +1 -1
- package/dist/state.d.ts +1 -0
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +1 -1
- package/dist/state.js.map +1 -1
- package/dist/theme.d.ts +1 -0
- package/dist/theme.d.ts.map +1 -1
- package/dist/theme.js +13 -0
- package/dist/theme.js.map +1 -1
- package/dist/themes/nerv.json +227 -0
- package/dist/themes/termcast.json +72 -71
- package/dist/themes.d.ts +2 -1
- package/dist/themes.d.ts.map +1 -1
- package/dist/themes.js +7 -5
- package/dist/themes.js.map +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +3 -0
- package/dist/utils.js.map +1 -1
- package/package.json +13 -5
- package/src/build.tsx +13 -0
- package/src/cli.tsx +5 -49
- package/src/colors.tsx +7 -7
- package/src/compile.tsx +52 -29
- package/src/components/actions.tsx +1 -1
- package/src/components/bar-chart.tsx +271 -0
- package/src/components/bar-graph.tsx +214 -0
- package/src/components/detail.tsx +7 -8
- package/src/components/footer.tsx +14 -15
- package/src/components/form/date-picker.tsx +9 -0
- package/src/components/form/dropdown.tsx +13 -3
- package/src/components/form/index.tsx +4 -6
- package/src/components/form/use-form-navigation.tsx +6 -0
- package/src/components/graph.tsx +506 -0
- package/src/components/icon.tsx +5 -5
- package/src/components/list.tsx +210 -102
- package/src/components/loading-bar.tsx +3 -3
- package/src/components/loading-text.tsx +4 -2
- package/src/components/metadata.tsx +2 -2
- package/src/components/row.tsx +31 -0
- package/src/components/table.tsx +511 -0
- package/src/descendants.tsx +13 -13
- package/src/examples/action-shortcut.vitest.tsx +1 -1
- package/src/examples/actions-context.vitest.tsx +1 -1
- package/src/examples/bar-graph-weekly.tsx +264 -0
- package/src/examples/bar-graph-weekly.vitest.tsx +275 -0
- package/src/examples/detail-metadata-showcase.vitest.tsx +8 -8
- package/src/examples/form-basic.vitest.tsx +239 -0
- package/src/examples/form-dropdown.vitest.tsx +29 -29
- package/src/examples/form-tagpicker.vitest.tsx +27 -27
- package/src/examples/github.vitest.tsx +4 -4
- package/src/examples/graph-bar-chart.tsx +408 -0
- package/src/examples/graph-bar-chart.vitest.tsx +283 -0
- package/src/examples/graph-multi-series.tsx +36 -0
- package/src/examples/graph-multi-series.vitest.tsx +89 -0
- package/src/examples/graph-polymarket.tsx +182 -0
- package/src/examples/graph-polymarket.vitest.tsx +130 -0
- package/src/examples/graph-row.tsx +347 -0
- package/src/examples/graph-row.vitest.tsx +295 -0
- package/src/examples/graph-styles.tsx +457 -0
- package/src/examples/graph-styles.vitest.tsx +322 -0
- package/src/examples/list-accessory-table.tsx +77 -0
- package/src/examples/list-detail-metadata.vitest.tsx +21 -21
- package/src/examples/list-dropdown-default.vitest.tsx +12 -12
- package/src/examples/list-item-accessories.tsx +106 -0
- package/src/examples/list-item-accessories.vitest.tsx +115 -0
- package/src/examples/list-no-actions.tsx +18 -0
- package/src/examples/list-no-actions.vitest.tsx +97 -0
- package/src/examples/list-spacing-mode.vitest.tsx +6 -6
- package/src/examples/list-with-detail.vitest.tsx +92 -92
- package/src/examples/list-with-dropdown.vitest.tsx +49 -6
- package/src/examples/list-with-sections.vitest.tsx +61 -56
- package/src/examples/simple-detail-markdown.vitest.tsx +21 -17
- package/src/examples/simple-detail-table.tsx +65 -0
- package/src/examples/simple-detail-table.vitest.tsx +200 -0
- package/src/examples/simple-graph.tsx +51 -0
- package/src/examples/simple-graph.vitest.tsx +124 -0
- package/src/examples/simple-grid.vitest.tsx +3 -3
- package/src/examples/simple-list-search.vitest.tsx +65 -0
- package/src/examples/simple-navigation.vitest.tsx +3 -3
- package/src/examples/simple-table-wrap.tsx +55 -0
- package/src/examples/simple-table-wrap.vitest.tsx +91 -0
- package/src/examples/store.vitest.tsx +1 -1
- package/src/examples/table-edge-cases.tsx +72 -0
- package/src/examples/table-edge-cases.vitest.tsx +307 -0
- package/src/examples/table-flex-grow.tsx +53 -0
- package/src/examples/table-flex-grow.vitest.tsx +124 -0
- package/src/extensions/dev.tsx +7 -1
- package/src/globals.ts +3 -0
- package/src/index.tsx +31 -0
- package/src/internal/date-picker-widget.tsx +4 -0
- package/src/internal/providers.tsx +1 -4
- package/src/markdown-utils.tsx +82 -1
- package/src/opentui.tsx +5 -0
- package/src/release.tsx +3 -0
- package/src/state.tsx +2 -1
- package/src/theme.tsx +14 -0
- package/src/themes/nerv.json +231 -0
- package/src/themes/termcast.json +75 -71
- package/src/themes.ts +8 -5
- package/src/utils.tsx +4 -0
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Graph component for rendering line charts in the terminal using braille characters.
|
|
3
|
+
*
|
|
4
|
+
* Uses a custom opentui Renderable (GraphPlotRenderable) for the actual plot drawing,
|
|
5
|
+
* registered via extend() so it can be used as <graph-plot> in JSX. The plot renderable
|
|
6
|
+
* draws directly to OptimizedBuffer using setCell() with braille Unicode characters
|
|
7
|
+
* (U+2800-U+28FF), giving 2x horizontal and 4x vertical sub-pixel resolution per
|
|
8
|
+
* terminal character cell.
|
|
9
|
+
*
|
|
10
|
+
* The React <Graph> component is a thin wrapper that collects series data from
|
|
11
|
+
* <Graph.Line> children, computes axis ranges, and passes everything to the renderable.
|
|
12
|
+
*
|
|
13
|
+
* Braille dot layout per cell:
|
|
14
|
+
* col0 col1
|
|
15
|
+
* 1 8 row0
|
|
16
|
+
* 2 16 row1
|
|
17
|
+
* 4 32 row2
|
|
18
|
+
* 64 128 row3
|
|
19
|
+
*
|
|
20
|
+
* For a plot of W cols x H rows we get W*2 x H*4 virtual pixels.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import React, { ReactNode, useMemo } from 'react'
|
|
24
|
+
import { Renderable, RGBA } from '@opentui/core'
|
|
25
|
+
import type { RenderableOptions, RenderContext } from '@opentui/core'
|
|
26
|
+
import type { OptimizedBuffer } from '@opentui/core'
|
|
27
|
+
import { extend } from '@opentui/react'
|
|
28
|
+
import { useTheme, getThemePalette } from 'termcast/src/theme'
|
|
29
|
+
import { Color, resolveColor } from 'termcast/src/colors'
|
|
30
|
+
|
|
31
|
+
// ── Graph variant ────────────────────────────────────────────────────
|
|
32
|
+
// Three rendering modes for the plot area:
|
|
33
|
+
// - 'area': braille dots filling area under curve (2×4 sub-pixel resolution)
|
|
34
|
+
// - 'filled': solid block characters (2× vertical sub-row resolution)
|
|
35
|
+
// - 'striped': all columns filled, alternating between two colors
|
|
36
|
+
// (pass 'transparent' for one color to get gap-style bars)
|
|
37
|
+
|
|
38
|
+
export type GraphVariant = 'area' | 'filled' | 'striped'
|
|
39
|
+
|
|
40
|
+
// ── Block characters for Filled/Striped modes ───────────────────────
|
|
41
|
+
// We use ▀/▄ with fg+bg color encoding to eliminate the tiny gaps
|
|
42
|
+
// some terminals show between adjacent █ rows.
|
|
43
|
+
// ▀ = top half drawn with fg, bottom half shows bg
|
|
44
|
+
// ▄ = bottom half drawn with fg, top half shows bg
|
|
45
|
+
// ▁ = lower 1/8 block, used as a thin baseline for zero/minimum values
|
|
46
|
+
const UPPER_HALF = '▀' // U+2580
|
|
47
|
+
const LOWER_HALF = '▄' // U+2584
|
|
48
|
+
const LOWER_EIGHTH = '▁' // U+2581
|
|
49
|
+
|
|
50
|
+
// ── Braille bit map ──────────────────────────────────────────────────
|
|
51
|
+
// Maps (subCol, subRow) to the braille bit for that dot position.
|
|
52
|
+
// subCol is 0 or 1, subRow is 0..3.
|
|
53
|
+
const BRAILLE_BITS: number[][] = [
|
|
54
|
+
// col 0 col 1
|
|
55
|
+
[1, 8], // row 0
|
|
56
|
+
[2, 16], // row 1
|
|
57
|
+
[4, 32], // row 2
|
|
58
|
+
[64, 128], // row 3
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
function brailleBit(subCol: number, subRow: number): number {
|
|
62
|
+
return BRAILLE_BITS[subRow]![subCol]!
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── Series data passed to the renderable ─────────────────────────────
|
|
66
|
+
|
|
67
|
+
export interface SeriesData {
|
|
68
|
+
data: number[]
|
|
69
|
+
color: string // hex color
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── GraphPlotRenderable ──────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
export interface GraphPlotOptions extends RenderableOptions {
|
|
75
|
+
series?: SeriesData[]
|
|
76
|
+
xLabels?: string[]
|
|
77
|
+
yMin?: number
|
|
78
|
+
yMax?: number
|
|
79
|
+
yTicks?: number
|
|
80
|
+
yFormat?: (v: number) => string
|
|
81
|
+
axisColor?: string
|
|
82
|
+
variant?: GraphVariant
|
|
83
|
+
stripeColor1?: string // hex color for even columns in striped mode
|
|
84
|
+
stripeColor2?: string // hex color for odd columns in striped mode
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export class GraphPlotRenderable extends Renderable {
|
|
88
|
+
private _series: SeriesData[] = []
|
|
89
|
+
private _xLabels: string[] = []
|
|
90
|
+
private _yMin = 0
|
|
91
|
+
private _yMax = 100
|
|
92
|
+
private _yTicks = 5
|
|
93
|
+
private _yFormat: (v: number) => string = (v) => {
|
|
94
|
+
return v >= 1000 ? v.toFixed(0) : v.toFixed(1)
|
|
95
|
+
}
|
|
96
|
+
private _axisColor: string = '#666666'
|
|
97
|
+
private _variant: GraphVariant = 'area'
|
|
98
|
+
private _stripeColor1: string = '#0080FF'
|
|
99
|
+
private _stripeColor2: string = '#FF8000'
|
|
100
|
+
|
|
101
|
+
constructor(ctx: RenderContext, options: GraphPlotOptions) {
|
|
102
|
+
super(ctx, options)
|
|
103
|
+
if (options.series) this._series = options.series
|
|
104
|
+
if (options.xLabels) this._xLabels = options.xLabels
|
|
105
|
+
if (options.yMin !== undefined) this._yMin = options.yMin
|
|
106
|
+
if (options.yMax !== undefined) this._yMax = options.yMax
|
|
107
|
+
if (options.yTicks !== undefined) this._yTicks = options.yTicks
|
|
108
|
+
if (options.yFormat) this._yFormat = options.yFormat
|
|
109
|
+
if (options.axisColor) this._axisColor = options.axisColor
|
|
110
|
+
if (options.variant) this._variant = options.variant
|
|
111
|
+
if (options.stripeColor1) this._stripeColor1 = options.stripeColor1
|
|
112
|
+
if (options.stripeColor2) this._stripeColor2 = options.stripeColor2
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
set series(value: SeriesData[]) { this._series = value; this.requestRender() }
|
|
116
|
+
set xLabels(value: string[]) { this._xLabels = value; this.requestRender() }
|
|
117
|
+
set yMin(value: number) { this._yMin = value; this.requestRender() }
|
|
118
|
+
set yMax(value: number) { this._yMax = value; this.requestRender() }
|
|
119
|
+
set yTicks(value: number) { this._yTicks = value; this.requestRender() }
|
|
120
|
+
set yFormat(value: (v: number) => string) { this._yFormat = value; this.requestRender() }
|
|
121
|
+
set axisColor(value: string) { this._axisColor = value; this.requestRender() }
|
|
122
|
+
set variant(value: GraphVariant) { this._variant = value; this.requestRender() }
|
|
123
|
+
set stripeColor1(value: string) { this._stripeColor1 = value; this.requestRender() }
|
|
124
|
+
set stripeColor2(value: string) { this._stripeColor2 = value; this.requestRender() }
|
|
125
|
+
|
|
126
|
+
// ── Shared: compute layout and draw axes ─────────────────────
|
|
127
|
+
private computeLayout(): {
|
|
128
|
+
plotX: number; plotY: number; plotW: number; plotH: number
|
|
129
|
+
yAxisWidth: number; yLabels: string[]
|
|
130
|
+
} | null {
|
|
131
|
+
const totalW = this.width
|
|
132
|
+
const totalH = this.height
|
|
133
|
+
if (totalW <= 0 || totalH <= 0) return null
|
|
134
|
+
|
|
135
|
+
const yLabels: string[] = []
|
|
136
|
+
for (let i = 0; i < this._yTicks; i++) {
|
|
137
|
+
const value = this._yMin + (this._yMax - this._yMin) * (1 - i / (this._yTicks - 1))
|
|
138
|
+
yLabels.push(this._yFormat(value))
|
|
139
|
+
}
|
|
140
|
+
const yAxisWidth = Math.max(...yLabels.map((l) => l.length))
|
|
141
|
+
|
|
142
|
+
const plotX = this.x + yAxisWidth + 1
|
|
143
|
+
const plotY = this.y
|
|
144
|
+
const plotH = totalH - 1
|
|
145
|
+
const plotW = totalW - yAxisWidth - 1
|
|
146
|
+
if (plotW <= 0 || plotH <= 0) return null
|
|
147
|
+
|
|
148
|
+
return { plotX, plotY, plotW, plotH, yAxisWidth, yLabels }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private drawAxes(buffer: OptimizedBuffer, layout: {
|
|
152
|
+
plotX: number; plotY: number; plotW: number; plotH: number
|
|
153
|
+
yAxisWidth: number; yLabels: string[]
|
|
154
|
+
}): void {
|
|
155
|
+
const { plotX, plotY, plotW, plotH, yAxisWidth, yLabels } = layout
|
|
156
|
+
const axisRgba = RGBA.fromHex(this._axisColor)
|
|
157
|
+
|
|
158
|
+
// Y-axis labels + separator
|
|
159
|
+
const labelRows = new Set<number>()
|
|
160
|
+
for (let i = 0; i < this._yTicks; i++) {
|
|
161
|
+
const row = Math.round(plotY + (i / (this._yTicks - 1)) * (plotH - 1))
|
|
162
|
+
labelRows.add(row)
|
|
163
|
+
const label = yLabels[i]!
|
|
164
|
+
buffer.drawText(label, this.x + yAxisWidth - label.length, row, axisRgba)
|
|
165
|
+
buffer.drawText('│', this.x + yAxisWidth, row, axisRgba)
|
|
166
|
+
}
|
|
167
|
+
for (let row = plotY; row < plotY + plotH; row++) {
|
|
168
|
+
if (!labelRows.has(row)) {
|
|
169
|
+
buffer.drawText('│', this.x + yAxisWidth, row, axisRgba)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// X-axis labels
|
|
174
|
+
if (this._xLabels.length > 0) {
|
|
175
|
+
const xAxisRow = plotY + plotH
|
|
176
|
+
const labelCount = this._xLabels.length
|
|
177
|
+
for (let i = 0; i < labelCount; i++) {
|
|
178
|
+
const label = this._xLabels[i]!
|
|
179
|
+
const labelX = plotX + Math.round((i / Math.max(1, labelCount - 1)) * (plotW - 1))
|
|
180
|
+
const centeredX = Math.max(plotX, Math.min(labelX - Math.floor(label.length / 2), plotX + plotW - label.length))
|
|
181
|
+
buffer.drawText(label, centeredX, xAxisRow, axisRgba)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ── Shared: interpolate line Y per column ────────────────────
|
|
187
|
+
// Returns an array where lineY[col] = the topmost virtual-row Y
|
|
188
|
+
// of the series line at that column. virtualRows is the total
|
|
189
|
+
// number of virtual rows (pixW for braille, plotW for block modes).
|
|
190
|
+
private computeLineYPerColumn({ series, colCount, virtualH }: {
|
|
191
|
+
series: { data: number[]; color: RGBA }
|
|
192
|
+
colCount: number
|
|
193
|
+
virtualH: number
|
|
194
|
+
}): Int32Array {
|
|
195
|
+
const yRange = this._yMax - this._yMin
|
|
196
|
+
const dataLen = series.data.length
|
|
197
|
+
const lineY = new Int32Array(colCount).fill(virtualH)
|
|
198
|
+
|
|
199
|
+
const pixelYs: number[] = series.data.map((v) => {
|
|
200
|
+
const normalized = (v - this._yMin) / yRange
|
|
201
|
+
return Math.round((1 - normalized) * (virtualH - 1))
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
for (let i = 0; i < dataLen - 1; i++) {
|
|
205
|
+
const x0 = Math.round((i / (dataLen - 1)) * (colCount - 1))
|
|
206
|
+
const y0 = pixelYs[i]!
|
|
207
|
+
const x1 = Math.round(((i + 1) / (dataLen - 1)) * (colCount - 1))
|
|
208
|
+
const y1 = pixelYs[i + 1]!
|
|
209
|
+
|
|
210
|
+
let dx = Math.abs(x1 - x0)
|
|
211
|
+
let dy = Math.abs(y1 - y0)
|
|
212
|
+
const sx = x0 < x1 ? 1 : -1
|
|
213
|
+
const sy = y0 < y1 ? 1 : -1
|
|
214
|
+
let err = dx - dy
|
|
215
|
+
let cx = x0
|
|
216
|
+
let cy = y0
|
|
217
|
+
|
|
218
|
+
while (true) {
|
|
219
|
+
if (cx >= 0 && cx < colCount && cy >= 0 && cy < virtualH) {
|
|
220
|
+
if (cy < lineY[cx]!) lineY[cx] = cy
|
|
221
|
+
}
|
|
222
|
+
if (cx === x1 && cy === y1) break
|
|
223
|
+
const e2 = 2 * err
|
|
224
|
+
if (e2 > -dy) { err -= dy; cx += sx }
|
|
225
|
+
if (e2 < dx) { err += dx; cy += sy }
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (dataLen === 1) {
|
|
230
|
+
const px = Math.round(colCount / 2)
|
|
231
|
+
const py = pixelYs[0]!
|
|
232
|
+
if (px >= 0 && px < colCount && py >= 0 && py < virtualH) {
|
|
233
|
+
lineY[px] = py
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return lineY
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ── Style: Area (braille) ────────────────────────────────────
|
|
241
|
+
private renderArea(buffer: OptimizedBuffer, plotX: number, plotY: number, plotW: number, plotH: number): void {
|
|
242
|
+
const transparent = RGBA.fromValues(0, 0, 0, 0)
|
|
243
|
+
const pixW = plotW * 2
|
|
244
|
+
const pixH = plotH * 4
|
|
245
|
+
const yRange = this._yMax - this._yMin
|
|
246
|
+
if (yRange === 0) return
|
|
247
|
+
|
|
248
|
+
const cellCount = plotW * plotH
|
|
249
|
+
const cellBits = new Uint8Array(cellCount)
|
|
250
|
+
const cellColors: RGBA[] = Array.from({ length: cellCount }, () => transparent)
|
|
251
|
+
|
|
252
|
+
for (const series of this._series) {
|
|
253
|
+
if (series.data.length === 0) continue
|
|
254
|
+
const seriesColor = RGBA.fromHex(series.color)
|
|
255
|
+
|
|
256
|
+
const lineY = this.computeLineYPerColumn({
|
|
257
|
+
series: { data: series.data, color: seriesColor },
|
|
258
|
+
colCount: pixW,
|
|
259
|
+
virtualH: pixH,
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
for (let px = 0; px < pixW; px++) {
|
|
263
|
+
const topY = lineY[px]!
|
|
264
|
+
if (topY >= pixH) continue
|
|
265
|
+
for (let py = topY; py < pixH; py++) {
|
|
266
|
+
const cellX = Math.floor(px / 2)
|
|
267
|
+
const cellY = Math.floor(py / 4)
|
|
268
|
+
const subCol = px % 2
|
|
269
|
+
const subRow = py % 4
|
|
270
|
+
const cellIdx = cellY * plotW + cellX
|
|
271
|
+
if (cellIdx >= 0 && cellIdx < cellCount) {
|
|
272
|
+
cellBits[cellIdx]! |= brailleBit(subCol, subRow)
|
|
273
|
+
cellColors[cellIdx] = seriesColor
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
for (let cy = 0; cy < plotH; cy++) {
|
|
280
|
+
for (let cx = 0; cx < plotW; cx++) {
|
|
281
|
+
const cellIdx = cy * plotW + cx
|
|
282
|
+
const bits = cellBits[cellIdx]!
|
|
283
|
+
if (bits === 0) continue
|
|
284
|
+
buffer.setCell(plotX + cx, plotY + cy, String.fromCharCode(0x2800 + bits), cellColors[cellIdx]!, transparent)
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ── Style: Filled / Striped (block characters) ───────────────
|
|
290
|
+
// Always uses ▀ (upper-half block) with fg=top color, bg=bottom color.
|
|
291
|
+
// This eliminates the tiny gaps some terminals show between adjacent █ rows.
|
|
292
|
+
// Filled: every column uses the series color.
|
|
293
|
+
// Striped: all columns filled, even cols = stripeColor1, odd = stripeColor2.
|
|
294
|
+
// Pass a transparent color to skip those columns (gap-style bars).
|
|
295
|
+
private renderBlock(buffer: OptimizedBuffer, plotX: number, plotY: number, plotW: number, plotH: number, striped: boolean): void {
|
|
296
|
+
const transparent = RGBA.fromValues(0, 0, 0, 0)
|
|
297
|
+
const virtualH = plotH * 2 // 2 sub-rows per terminal row
|
|
298
|
+
const yRange = this._yMax - this._yMin
|
|
299
|
+
if (yRange === 0) return
|
|
300
|
+
|
|
301
|
+
const stripe1 = RGBA.fromHex(this._stripeColor1)
|
|
302
|
+
const stripe2 = RGBA.fromHex(this._stripeColor2)
|
|
303
|
+
|
|
304
|
+
for (const series of this._series) {
|
|
305
|
+
if (series.data.length === 0) continue
|
|
306
|
+
const seriesColor = RGBA.fromHex(series.color)
|
|
307
|
+
|
|
308
|
+
const lineY = this.computeLineYPerColumn({
|
|
309
|
+
series: { data: series.data, color: seriesColor },
|
|
310
|
+
colCount: plotW,
|
|
311
|
+
virtualH,
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
for (let col = 0; col < plotW; col++) {
|
|
315
|
+
// Determine fill color for this column
|
|
316
|
+
const fillColor = striped
|
|
317
|
+
? (col % 2 === 0 ? stripe1 : stripe2)
|
|
318
|
+
: seriesColor
|
|
319
|
+
|
|
320
|
+
// Skip transparent columns (allows gap-style bars in striped mode)
|
|
321
|
+
if (fillColor.a === 0) continue
|
|
322
|
+
|
|
323
|
+
const topVRow = lineY[col]!
|
|
324
|
+
if (topVRow >= virtualH) continue
|
|
325
|
+
|
|
326
|
+
// Fill from topVRow down to virtualH-1 using ▀/▄ with fg+bg encoding.
|
|
327
|
+
// ▀: fg paints top half, bg paints bottom half.
|
|
328
|
+
// ▄: fg paints bottom half, bg paints top half.
|
|
329
|
+
// We never set fg=transparent on a visible glyph part (would show as black).
|
|
330
|
+
for (let row = 0; row < plotH; row++) {
|
|
331
|
+
const vTop = row * 2 // virtual row for top half
|
|
332
|
+
const vBot = row * 2 + 1 // virtual row for bottom half
|
|
333
|
+
const topFilled = vTop >= topVRow
|
|
334
|
+
const botFilled = vBot >= topVRow
|
|
335
|
+
|
|
336
|
+
if (!topFilled && !botFilled) continue
|
|
337
|
+
|
|
338
|
+
if (topFilled && botFilled) {
|
|
339
|
+
// Both halves: ▀ with fg=color, bg=color → seamless full block
|
|
340
|
+
buffer.setCell(plotX + col, plotY + row, UPPER_HALF, fillColor, fillColor)
|
|
341
|
+
} else if (topFilled) {
|
|
342
|
+
// Top only: ▀ with fg=color, bg=transparent
|
|
343
|
+
buffer.setCell(plotX + col, plotY + row, UPPER_HALF, fillColor, transparent)
|
|
344
|
+
} else if (topVRow >= virtualH - 1) {
|
|
345
|
+
// Minimum fill: only the very last virtual row is filled (zero/min value).
|
|
346
|
+
// Use ▁ (lower 1/8 block) for a thin baseline indicator.
|
|
347
|
+
buffer.setCell(plotX + col, plotY + row, LOWER_EIGHTH, fillColor, transparent)
|
|
348
|
+
} else {
|
|
349
|
+
// Bottom only: ▄ with fg=color, bg=transparent
|
|
350
|
+
buffer.setCell(plotX + col, plotY + row, LOWER_HALF, fillColor, transparent)
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// ── Main render ──────────────────────────────────────────────
|
|
358
|
+
protected renderSelf(buffer: OptimizedBuffer): void {
|
|
359
|
+
const layout = this.computeLayout()
|
|
360
|
+
if (!layout) return
|
|
361
|
+
const { plotX, plotY, plotW, plotH } = layout
|
|
362
|
+
|
|
363
|
+
this.drawAxes(buffer, layout)
|
|
364
|
+
|
|
365
|
+
const yRange = this._yMax - this._yMin
|
|
366
|
+
if (yRange === 0 || this._series.length === 0) return
|
|
367
|
+
|
|
368
|
+
switch (this._variant) {
|
|
369
|
+
case 'area': {
|
|
370
|
+
this.renderArea(buffer, plotX, plotY, plotW, plotH)
|
|
371
|
+
break
|
|
372
|
+
}
|
|
373
|
+
case 'filled': {
|
|
374
|
+
this.renderBlock(buffer, plotX, plotY, plotW, plotH, false)
|
|
375
|
+
break
|
|
376
|
+
}
|
|
377
|
+
case 'striped': {
|
|
378
|
+
this.renderBlock(buffer, plotX, plotY, plotW, plotH, true)
|
|
379
|
+
break
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// ── Register the custom renderable ───────────────────────────────────
|
|
386
|
+
|
|
387
|
+
extend({ 'graph-plot': GraphPlotRenderable })
|
|
388
|
+
|
|
389
|
+
declare module '@opentui/react' {
|
|
390
|
+
interface OpenTUIComponents {
|
|
391
|
+
'graph-plot': typeof GraphPlotRenderable
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// ── Graph.Line (data-only child, renders null) ───────────────────────
|
|
396
|
+
|
|
397
|
+
export interface GraphLineProps {
|
|
398
|
+
/** Y-values for this series */
|
|
399
|
+
data: number[]
|
|
400
|
+
/** Line color */
|
|
401
|
+
color?: Color.ColorLike
|
|
402
|
+
/** Series label (for future legend) */
|
|
403
|
+
title?: string
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const GraphLine = (_props: GraphLineProps): any => {
|
|
407
|
+
return null
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// ── Graph React component ────────────────────────────────────────────
|
|
411
|
+
|
|
412
|
+
export interface GraphProps {
|
|
413
|
+
/** Height of the graph in terminal rows (default: 15) */
|
|
414
|
+
height?: number
|
|
415
|
+
/** X-axis labels */
|
|
416
|
+
xLabels?: string[]
|
|
417
|
+
/** Manual Y-axis range [min, max] (default: auto from data) */
|
|
418
|
+
yRange?: [number, number]
|
|
419
|
+
/** Number of Y-axis tick labels (default: 5) */
|
|
420
|
+
yTicks?: number
|
|
421
|
+
/** Custom Y-axis label formatter */
|
|
422
|
+
yFormat?: (v: number) => string
|
|
423
|
+
/** Rendering variant: 'area' (braille), 'filled' (blocks), 'striped' (alternating colors) */
|
|
424
|
+
variant?: GraphVariant
|
|
425
|
+
/** Two alternating colors for 'striped' variant [even, odd]. Defaults to [theme.primary, theme.accent].
|
|
426
|
+
* Pass 'transparent' for one to get gap-style bars. */
|
|
427
|
+
stripeColors?: [Color.ColorLike, Color.ColorLike]
|
|
428
|
+
/** Graph.Line children */
|
|
429
|
+
children: ReactNode
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
interface GraphType {
|
|
433
|
+
(props: GraphProps): any
|
|
434
|
+
Line: (props: GraphLineProps) => any
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const Graph: GraphType = (props) => {
|
|
438
|
+
const theme = useTheme()
|
|
439
|
+
const { height = 15, xLabels = [], yRange, yTicks = 5, yFormat, variant = 'area', stripeColors, children } = props
|
|
440
|
+
|
|
441
|
+
const palette = getThemePalette(theme)
|
|
442
|
+
|
|
443
|
+
// Collect series data from Graph.Line children
|
|
444
|
+
const series = useMemo<SeriesData[]>(() => {
|
|
445
|
+
const result: SeriesData[] = []
|
|
446
|
+
let colorIndex = 0
|
|
447
|
+
React.Children.forEach(children, (child) => {
|
|
448
|
+
if (!React.isValidElement(child)) return
|
|
449
|
+
const childProps = child.props as GraphLineProps
|
|
450
|
+
if (!childProps.data) return
|
|
451
|
+
|
|
452
|
+
const color = resolveColor(childProps.color) || palette[colorIndex % palette.length]!
|
|
453
|
+
result.push({
|
|
454
|
+
data: childProps.data,
|
|
455
|
+
color,
|
|
456
|
+
})
|
|
457
|
+
colorIndex++
|
|
458
|
+
})
|
|
459
|
+
return result
|
|
460
|
+
}, [children, palette])
|
|
461
|
+
|
|
462
|
+
// Auto-compute Y range if not provided
|
|
463
|
+
const computedYRange = useMemo<[number, number]>(() => {
|
|
464
|
+
if (yRange) return yRange
|
|
465
|
+
let min = Infinity
|
|
466
|
+
let max = -Infinity
|
|
467
|
+
for (const s of series) {
|
|
468
|
+
for (const v of s.data) {
|
|
469
|
+
if (v < min) min = v
|
|
470
|
+
if (v > max) max = v
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
if (min === Infinity) return [0, 100]
|
|
474
|
+
// Add small padding so lines don't touch edges
|
|
475
|
+
const padding = (max - min) * 0.05
|
|
476
|
+
return [min - padding, max + padding]
|
|
477
|
+
}, [series, yRange])
|
|
478
|
+
|
|
479
|
+
// Resolve stripe colors (defaults to theme primary + accent)
|
|
480
|
+
const resolvedStripe1 = resolveColor(stripeColors?.[0]) || theme.primary
|
|
481
|
+
const resolvedStripe2 = resolveColor(stripeColors?.[1]) || theme.accent
|
|
482
|
+
|
|
483
|
+
// Total height = plot rows + 1 for x-axis labels
|
|
484
|
+
const totalHeight = height + (xLabels.length > 0 ? 1 : 0)
|
|
485
|
+
|
|
486
|
+
return (
|
|
487
|
+
<graph-plot
|
|
488
|
+
width="100%"
|
|
489
|
+
height={totalHeight}
|
|
490
|
+
series={series}
|
|
491
|
+
xLabels={xLabels}
|
|
492
|
+
yMin={computedYRange[0]}
|
|
493
|
+
yMax={computedYRange[1]}
|
|
494
|
+
yTicks={yTicks}
|
|
495
|
+
yFormat={yFormat}
|
|
496
|
+
axisColor={theme.textMuted}
|
|
497
|
+
variant={variant}
|
|
498
|
+
stripeColor1={resolvedStripe1}
|
|
499
|
+
stripeColor2={resolvedStripe2}
|
|
500
|
+
/>
|
|
501
|
+
)
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
Graph.Line = GraphLine
|
|
505
|
+
|
|
506
|
+
export { Graph }
|
package/src/components/icon.tsx
CHANGED
|
@@ -45,8 +45,8 @@ const ICON_MAP: Record<string, string> = {
|
|
|
45
45
|
'at-symbol-16': '@',
|
|
46
46
|
'band-aid-16': '╋',
|
|
47
47
|
'bank-note-16': '¤',
|
|
48
|
-
'bar-chart-16': '▊',
|
|
49
|
-
'bar-code-16': '▐',
|
|
48
|
+
// 'bar-chart-16': '▊',
|
|
49
|
+
// 'bar-code-16': '▐',
|
|
50
50
|
'bath-tub-16': '▬',
|
|
51
51
|
'battery-16': '▮',
|
|
52
52
|
'battery-charging-16': '▮',
|
|
@@ -188,7 +188,7 @@ const ICON_MAP: Record<string, string> = {
|
|
|
188
188
|
'key-16': '⊢',
|
|
189
189
|
'keyboard-16': '⌗',
|
|
190
190
|
'layers-16': '◫',
|
|
191
|
-
'leaderboard-16': '▊',
|
|
191
|
+
// 'leaderboard-16': '▊',
|
|
192
192
|
'leaf-16': '∿',
|
|
193
193
|
'light-bulb-16': '※',
|
|
194
194
|
'light-bulb-off-16': '※',
|
|
@@ -288,8 +288,8 @@ const ICON_MAP: Record<string, string> = {
|
|
|
288
288
|
'shuffle-16': '⇝',
|
|
289
289
|
'signal-0-16': '▁',
|
|
290
290
|
'signal-1-16': '▂',
|
|
291
|
-
'signal-2-16': '▄',
|
|
292
|
-
'signal-3-16': '█',
|
|
291
|
+
// 'signal-2-16': '▄',
|
|
292
|
+
// 'signal-3-16': '█',
|
|
293
293
|
'snippets-16': '⊠',
|
|
294
294
|
'snowflake-16': '※',
|
|
295
295
|
'soccer-ball-16': '◎',
|