Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions examples/basic-line.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ const timestamps = Array.from({ length: count }, (_, i) => i);
const values = timestamps.map(t => Math.sin(t / 5) * 40 + 50);

const opts = {
scales: { x: { time: false } },
series: [
{},
{ stroke: 'cyan', label: 'Value', width: 2 },
],
axes: [
{ stroke: '#555', grid: { stroke: '#333' } },
{ stroke: '#555', grid: { stroke: '#333' } },
{ stroke: '#999', grid: { stroke: '#333' } },
{ stroke: '#999', grid: { stroke: '#333' } },
],
};

Expand Down
2 changes: 1 addition & 1 deletion examples/dual-y-axis.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const opts = {
{ stroke: '#ffaa00', label: 'Volume', width: 1, scale: 'volume' },
],
axes: [
{ stroke: '#555', grid: { stroke: '#222' } },
{ stroke: '#999', grid: { stroke: '#222' } },
{ stroke: '#00ccff', grid: { stroke: '#222' }, scale: 'y' },
{ stroke: '#ffaa00', grid: { show: false }, scale: 'volume', side: 1 },
],
Expand Down
4 changes: 2 additions & 2 deletions examples/live-trading.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,15 +95,15 @@ function App({ exit }: { exit: () => void }) {
],
axes: [
{
stroke: '#555',
stroke: '#999',
grid: { stroke: '#222' },
values: (_u: any, vals: number[]) => vals.map(v => {
const m = Math.floor(Math.abs(v) / 60);
const s = Math.floor(Math.abs(v) % 60);
return `${v < 0 ? '-' : ''}${m}:${String(s).padStart(2, '0')}`;
}),
},
{ stroke: '#555', grid: { stroke: '#222' }, scale: 'y', side: 1 },
{ stroke: '#999', grid: { stroke: '#222' }, scale: 'y', side: 1 },
],
};

Expand Down
5 changes: 3 additions & 2 deletions examples/multi-series.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,16 @@ const cos = x.map(t => Math.cos(t / 8) * 30 + 50);
const saw = x.map(t => ((t % 40) / 40) * 80 + 10);

const opts = {
scales: { x: { time: false } },
series: [
{},
{ stroke: 'cyan', label: 'Sin', width: 2 },
{ stroke: '#ff6600', label: 'Cos', width: 2 },
{ stroke: '#aa44ff', label: 'Saw', width: 2 },
],
axes: [
{ stroke: '#555', grid: { stroke: '#222' } },
{ stroke: '#555', grid: { stroke: '#222' } },
{ stroke: '#999', grid: { stroke: '#222' } },
{ stroke: '#999', grid: { stroke: '#222' } },
],
};

Expand Down
5 changes: 3 additions & 2 deletions examples/shaded-area.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const y1 = x.map(t => Math.sin(t / 12) * 30 + 60);
const y2 = x.map(t => Math.cos(t / 15) * 20 + 30);

const opts = {
scales: { x: { time: false } },
series: [
{},
{
Expand All @@ -25,8 +26,8 @@ const opts = {
},
],
axes: [
{ stroke: '#555', grid: { stroke: '#222' } },
{ stroke: '#555', grid: { stroke: '#222' } },
{ stroke: '#999', grid: { stroke: '#222' } },
{ stroke: '#999', grid: { stroke: '#222' } },
],
};

Expand Down
4 changes: 2 additions & 2 deletions examples/timestamps.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ const opts = {
{ stroke: '#f7931a', label: 'BTC/USD', width: 2 },
],
axes: [
{ stroke: '#555', grid: { stroke: '#222' } },
{ stroke: '#555', grid: { stroke: '#222' } },
{ stroke: '#999', grid: { stroke: '#222' } },
{ stroke: '#999', grid: { stroke: '#222' } },
],
};

