diff --git a/examples/basic-line.tsx b/examples/basic-line.tsx
index 3a36ec2..1e33fac 100644
--- a/examples/basic-line.tsx
+++ b/examples/basic-line.tsx
@@ -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' } },
],
};
diff --git a/examples/dual-y-axis.tsx b/examples/dual-y-axis.tsx
index fbb1ac0..da16af2 100644
--- a/examples/dual-y-axis.tsx
+++ b/examples/dual-y-axis.tsx
@@ -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 },
],
diff --git a/examples/live-trading.tsx b/examples/live-trading.tsx
index ea1a9bd..d38dfe2 100644
--- a/examples/live-trading.tsx
+++ b/examples/live-trading.tsx
@@ -95,7 +95,7 @@ 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);
@@ -103,7 +103,7 @@ function App({ exit }: { exit: () => void }) {
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 },
],
};
diff --git a/examples/multi-series.tsx b/examples/multi-series.tsx
index abd46fd..0a49e0b 100644
--- a/examples/multi-series.tsx
+++ b/examples/multi-series.tsx
@@ -10,6 +10,7 @@ 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 },
@@ -17,8 +18,8 @@ const opts = {
{ 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' } },
],
};
diff --git a/examples/shaded-area.tsx b/examples/shaded-area.tsx
index 248d654..0da0747 100644
--- a/examples/shaded-area.tsx
+++ b/examples/shaded-area.tsx
@@ -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: [
{},
{
@@ -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' } },
],
};
diff --git a/examples/timestamps.tsx b/examples/timestamps.tsx
index 54d9da0..b0b6b2d 100644
--- a/examples/timestamps.tsx
+++ b/examples/timestamps.tsx
@@ -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' } },
],
};
diff --git a/src/InkUPlot.tsx b/src/InkUPlot.tsx
index 984a9e4..f28fb2d 100644
--- a/src/InkUPlot.tsx
+++ b/src/InkUPlot.tsx
@@ -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);
@@ -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,
@@ -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 Error rendering chart: {error};
diff --git a/src/renderer.ts b/src/renderer.ts
index 3d87207..38c5ec9 100644
--- a/src/renderer.ts
+++ b/src/renderer.ts
@@ -10,6 +10,7 @@ function sanitizeOpts(
width: number,
height: number,
format: RenderFormat,
+ showAxes: boolean,
): uPlotType.Options {
const base: uPlotType.Options = {
...opts,
@@ -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 },
@@ -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;
}
@@ -69,6 +110,7 @@ async function renderChart(
canvasWidth: number,
canvasHeight: number,
format: RenderFormat,
+ showAxes: boolean,
) {
const shim = installDOMShim(canvasWidth, canvasHeight);
@@ -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();
@@ -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');
@@ -134,8 +177,9 @@ export async function renderToPNG(
canvasWidth: number,
canvasHeight: number,
format: RenderFormat = 'iterm2',
+ showAxes = true,
): Promise {
- 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');