Skip to main content

Draw charts in the terminal

Use the charting helpers for sparklines, line and area charts, histograms, heatmaps, pie charts, and sequence diagrams that belong naturally in terminal output. They draw into Ultraviolet buffers and can be printed as text or placed inside a widget app.

The API is currently exported by package:artisanal/artisanal.dart.

Quick Start (Sparkline)

import 'package:artisanal/artisanal.dart' as chart;

void main() {
final values = [12, 18, 22, 19, 25, 29, 31, 28, 24, 26, 30, 34];
final lines = chart.renderChartLines(40, 3, (screen, area) {
chart.drawSparkline(
screen,
area,
values,
style: chart.uvStyleFromHex('#56ccf2'),
showGrid: true,
gridStyle: chart.uvStyleFromHex('#3b4252'),
);
});
print(lines.join('\n'));
}

Line Chart

import 'dart:math' as math;
import 'package:artisanal/artisanal.dart' as chart;

void main() {
final series = _series(60, seed: 13, min: 20, max: 90);
final lines = chart.renderChartLines(64, 12, (screen, area) {
chart.drawLineChart(
screen,
area,
series,
lineStyle: chart.uvStyleFromHex('#56ccf2'),
showGrid: true,
gridRows: 3,
gridCols: 3,
gridStyle: chart.uvStyleFromHex('#3b4252'),
xLabels: const ['-60s', '-30s', 'now'],
yLabels: const ['100%', '50%', '0%'],
labelStyle: chart.uvStyleFromHex('#3b4252'),
);
});
print(lines.join('\n'));
}

List<double> _series(
int count, {
required int seed,
required double min,
required double max,
}) {
final rng = math.Random(seed);
var value = (min + max) / 2;
return List<double>.generate(count, (i) {
value += (rng.nextDouble() * 2 - 1) * ((max - min) * 0.08);
value = value.clamp(min, max).toDouble();
return value;
}, growable: false);
}

Ribbon / Area Chart

import 'dart:math' as math;
import 'package:artisanal/artisanal.dart' as chart;

void main() {
final seriesA = _series(60, seed: 13, min: 20, max: 90);
final seriesB = _series(60, seed: 42, min: 10, max: 75);
final seriesC = _series(60, seed: 77, min: 5, max: 55);

final lines = chart.renderChartLines(64, 12, (screen, area) {
final styles = [
chart.uvStyleFromHex('#00bbf9'),
chart.uvStyleFromHex('#00f5d4'),
chart.uvStyleFromHex('#f15bb5'),
];
chart.drawRibbonChart(
screen,
area,
[seriesA, seriesB, seriesC],
styles: styles,
showGrid: true,
gridRows: 2,
gridStyle: chart.uvStyleFromHex('#3b4252'),
);
});

print(lines.join('\n'));
}

List<double> _series(
int count, {
required int seed,
required double min,
required double max,
}) {
final rng = math.Random(seed);
var value = (min + max) / 2;
return List<double>.generate(count, (i) {
value += (rng.nextDouble() * 2 - 1) * ((max - min) * 0.08);
value = value.clamp(min, max).toDouble();
return value;
}, growable: false);
}

Histogram

import 'dart:math' as math;
import 'package:artisanal/artisanal.dart' as chart;

void main() {
final values = _series(50, seed: 99, min: 0, max: 100);
final lines = chart.renderChartLines(64, 10, (screen, area) {
chart.drawHistogram(
screen,
area,
values,
barStyle: chart.uvStyleFromHex('#9b5de5'),
axisStyle: chart.uvStyleFromHex('#3b4252'),
showGrid: true,
gridRows: 3,
gridCols: 2,
gridStyle: chart.uvStyleFromHex('#3b4252'),
xLabels: const ['0', 'mid', 'max'],
yLabels: const ['100', '50', '0'],
labelStyle: chart.uvStyleFromHex('#3b4252'),
);
});
print(lines.join('\n'));
}

List<double> _series(
int count, {
required int seed,
required double min,
required double max,
}) {
final rng = math.Random(seed);
return List<double>.generate(
count,
(i) => rng.nextDouble() * (max - min) + min,
growable: false,
);
}

Heatmap

