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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
- **🏷️ Timeline Annotations**: Pin events, deploys, and alerts onto the sliding timeline with custom glyphs and vertical guidelines.
- **📦 Live Legend Box**: Bordered legend overlay displaying swatches, names, and real-time values.
- **⚖️ Bidirectional & RX/TX Traffic**: Grafana-style zero baseline rulings, inverted mirror series, auto-symmetric centering, and adaptive byte rate formatters.
- **⌇ Gaps for Missing Points**: Push `math.NaN()` for a missing sample. `NaN` and `±Inf` draw as gaps, with no line, fill, or connecting stroke.
- **└┴ Optional Time Axis**: `WithXAxis` adds an axis row and a label row under the plot, with labels from your own function of the point age.
- **🫧 Charm Native**: Fully customizable with Lip Gloss styles and built for Bubble Tea event loops.

## 🚀 Quick Start
Expand Down
Binary file modified assets/braille.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/events.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/fill.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/hero.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/quickstart.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/rxtx.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/styled.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/xaxis.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/zerocross.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
51 changes: 50 additions & 1 deletion cmd/gen-shots/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"math"
"math/rand"
"time"

"charm.land/lipgloss/v2"
"github.com/ingvarch/pulse"
Expand All @@ -13,7 +14,7 @@ import (
)

func main() {
mode := flag.String("mode", "hero", "hero | quickstart | styled | braille | rxtx | events | fill | zerocross")
mode := flag.String("mode", "hero", "hero | quickstart | styled | braille | rxtx | events | fill | zerocross | xaxis")
flag.Parse()

switch *mode {
Expand All @@ -33,6 +34,8 @@ func main() {
renderFill()
case "zerocross":
renderZeroCross()
case "xaxis":
renderXAxis()
}
}

Expand Down Expand Up @@ -340,3 +343,49 @@ func renderZeroCross() {

fmt.Println(box)
}

func renderXAxis() {
preset := theme.TokyoNight()
chartW, chartH := 80, 13
newest := time.Date(2026, 10, 15, 0, 0, 0, 0, time.UTC)
step := 7 * 24 * time.Hour / time.Duration(chartW)

lc := pulse.New(chartW, chartH,
pulse.WithRange(0, 100),
pulse.WithTicks(0, 25, 50, 75, 100),
pulse.WithLineWidth(1),
pulse.WithFill(true),
pulse.WithTintedFill(true),
pulse.WithGrid(true),
pulse.WithAxisStyle(preset.Axis),
pulse.WithLineStyle(lipgloss.NewStyle().Foreground(lipgloss.Color("#7aa2f7"))),
pulse.WithXAxis(func(age int) string {
return newest.Add(-time.Duration(age) * step).Format("Jan 2")
}),
)

rng := rand.New(rand.NewSource(7))
for i := 0; i < chartW; i++ {
if i >= 52 && i < 58 {
lc.Push(math.NaN())
continue
}
v := 38 + 24*math.Sin(float64(i)*0.2) + 8*math.Cos(float64(i)*0.07) + (rng.Float64()-0.5)*6
lc.Push(math.Max(0, math.Min(100, v)))
}

header := lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color("#c0caf5")).
Render("CPU, LAST 7 DAYS (GAP = NO DATA)")

content := fmt.Sprintf("%s\n\n%s", header, lc.View())

box := lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(lipgloss.Color("#3b4261")).
Padding(1, 2).
Render(content)

fmt.Println(box)
}
32 changes: 32 additions & 0 deletions docs/axes-and-grid.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,38 @@ chart := pulse.New(60, 12,
<img src="../assets/fill.png" alt="Grafana-Style Coordinate Grid" width="100%" />
</p>

## Time Axis (`WithXAxis`)

`pulse.WithXAxis(label)` draws an axis row and a label row under the plot. `label(age)` returns the text for the point `age` steps before the newest one, so age 0 is the newest point. The function runs on every `View()` and gets only the age. To keep the labels with the data, compute them from the time of the newest point and update that time on each push.

```go
var newest time.Time // time of the newest point
step := 7 * 24 * time.Hour / 60 // 60 points cover 7 days

chart := pulse.New(60, 10,
pulse.WithRange(0, 100),
pulse.WithXAxis(func(age int) string {
return newest.Add(-time.Duration(age) * step).Format("Jan 2")
}),
)

// For each sample:
newest = sample.Time
chart.Push(sample.Value)
```

Labels sit at the left edge, the right edge, and at the quarter columns (`w / 4`, `2w / 4`, `3w / 4`), the same columns as the vertical grid. The left label starts at column 0, the right label ends at the last column, and the middle labels are centered on their column with a `┴` mark on the axis row.

- An empty string means no label and no mark at that column.
- A label that does not fit inside the plot width is dropped.
- Labels keep at least one space between them. The right edge is placed first, then the left edge, then the quarters. A label that would touch one already placed is dropped, so narrow charts show only the edge labels.

`Height()` stays the plot height. With the axis on, `View()` returns `Height()+2` lines: the plot rows, the axis row, and the label row. Leave room for the two extra lines when you lay the chart out. The axis rows do not depend on the grid, the render mode, or events. `chart.SetXAxis(nil)` turns the axis off.

<p align="center">
<img src="../assets/xaxis.png" alt="Time axis under a 7-day CPU chart with a gap" width="100%" />
</p>

## Symmetric Y-Axis (`WithSymmetric`)

When plotting bidirectional metrics (such as network RX/TX, disk read/write, or profit/loss), you often need zero to stay directly in the vertical center of the chart with equal positive and negative scaling.
Expand Down
13 changes: 13 additions & 0 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,18 @@ totalPoints := chart.Len() // Default series
cpuPoints := chart.LenSeries("cpu") // Named series
```

## Missing Points

Push `math.NaN()` when a sample is missing. `NaN`, `+Inf` and `-Inf` are drawn as gaps: the series draws nothing in that column, and its line stops before the gap and starts again after it. Grid lines and event markers still show in a gap.

```go
chart.Push(42)
chart.Push(math.NaN()) // gap
chart.Push(45)
```

`Last(name)` skips trailing gaps and returns the last finite value. It returns `(0, false)` if the series has none, and `LegendBox()` then shows no value for it. `SeriesData(name)` returns the raw values, including `NaN` and `Inf`.

## Multi-Series Plotting

You can plot multiple concurrent time-series on the same coordinate space:
Expand Down Expand Up @@ -99,6 +111,7 @@ if val, ok := chart.Last("cpu"); ok {
All styling and rendering options can be modified dynamically at runtime:
```go
chart.SetGrid(true) // toggle grid
chart.SetXAxis(label) // time labels under the plot (View adds 2 lines)
chart.SetFill(false) // toggle shaded area fill
chart.SetLineWidth(2) // switch between thin (1) and bold (2)
chart.SetMode(pulse.ModeBraille)// switch between ModeLines and ModeBraille
Expand Down
Loading
Loading