Table of Contents
- Series Context
- Why ASCII Charts Matter
- The Code Size Reality: 2.5x Larger
- The String Formatting Gap (Biggest Challenge)
- What Worked Brilliantly
- The Banker's Rounding Gotcha
- Development Velocity
- Practical Example: ML Serving Monitoring
- Lessons for Porting Python to Mojo
- What This Series Taught Me
- The String Formatting Reality Check
- Conclusion: Is Mojo Ready?
- What's Next
Final post in the Mojo library development series: Porting Python's asciichartpy (215 lines) to Mojo (548 lines), achieving pixel-perfect compatibility.
Series Context
This is the third and final post in my Mojo library development series:
- Part 1: Building mojo-dotenv - Configuration management (~200 lines Mojo code, 42 tests)
- Part 2: Building mojo-toml - TOML parser (~1,500 lines Mojo code, 96 tests)
- Part 3: This post - ASCII charting (~550 lines Mojo code, 29 tests)
Together, these three projects represent my "Month of Mojo" intensive learning journey, transitioning from simple line-based parsing to complex recursive parsers, and finally to visualisation.
Why ASCII Charts Matter
ASCII charts solve real problems in constrained environments: SSH sessions, CI/CD logs, embedded systems, and terminal monitoring. No GUI required—just fast, lightweight visualisation. They're particularly valuable for:
- Production monitoring - ML model serving latency tracking
- CI/CD pipelines - Build time trends without external dashboards
- Development - Quick visual feedback during prototyping
- Embedded systems - Resource-constrained environments without graphical capabilities
The Code Size Reality: 2.5x Larger
| Metric | Python | Mojo | Notes |
|---|---|---|---|
| Lines of code | 215 Python | 548 Mojo | 2.5x larger |
| Core logic | ~180 | ~430 | Similar complexity |
| Type declarations | Minimal | Explicit | Mojo requires types everywhere |
| String formatting | f"{x:8.2f}" | ~60 lines | Generic formatter |
| Dependencies | stdlib only | stdlib only | Both self-contained |
Why is Mojo larger?
- Missing string formatting - Python's
f"{value:8.2f}"required ~60 lines for a generic formatter - Explicit types - Every variable needs
var data: List[Float64]declarations - Ownership semantics - Must manage memory with
var,owned,borrowedexplicitly - No default arguments yet - Requires more function overloads
- Verbose struct initialisation -
@fieldwise_initboilerplate
The String Formatting Gap (Biggest Challenge)
Python (1 line):
label = f"{value:8.2f} "
Mojo (60+ lines for generic formatter):
fn format_float(value: Float64, width: Int, precision: Int) -> String:
"""Format a Float64 with specified width and precision.
Mimics Python's format specifier {value:width.precisionf}.
For example, format_float(12.3456, 8, 2) produces " 12.35".
"""
var prec = precision if precision >= 0 else 0
# Extract and handle negative values
var int_part = Int(value)
var frac_part = value - Float64(int_part)
var is_negative = value < 0
if is_negative:
int_part = -int_part
frac_part = -frac_part
# Convert fractional part with rounding
var multiplier = 1.0
for _ in range(prec):
multiplier *= 10.0
var frac_int = Int(frac_part * multiplier + 0.5)
# Handle rounding overflow (e.g., 0.999 -> 1.00)
if frac_int >= Int(multiplier):
int_part += 1
frac_int = 0
# Build string with padding
var result = String(int_part)
if prec > 0:
result += "."
var frac_str = String(frac_int)
var padding_needed = prec - len(frac_str)
for _ in range(padding_needed):
result += "0"
result += frac_str
if is_negative:
result = "-" + result
# Right-align in width
while len(result) < width:
result = " " + result
return result
# Usage for chart labels:
fn _format_label(value: Float64) -> String:
return format_float(value, 8, 2) + " "
The impact: String formatting accounted for ~60 lines of the size difference. Rather than hardcoding for 8.2f, I built a generic format_float(value, width, precision) function that works for any format.
Why needed? Mojo's string formatting (v0.25.7) doesn't yet support format specifiers like {:8.2f}—it only handles basic {} substitution. The docs note: "Format specifiers for controlling output format (width, precision, and so on)" are not yet supported. Once Mojo adds this, the custom formatter can be replaced with native functionality.
What Worked Brilliantly
1. UTF-8 Support
Mojo handled box-drawing characters (┼├┤╭╮╯╰│─) perfectly—no special encoding needed. This is actually better than C/C++, where Unicode handling often requires external libraries.
2. Python Interop for Validation
Used asciichartpy directly in six interop tests to verify pixel-perfect compatibility:
from python import Python
fn test_simple_comparison() raises:
# Python
var py_dotenv = Python.import_module("asciichartpy")
var py_output = py_dotenv.plot(py_data)
# Mojo
var mojo_output = plot(mojo_data)
# Verify identical
assert_equal(String(py_output), mojo_output)
This continuous validation caught subtle differences in banker's rounding that would have been painful to debug in production.
3. ANSI Colour Support
Added six colour themes using Mojo's stdlib (utils._ansi.Color) with zero external dependencies:
from asciichart import plot, Config, ChartColors
fn main() raises:
var config = Config()
config.colors = ChartColors.matrix() # Green terminal theme
print(plot(data, config))
Adding colours requires zero external dependencies—just Mojo's stdlib.
The Banker's Rounding Gotcha
Python uses IEEE 754 round-half-to-even. Simple floor(x + 0.5) rounding failed:
// Wrong: floor(x + 0.5)
12.5 → 13 // Should be 12
20.5 → 21 // Should be 20
// Correct: Banker's rounding
if diff == 0.5:
var floor_int = Int(floored)
rounded = floor_int if floor_int % 2 == 0 else floor_int + 1
Lesson: For pixel-perfect compatibility, match Python's rounding behaviour exactly. This small detail broke output alignment until fixed.
Development Velocity
- Initial port: 2 hours (core plotting algorithm)
- Python compatibility: 2.5 hours (formatting, banker's rounding)
- Testing: 1.5 hours (29 tests including 6 Python interop tests)
- Colours & benchmarks: 3 hours (v1.1.0 features)
Total: ~10 hours from idea to production-ready library with colours, benchmarks, and verified compatibility.
Practical Example: ML Serving Monitoring
Created examples/ml_serving.mojo demonstrating a realistic production use case:
// Monitor ML model prediction latencies
var latencies = collect_api_latencies() // Last 100 requests
var config = Config()
config.colors = ChartColors.fire() // Red/yellow for 'hot' data
print(plot(latencies, config))
// Output includes:
// - Latency trends with spikes visible
// - Statistics: Mean=25.3ms | P95=63.7ms | Max=101.9ms
// - Actionable insights: "High latency detected"
Perfect for SSH sessions, CI/CD logs, or quick health checks without external dashboards.
Lessons for Porting Python to Mojo
- Expect 2-3x code size - Missing stdlib features require manual implementation
- String formatting is the biggest gap - Budget significant time for this
- Python interop is invaluable - Use it for validation tests
- Visual testing matters - Automated tests missed spacing issues that only eyeballing caught
- Banker's rounding matters - IEEE 754 compliance is not optional for exact compatibility
- Performance is a bonus, not the goal - The library is fast enough; focus on correctness and capability first
What This Series Taught Me
Across three projects (dotenv, toml, asciichart), I learned:
Progressive Complexity
- dotenv: Line-based parsing, string handling basics
- toml: Recursive parsing, lexer design, complex state management
- asciichart: Performance optimisation, visual output, benchmark-driven development
Common Patterns
- Python interop testing - Gold standard for validation
- Explicit typing - Verbose but valuable for correctness
- Missing stdlib features - String formatting is the biggest gap across all three projects
- Code organisation - Tests should scale with implementation
Performance Reality
The library is fast enough for all practical uses (microseconds for typical charts). While Mojo offers performance advantages, the real value is in having native ASCII charting capability without external dependencies.
The String Formatting Reality Check
Across all three projects, string formatting was the single biggest source of verbosity:
- dotenv: Minimal impact (simple concatenation only)
- toml: Moderate impact (error messages with positions)
- asciichart: Major impact (~60 lines for generic float formatter)
The bottom line: Once Mojo adds format strings to its stdlib, Python→Mojo ports will become significantly easier. The algorithm porting is straightforward—it's the string handling that adds lines.
Conclusion: Is Mojo Ready?
Yes, for libraries where capability matters.
The 2.5x code size is acceptable when you achieve pixel-perfect Python compatibility and zero external dependencies. Mojo is ready for production library work today, not someday.
Would I do it again? Absolutely. The hardest part wasn't the algorithm—it was reimplementing Python's string formatting. Once that's in Mojo's stdlib, porting will be dramatically easier.
For businesses evaluating Mojo: These three libraries (configuration, parsing, visualisation) reduce the barrier to adoption. Focus your migration effort on your actual business logic, not on rebuilding infrastructure.
What's Next
This concludes my Month of Mojo intensive. Three projects, learned the language from scratch, contributed to the ecosystem. Now it's time for some well-earned Australian summer beach time! 🏖️🇦🇺☀️🌊
The repos will continue to be maintained, and I'll be back with fresh energy and ideas.
Try it yourself:
git clone https://github.com/databooth/mojo-asciichart
cd mojo-asciichart
pixi install
pixi run example-ml-serving # See realistic monitoring example
pixi run bench-python-comparison # Verify performance claims
Project Links:
Licence: Apache 2.0
Building high-performance data and AI services with Mojo at DataBooth. Questions or want to collaborate? Get in touch.