{"id":50567922,"url":"https://github.com/nimblemarkets/ntcharts-svg","last_synced_at":"2026-06-04T16:01:23.242Z","repository":{"id":360048243,"uuid":"1241443243","full_name":"NimbleMarkets/ntcharts-svg","owner":"NimbleMarkets","description":"SVG rendering and immediate-mode for ntcharts","archived":false,"fork":false,"pushed_at":"2026-05-24T18:12:16.000Z","size":80,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-24T20:15:38.202Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/NimbleMarkets.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-17T11:55:06.000Z","updated_at":"2026-05-24T18:12:19.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/NimbleMarkets/ntcharts-svg","commit_stats":null,"previous_names":["nimblemarkets/ntcharts-svg"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/NimbleMarkets/ntcharts-svg","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NimbleMarkets%2Fntcharts-svg","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NimbleMarkets%2Fntcharts-svg/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NimbleMarkets%2Fntcharts-svg/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NimbleMarkets%2Fntcharts-svg/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/NimbleMarkets","download_url":"https://codeload.github.com/NimbleMarkets/ntcharts-svg/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NimbleMarkets%2Fntcharts-svg/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33912343,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-04T02:00:06.755Z","response_time":64,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-04T16:01:20.877Z","updated_at":"2026-06-04T16:01:23.207Z","avatar_url":"https://github.com/NimbleMarkets.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ntcharts-svg — Terminal SVG viewer \u0026 vector canvas for Bubble Tea\n\n\u003cp\u003e\n    \u003ca href=\"https://github.com/NimbleMarkets/ntcharts-svg/tags\"\u003e\u003cimg src=\"https://img.shields.io/github/tag/NimbleMarkets/ntcharts-svg.svg\" alt=\"Latest Release\"\u003e\u003c/a\u003e\n    \u003ca href=\"https://pkg.go.dev/github.com/NimbleMarkets/ntcharts-svg?tab=doc\"\u003e\u003cimg src=\"https://godoc.org/github.com/golang/gddo?status.svg\" alt=\"GoDoc\"\u003e\u003c/a\u003e\n    \u003ca href=\"https://github.com/NimbleMarkets/ntcharts-svg/blob/main/CODE_OF_CONDUCT.md\"\u003e\u003cimg src=\"https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg\" alt=\"Code Of Conduct\"\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n`ntcharts-svg` is a [Bubble Tea](https://github.com/charmbracelet/bubbletea) widget that makes SVG documents first-class citizens in terminal UIs (and WASM/browser builds). It pairs [`oksvg`](https://github.com/srwiley/oksvg) + [`rasterx`](https://github.com/srwiley/rasterx) for **pure-Go** SVG rasterization with [`ntcharts/v2/picture`](https://github.com/NimbleMarkets/ntcharts) for image rendering — half-block glyphs anywhere, full-resolution Kitty graphics on terminals that support them (Kitty, Ghostty, WezTerm).\n\n[**Try out the live WASM demo.**](https://nimblemarkets.github.io/ntcharts-svg)\n\nIt is the sibling of [`ntcharts-pdf`](https://github.com/NimbleMarkets/ntcharts-pdf) and mirrors its architecture. The key difference: because the SVG rasterizer is pure Go, image rendering needs **no CGO, no system dependencies, and no JS bridge** — it works identically on native, browser-WASM, and WASI builds.\n\n\nThe widget does double duty:\n\n- **View existing SVGs** — load any `.svg` file or in-memory bytes, rasterize, zoom, and pan.\n- **Generate vector graphics** — draw with an immediate-mode `Canvas` and chart helpers, export clean SVG (or PNG), and display the result in the same viewer.\n\n## Quickstart\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"os\"\n\n    tea \"charm.land/bubbletea/v2\"\n    \"github.com/NimbleMarkets/ntcharts-svg/svg\"\n)\n\ntype model struct{ sv svg.Model }\n\nfunc (m model) Init() tea.Cmd { return m.sv.Init() }\n\nfunc (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {\n    if k, ok := msg.(tea.KeyMsg); ok \u0026\u0026 (k.String() == \"q\" || k.String() == \"ctrl+c\") {\n        return m, tea.Quit\n    }\n    if sz, ok := msg.(tea.WindowSizeMsg); ok {\n        return m, m.sv.SetSize(sz.Width, sz.Height)\n    }\n    var cmd tea.Cmd\n    m.sv, cmd = m.sv.Update(msg)\n    return m, cmd\n}\n\nfunc (m model) View() tea.View { return m.sv.View() }\n\nfunc main() {\n    sv := svg.NewWithConfig(svg.Config{InitialPath: os.Args[1]})\n    if _, err := tea.NewProgram(model{sv: sv}).Run(); err != nil {\n        fmt.Println(err); os.Exit(1)\n    }\n}\n```\n\nToggle Raster ↔ Info with `m`, swap Glyph ↔ Kitty with `g`, zoom with `+` / `-`, pan with arrows, cycle fit with `f`, reload with `r`, reset view with `0`. The widget's `Update` dispatches all of these through `DefaultKeyMap()`; rebind a field to change the keystroke (or set a binding to `key.Binding{}` to disable it).\n\n## Demo\n\nA fuller demo lives at [`examples/svgview`](./examples/svgview/main.go) — adds a status bar, help bubble, and the `c` / `e` keys that generate and export a chart at runtime.\n\n```sh\ntask build-ex-svgview\n./bin/ntcharts-svgview path/to/your.svg   # or no argument for the embedded sample\n```\n\nThe same example compiles to WebAssembly and runs in the browser via [`booba`](https://github.com/NimbleMarkets/go-booba) — no JS bridge required, since the rasterizer is pure Go. Build and serve it locally:\n\n```sh\ntask serve-wasm-site   # builds web/app.wasm, stages booba assets, serves at :8000\n```\n\n`task build-wasm-site` alone produces the deployable `web/` directory; the `.github/workflows/pages.yml` workflow publishes it to GitHub Pages on every push to `main`.\n\n## Modes\n\n| Mode | What it does | Where it works |\n|---|---|---|\n| `svg.RasterMode` (default) | Rasterizes the SVG via a `Renderer` (default: `oksvg`/`rasterx`), fed to `ntcharts/v2/picture`. Supports zoom (`+`/`-` up to ×64) and pan (arrow keys when zoomed) | Everywhere — native, browser-WASM, WASI |\n| `svg.InfoMode` | Zero-dependency textual summary: view box, declared size, element-type histogram, embedded `\u003ctitle\u003e` / `\u003cdesc\u003e` | Everywhere |\n\n`sv.ToggleMode()` returns a `tea.Cmd` that swaps modes and re-renders. RasterMode internally uses `picture.PictureGlyph` (universal half-blocks) or `picture.PictureKitty` (high-resolution); switch between them with `sv.ToggleRenderMode()`.\n\nUnlike `ntcharts-pdf`, RasterMode is the default — the rasterizer is pure Go and always available, so there is no platform where image rendering silently degrades.\n\n## Renderer\n\n```go\ntype Renderer interface {\n    Render(maxW, maxH int) (image.Image, error)\n    ViewBox() (w, h float64)\n    Close() error\n}\n\ntype RendererFactory func(name string, data []byte) (Renderer, error)\n```\n\nThe factory is **bytes-first**: the widget reads file contents once (subject to `Limits.MaxFileBytes`) and hands the bytes to the factory, so custom factories never have to implement their own file I/O. Hosts that already have the bytes — an `//go:embed` blob, an HTTP response, a generated `Canvas` — skip the path round-trip via `sv.SetSVGData(name, data)` or `Config.InitialData` / `InitialName`.\n\nWhen `Config.RendererFactory` is nil, `NewWithConfig` installs `DefaultRendererFactory()`, backed by `oksvg`/`rasterx`. Bring-your-own renderers (resvg via CGO, a server-side rasterizer, a caching layer) plug in through the same interface. `Config.RenderEdge` (default 1024) sets the longer-edge resolution of the rasterized bitmap; zoom then crops into that bitmap.\n\n### Loading APIs\n\n| Call | When to use |\n|---|---|\n| `sv.SetSVG(path)` / `Config.InitialPath` | SVG is on disk; widget reads it once (`Limits.MaxFileBytes` enforced via `os.Stat`) |\n| `sv.SetSVGData(name, data)` / `Config.InitialData` + `InitialName` | SVG bytes already in memory (embedded, fetched, generated) |\n| `sv.ShowCanvas(c)` | Display a `*svg.Canvas` you drew with the immediate-mode API |\n| `sv.SetImage(img)` | Install a pre-rasterized `image.Image`, bypassing the `Renderer` entirely |\n\n### Renderer state introspection\n\n- `sv.HasRenderer() bool` — true when the factory produced a Renderer for the current document.\n- `sv.RendererErr() error` — the most recent factory error, persisted for the document's lifetime (cleared on the next load). InfoMode still works; surface this to explain why RasterMode is unavailable.\n\n## Immediate-mode Canvas\n\n`Canvas` is a self-contained, pure-Go SVG writer — draw with chainable primitives, then export clean SVG, a PNG, or an `image.Image`.\n\n```go\nc := svg.NewCanvas(200, 120).Background(\"white\")\nc.Circle(60, 60, 40, svg.Fill(\"#4e79a7\"))\nc.Line(0, 0, 200, 120, svg.Stroke(\"#e15759\", 3))\nc.Text(100, 110, \"hello\", svg.Paint{}.WithFont(\"sans-serif\", 12).WithAnchor(\"middle\"))\n\nsvgText := c.ToSVG()              // standalone SVG markup\n_ = c.WriteSVG(\"out.svg\")          // write a .svg file\n_ = c.WritePNG(\"out.png\", 800, 600) // rasterize to PNG\nimg, _ := c.ToImage(800, 600)      // rasterize to image.Image\n```\n\nPrimitives: `Line`, `Rect`, `RoundRect`, `Circle`, `Ellipse`, `Polyline`, `Polygon`, `Path`, `Text`, and `Raw` for an escape hatch. `Paint` carries fill / stroke / opacity / dash / font, with fluent `With*` builders and the `Fill` / `Stroke` constructors.\n\n### Chart helpers\n\n`Chart` builds bar and line charts on top of the `Canvas`:\n\n```go\ncanvas := svg.NewChart(360, 240).\n    SetTitle(\"Quarterly Revenue\").\n    SetLabels(\"Q1\", \"Q2\", \"Q3\", \"Q4\").\n    AddSeries(\"2025\", \"\", 12, 19, 14, 23).\n    AddSeries(\"2026\", \"\", 17, 15, 21, 28).\n    BarCanvas() // or LineCanvas()\n\ncmd := mySvgModel.ShowCanvas(canvas) // render it straight into the viewer\n```\n\nThis is the \"generate chart → SVG → display\" loop the `c` key in the example demonstrates.\n\n## Resource limits (`Config.Limits`)\n\nSVG is an untrusted-input format; a hostile document can nest elements millions deep, inline a gigabyte of base64 image data, or declare a viewBox that projects to a terabyte bitmap. `Config.Limits` exposes conservative caps applied automatically:\n\n| Field | Default | What it caps |\n|---|---|---|\n| `MaxFileBytes` | 64 MiB | SVG size before the loader reads it |\n| `MaxElements` | 250 000 | Element-histogram counting; the scan stops and reports \"capped\" |\n| `MaxRenderPixels` | 100 000 000 | Rasterized bitmap area; the renderer scales down to fit |\n| `MaxRenderEdge` | 8192 | Either edge of the rasterized bitmap |\n\nSet any field to `-1` to disable that cap for trusted input; zero uses the default. SVG parsing and rasterization are both panic-guarded — a malformed document surfaces an error rather than crashing the load goroutine.\n\n### Sanitizing SVG-derived strings\n\nSVG titles, descriptions, element names, and error messages can carry C0/C1 control sequences and Unicode bidi format chars (\"Trojan Source\"-style attacks). The widget's own status text is already sanitized. For host-displayed strings derived from the SVG — `Model.Title()`, `Model.Name()` for in-memory docs, `RendererErr().Error()` — use `svg.SanitizeForTerminal(s string) string`, which drops anything outside Unicode's printable categories and folds newlines / tabs to single spaces.\n\n## Known caveats\n\n- **Zoom is vector-sharp.** With a document `Renderer` attached, `ZoomIn` / `PanLeft` (etc.) re-rasterize the viewport sub-rectangle via `Renderer.RenderRegion`, so zoomed-in views stay crisp at any depth rather than upscaling a fixed bitmap. Zoom 0 reuses the full-document bitmap (no re-render); each zoom/pan keypress at depth costs one rasterize. A host bitmap installed via `SetImage` has no renderer, so it still crops (chunky-pixel inspection) — there are no vectors to re-rasterize.\n- **`oksvg` supports a broad but not exhaustive slice of SVG.** Filters, advanced gradients, embedded fonts, and CSS-heavy documents may render approximately or skip elements (the parser runs in ignore-errors mode). For pixel-perfect fidelity, wire a custom `RendererFactory` around a stricter rasterizer.\n- **The immediate-mode `Canvas` is a writer, not a layout engine.** It emits exactly the elements you draw. Text is not measured — `text-anchor` positions runs, but you supply the coordinates. This keeps `Canvas` dependency-free and WASM-safe.\n- **`WriteSVG` / `WritePNG` do filesystem I/O** and will fail at runtime under `GOOS=js`. Use `ToSVG` / `Bytes` / `ToImage` in the browser.\n- **Kitty placement composition through lipgloss** is best-effort. `picture.Model.View()` embeds the Kitty placement escapes directly in the rendered string, which composes cleanly inside `lipgloss.JoinVertical` and basic borders but may misbehave under styles that rewrite the content. Glyph mode has no such caveat.\n- **Vibe coded.** This comment was inserted by a human.\n\n## License\n\nThanks to the [`oksvg`](https://github.com/srwiley/oksvg) and [`rasterx`](https://github.com/srwiley/rasterx) libraries and their authors.  We are currently using [a fork]](https://github.com/NimbleMarkets/oksvg/tree/nm) of their `oksvg` library.\n\n[MIT License](./LICENSE.txt) — Copyright (c) 2026 [Neomantra Corp](https://www.neomantra.com).\n\n----\nMade with :heart: and :fire: by the team behind [Nimble.Markets](https://nimble.markets).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnimblemarkets%2Fntcharts-svg","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnimblemarkets%2Fntcharts-svg","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnimblemarkets%2Fntcharts-svg/lists"}