import 'dart:math' as math;
import 'package:artisanal/artisanal.dart' as chart;

void main() {
final grid = _heatmap(28, 12);
final lines = chart.renderChartLines(64, 12, (screen, area) {
chart.drawHeatmap(
screen,
area,
grid,
ramp: chart.ChartRamp.thermal(),
showGrid: true,
gridRows: 3,
gridCols: 4,
gridStyle: chart.uvStyleFromHex('#3b4252'),
);
});
print(lines.join('\n'));
}

List<List<double>> _heatmap(int width, int height) {
final rng = math.Random(7);
return List.generate(
height,
(y) => List<double>.generate(
width,
(x) => (math.sin(x * 0.22) + math.cos(y * 0.31)) * 0.25 +
rng.nextDouble() * 0.5,
growable: false,
),
growable: false,
);
}

Pie Chart

import 'package:artisanal/artisanal.dart' as chart;

void main() {
final lines = chart.renderChartLines(40, 12, (screen, area) {
final styles = [
chart.uvStyleFromHex('#ff006e'),
chart.uvStyleFromHex('#8338ec'),
chart.uvStyleFromHex('#3a86ff'),
chart.uvStyleFromHex('#ffbe0b'),
];
chart.drawPieChart(
screen,
area,
[30, 20, 15, 35],
styles: styles,
donut: true,
useBackground: true,
);
});
print(lines.join('\n'));
}

Notes:

  • Pie/donut rendering uses sub-cell sampling with quarter/half block glyphs, which gives smoother circular edges than full-cell fill alone.

Crosshair Overlay

import 'package:artisanal/artisanal.dart' as chart;

void main() {
final lines = chart.renderChartLines(40, 12, (screen, area) {
chart.drawLineChart(screen, area, [12, 20, 18, 30, 22]);

// Standard mode: draw line glyphs on empty cells and tint chart cells.
chart.drawCrosshair(
screen,
area,
18,
5,
style: chart.uvStyleFromHex('#ffff66'),
);
});

print(lines.join('\n'));
}

Use drawOnEmpty: false when you only want to tint existing chart cells (useful for compact pie/donut overlays):

chart.drawCrosshair(
screen,
area,
x,
y,
style: chart.uvStyleFromHex('#ffff66'),
drawOnEmpty: false,
);

Legend and Palette

Legend entries are typically shown in a framed panel so labels stay readable over dense chart regions.

import 'package:artisanal/artisanal.dart' as chart;

void main() {
final ramp = chart.ChartRamp.fromHexes(
['#0b132b', '#1b2a6b', '#3a86ff', '#88ffcc', '#ffbe0b', '#ff006e'],
);

final entries = List<chart.ChartLegendEntry>.generate(
ramp.colors.length,
(i) {
final t = ramp.colors.length == 1 ? 0.0 : i / (ramp.colors.length - 1);
final style = ramp.styleFor(t, background: true);
return chart.ChartLegendEntry(label: 'step $i', style: style);
},
growable: false,
);

final lines = chart.renderChartLines(30, 6, (screen, area) {
chart.drawLegend(screen, area, entries, columns: 2, rowGap: 0);
});

print(lines.join('\n'));
}

Sequence Diagrams

Sequence diagrams visualize interactions between actors/participants over time, showing message flows and control structures. Artisanal implements Mermaid-compatible syntax for terminal rendering.

Overview

Sequence diagrams are ideal for documenting:

  • API call flows between services
  • User authentication workflows
  • Request/response patterns
  • State machine transitions
  • Protocol negotiations

The renderer produces clean ASCII-art output with Unicode box-drawing characters, suitable for terminal display.

API Options

String Convenience API

import 'package:artisanal/artisanal.dart' as chart;

void main() {
final text = chart.renderSequenceDiagram('''
sequenceDiagram
participant U as User
participant S as Server
U->>S: GET /api
S-->>U: 200 OK
''');
print(text);
}

UV Canvas API

For fine-grained control over rendering:

import 'package:artisanal/artisanal.dart' as chart;
import 'package:ultraviolet/ultraviolet.dart' show Canvas, rect;