Expand Down
14 changes: 8 additions & 6 deletions src/InkUPlot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,13 @@ export function InkUPlot({
const chartCols = rawMode ? termCols : Math.max(1, termCols - leftLabelWidth - rightLabelWidth);
const chartRows = rawMode ? effHeight : Math.max(1, showAxes ? effHeight - 2 : effHeight);

// Cap canvas pixel dimensions to avoid WASM memory issues.
// Render at 2x cell density (supersample) so the chart stays crisp when the terminal
// upscales the image — at 1x it looks blurry on small/hi-DPI terminals. Display size is
// unchanged; only the pixel resolution goes up. Capped to avoid WASM memory issues.
const MAX_DIM = 4096;
const MAX_PIXELS = 2_000_000;
let canvasWidth = Math.min(chartCols * 8, MAX_DIM);
let canvasHeight = Math.min(chartRows * 16, MAX_DIM);
let canvasWidth = Math.min(chartCols * 16, MAX_DIM);
let canvasHeight = Math.min(chartRows * 32, MAX_DIM);
if (canvasWidth * canvasHeight > MAX_PIXELS) {
const scale = Math.sqrt(MAX_PIXELS / (canvasWidth * canvasHeight));
canvasWidth = Math.floor(canvasWidth * scale);
Expand Down Expand Up @@ -117,11 +119,11 @@ export function InkUPlot({

if (format === 'iterm2') {
// Fast path: node-canvas encodes PNG natively (C), skip chafa WASM entirely.
const png = await renderToPNG(opts, data, canvasWidth, canvasHeight, format);
const png = await renderToPNG(opts, data, canvasWidth, canvasHeight, format, showAxes);
if (cancelled) return;
ansi = iterm2Escape(png, chartCols, chartRows);
} else {
const imageData = await renderToImageData(opts, data, canvasWidth, canvasHeight, format);
const imageData = await renderToImageData(opts, data, canvasWidth, canvasHeight, format, showAxes);
if (cancelled) return;
ansi = await pixelsToTerminal(imageData, {
width: chartCols,
Expand Down Expand Up @@ -160,7 +162,7 @@ export function InkUPlot({
});

return () => { cancelled = true; };
}, [opts, data, canvasWidth, canvasHeight, chartCols, chartRows, format, color, resizeTick]);
}, [opts, data, canvasWidth, canvasHeight, chartCols, chartRows, format, color, showAxes, resizeTick]);

if (error) {
return <Text color="red">Error rendering chart: {error}</Text>;
Expand Down
76 changes: 60 additions & 16 deletions src/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ function sanitizeOpts(
width: number,
height: number,
format: RenderFormat,
showAxes: boolean,
): uPlotType.Options {
const base: uPlotType.Options = {
...opts,
Expand All @@ -22,8 +23,10 @@ function sanitizeOpts(
};

if (format === 'symbols') {
// Braille mode: hide axes (rendered as text by the component), thin lines.
base.axes = (opts.axes ?? []).map(a => ({
// Braille mode: hide canvas axes (rendered as text by the component), thin lines.
// Default to [x, y] so an undefined/empty axes array still hides uPlot's implicit axes
// (uPlot draws default axes when given []).
base.axes = (opts.axes?.length ? opts.axes : [{}, {}]).map(a => ({
...a,
show: false,
grid: { ...a?.grid, show: false },
Expand All @@ -34,23 +37,61 @@ function sanitizeOpts(
if (i === 0) return s;
return { ...s, width: Math.max((s as any).width ?? 1, 3) };
});
} else {
// Pixel-perfect mode (kitty/sixels/iterm2): keep uPlot's canvas axes.
base.axes = (opts.axes ?? []).map(a => ({

return base;
}

// Pixel-perfect mode (kitty/sixels/iterm2): uPlot draws the axes on the canvas.
if (!showAxes) {
// Borderless chart — hide axes, grid, and ticks entirely.
// Default to [x, y] so an undefined/empty axes array still hides uPlot's implicit axes
// (uPlot draws default axes when given []).
base.axes = (opts.axes?.length ? opts.axes : [{}, {}]).map(a => ({
...a,
stroke: a?.stroke ?? '#888',
grid: { show: true, stroke: '#333', ...a?.grid },
ticks: { show: true, stroke: '#555', ...a?.ticks },
font: `${Math.max(10, Math.round(height / 40))}px sans-serif`,
show: false,
grid: { ...a?.grid, show: false },
ticks: { ...a?.ticks, show: false },
}));
} else {
const fontPx = Math.max(10, Math.round(height / 40));
const fontStr = `${fontPx}px sans-serif`;

// Auto-size vertical (y) axes to their widest label. uPlot's default ~50px width
// clips large labels (e.g. "13,000") at the canvas edge, especially at bigger fonts.
const autoSizeY = (self: any, values: string[] | null, axisIdx: number, cycleNum: number): number => {
const axis = self.axes[axisIdx];
if (cycleNum > 1 && axis._size != null) return axis._size;
self.ctx.font = fontStr;
let maxW = 0;
for (const v of values ?? []) {
const w = self.ctx.measureText(String(v)).width;
if (w > maxW) maxW = w;
}
return Math.ceil(maxW) + 20; // label + tick + gap
};

base.series = (opts.series ?? []).map((s, i) => {
if (i === 0) return s;
const w = (s as any).width ?? 2;
return { ...s, width: w * 3 };
// Default to [x, y] so implicit axes (undefined/empty array — uPlot draws them anyway)
// still get styling and auto-sizing, not uPlot's tiny unstyled defaults.
base.axes = (opts.axes?.length ? opts.axes : [{}, {}]).map((a, i) => {
// side 1/3 are vertical; an undefined-side axis after index 0 defaults to a left y-axis.
const isY = a?.side === 1 || a?.side === 3 || (i > 0 && a?.side == null);
return {
...a,
stroke: a?.stroke ?? '#aaa',
grid: { show: true, stroke: '#333', ...a?.grid },
ticks: { show: true, stroke: '#666', ...a?.ticks },
font: fontStr,
...(isY && (a as any)?.size == null ? { size: autoSizeY } : {}),
};
});
}

base.series = (opts.series ?? []).map((s, i) => {
if (i === 0) return s;
const w = (s as any).width ?? 2;
return { ...s, width: w * 3 };
});

return base;
}

Expand All @@ -69,6 +110,7 @@ async function renderChart(
canvasWidth: number,
canvasHeight: number,
format: RenderFormat,
showAxes: boolean,
) {
const shim = installDOMShim(canvasWidth, canvasHeight);

Expand All @@ -85,7 +127,7 @@ async function renderChart(
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
}

const sanitized = sanitizeOpts(opts, canvasWidth, canvasHeight, format);
const sanitized = sanitizeOpts(opts, canvasWidth, canvasHeight, format, showAxes);

const chart = new uPlotCtor(sanitized, data, (self, init) => {
init();
Expand All @@ -106,8 +148,9 @@ export async function renderToImageData(
canvasWidth: number,
canvasHeight: number,
format: RenderFormat = 'symbols',
showAxes = true,
): Promise<{ data: Uint8ClampedArray; width: number; height: number }> {
const { canvas, chart } = await renderChart(opts, data, canvasWidth, canvasHeight, format);
const { canvas, chart } = await renderChart(opts, data, canvasWidth, canvasHeight, format, showAxes);

try {
const ctx = canvas.getContext('2d');
Expand All @@ -134,8 +177,9 @@ export async function renderToPNG(
canvasWidth: number,
canvasHeight: number,
format: RenderFormat = 'iterm2',
showAxes = true,
): Promise<Buffer> {
const { canvas, chart } = await renderChart(opts, data, canvasWidth, canvasHeight, format);
const { canvas, chart } = await renderChart(opts, data, canvasWidth, canvasHeight, format, showAxes);

try {
const png = canvas.toBuffer('image/png');
Expand Down
Loading