void main() {
final source = '''
sequenceDiagram
participant A as Alice
participant B as Bob
A->>B: Hello Bob
''';

final diagram = chart.parseSequenceDiagram(source);
if (diagram != null) {
final layout = chart.layoutSequenceDiagram(diagram);
final canvas = Canvas(layout.width, layout.height);
chart.drawSequenceDiagram(
canvas,
rect(0, 0, layout.width, layout.height),
diagram,
);
print(canvas.render());
}
}

Widget API

For TUI applications using artisanal_widgets:

import 'package:artisanal_widgets/charting.dart';

// In your widget tree:
SequenceDiagramChart(
mermaid: '''
sequenceDiagram
participant C as Client
participant S as Server
C->>S: Request
S-->>C: Response
''',
width: 80,
height: 20,
)

Mermaid Syntax

Supported Mermaid sequence diagram syntax:

Participant Declarations

sequenceDiagram
participant A as Alice
participant B as Bob
participant C

Participants can be declared with or without an alias using as. Undeclared participants are auto-created when referenced in messages.

Message Arrows

ArrowStyleDescription
->>solidSynchronous request
-->>dashedAsynchronous response
->solidSimple message
-->dashedReturn message
-xsolidLost message
--xdashedLost response
-\)solidOpen arrow
--\)dashedDashed open arrow

Activate/deactivate lifelines with + and -:

sequenceDiagram
A->>+B: Activate B
B-->>-A: Deactivate A

Notes

sequenceDiagram
participant A as Alice
participant B as Bob
note right of A: Local note
note over A,B: Spanning both

Position notes with right of, left of, or over participants.

Control Fragments

sequenceDiagram
alt Authentication Success
A->>B: Authenticated
else Authentication Failed
A->>B: Rejected
end

loop Retry Logic
A->>B: Try again
B-->>A: Response
end

opt Cache Miss
A->>C: Fetch data
end

critical Database Write
A->>D: Commit
end

Supported fragments: alt/else/end, loop, opt, critical, par/and/end, and rect.

Autonumbering

sequenceDiagram
autonumber
A->>B: First
B-->>A: Second

autonumber 10 5
A->>B: Tenth (starts at 10, increments by 5)

CSS Color Names

Use CSS color names for inline styling:

sequenceDiagram
A->>B: Blue message #blue
B-->>A: Red response #red

Color names are parsed from Mermaid style declarations and arrow colors.

Customization

SequenceDiagramTheme

Configure visual appearance with a custom theme:

import 'package:artisanal/artisanal.dart' as chart;
import 'package:ultraviolet/ultraviolet.dart' show UvColor, UvStyle;

final customTheme = const chart.SequenceDiagramTheme(
participantBox: UvStyle(fg: UvColor.rgb(100, 150, 200)),
participantLabel: UvStyle(fg: UvColor.rgb(255, 255, 255)),
lifeline: UvStyle(fg: UvColor.rgb(80, 80, 80)),
request: UvStyle(fg: UvColor.rgb(100, 255, 150)),
response: UvStyle(fg: UvColor.rgb(255, 150, 100)),
note: UvStyle(fg: UvColor.rgb(200, 200, 150)),
noteBackground: UvStyle(bg: UvColor.rgb(40, 40, 40)),
fragment: UvStyle(fg: UvColor.rgb(150, 200, 150)),
fragmentLabel: UvStyle(fg: UvColor.rgb(150, 200, 150), bg: UvColor.rgb(30, 45, 35)),
group: UvStyle(fg: UvColor.rgb(120, 140, 130)),
rect: UvStyle(fg: UvColor.rgb(150, 150, 150), bg: UvColor.rgb(35, 35, 35)),
);

final text = chart.renderSequenceDiagram(source, theme: customTheme);

Participant Ordering

Participants appear in declaration order. For custom ordering, declare participants explicitly before messages:

sequenceDiagram
participant C as Database
participant B as Service
participant A as Client
A->>B: Call service
B->>C: Query database

RTL Support

The renderer respects implicit participant ordering. For right-to-left diagrams, declare participants in reverse order:

sequenceDiagram
participant C
participant B
participant A
A->>C: Flows right-to-left

Width/Height Constraints

Control diagram dimensions:

import 'package:artisanal/artisanal.dart' as chart;

final layout = chart.layoutSequenceDiagram(
diagram,
options: const chart.SequenceDiagramOptions(
minParticipantGap: 20,
),
);

Examples

Basic Sequence Diagram

import 'package:artisanal/artisanal.dart' as chart;

void main() {
final text = chart.renderSequenceDiagram('''
sequenceDiagram
participant U as User
participant A as Auth Service
participant D as Database

U->>A: Login request
A->>D: Query user
D-->>A: User data
A-->>U: Auth token
''');
print(text);
}

Parsed Diagram with Custom Rendering

import 'package:artisanal/artisanal.dart' as chart;
import 'package:ultraviolet/ultraviolet.dart' show Canvas, rect, UvColor, UvStyle;

void main() {
final source = '''
sequenceDiagram
participant W as WebClient
participant S as Server
participant D as Database

autonumber
W->>S: POST /users
S->>+D: INSERT user
D-->>-S: OK
S-->>W: 201 Created
''';

final diagram = chart.parseSequenceDiagram(source);
if (diagram != null) {
final layout = chart.layoutSequenceDiagram(diagram);
final canvas = Canvas(layout.width, layout.height);
chart.drawSequenceDiagram(canvas, rect(0, 0, layout.width, layout.height), diagram);
print(canvas.render());
}
}

Widget Usage in TUI App

import 'package:artisanal/bubbles.dart';

void main() {
final diagram = SequenceDiagramModel(
mermaid: '''
sequenceDiagram
participant C as Client
participant S as Server
C->>S: GET /health
S-->>C: 200 OK
''',
width: 60,
height: 15,
);

print(diagram.view());
}

Theme Customization

import 'package:artisanal/artisanal.dart' as chart;
import 'package:ultraviolet/ultraviolet.dart' show UvColor, UvStyle;

void main() {
final darkTheme = const chart.SequenceDiagramTheme(
participantBox: UvStyle(fg: UvColor.rgb(80, 120, 100)),
participantLabel: UvStyle(fg: UvColor.rgb(200, 220, 210)),
lifeline: UvStyle(fg: UvColor.rgb(60, 80, 70)),
request: UvStyle(fg: UvColor.rgb(100, 255, 180)),
response: UvStyle(fg: UvColor.rgb(255, 200, 100)),
note: UvStyle(fg: UvColor.rgb(180, 200, 170), bg: UvColor.rgb(30, 45, 35)),
fragment: UvStyle(fg: UvColor.rgb(120, 180, 140)),
fragmentLabel: UvStyle(fg: UvColor.rgb(120, 180, 140), bg: UvColor.rgb(25, 40, 30)),
group: UvStyle(fg: UvColor.rgb(90, 110, 100)),
rect: UvStyle(fg: UvColor.rgb(140, 140, 140), bg: UvColor.rgb(35, 35, 35)),
);

final text = chart.renderSequenceDiagram('''
sequenceDiagram
participant A as Alice
participant B as Bob
A->>B: Hello #green
B-->>A: Hi back #blue
''', theme: darkTheme);

print(text);
}

Integration

UV-based Rendering

Use in any context where you have a UV Canvas:

import 'package:artisanal/artisanal.dart' as chart;
import 'package:ultraviolet/ultraviolet.dart' show Canvas, rect;

String renderDiagram(String mermaid) {
final diagram = chart.parseSequenceDiagram(mermaid);
if (diagram == null) return 'Invalid diagram';

final layout = chart.layoutSequenceDiagram(diagram);
final canvas = Canvas(layout.width, layout.height);
chart.drawSequenceDiagram(
canvas,
rect(0, 0, layout.width, layout.height),
diagram,
);
return canvas.render();
}

Widget-based TUI Apps

The SequenceDiagramChart widget integrates with artisanal_widgets:

import 'package:artisanal_widgets/charting.dart';

// Direct usage
SequenceDiagramChart(
mermaid: source,
width: 100,
height: 30,
)

Combine with other charting primitives in a single view:

import 'package:artisanal/artisanal.dart' as chart;

// Render multiple diagrams
for (final source in sources) {
print(chart.renderSequenceDiagram(source));
print(''); // blank line between
}

Where to go next