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
3 changes: 2 additions & 1 deletion demos/Termina.Demo.Gallery/Pages/GalleryMenuViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ public partial class GalleryMenuViewModel : ReactiveViewModel
new("Layouts", "Vertical, horizontal, grid, panels, borders", "/layouts"),
new("Animations", "Spinners, streaming text, progress indicators", "/animations"),
new("File Picker", "File/folder selection, directory navigation, fuzzy filtering", "/filepicker"),
new("Toast Notifications", "Colors, icons, positions, and presets", "/toasts")
new("Toast Notifications", "Colors, icons, positions, and presets", "/toasts"),
new("Graphs & Progress", "Live graphs, gradient colors, progress bars", "/graphs")
};

public void NavigateToGallery(string route)
Expand Down
138 changes: 138 additions & 0 deletions demos/Termina.Demo.Gallery/Pages/GraphGalleryPage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// Copyright (c) Petabridge, LLC. All rights reserved.
// Licensed under the Apache 2.0 license. See LICENSE file in the project root for full license information.

using R3;
using Termina.Components.Streaming;
using Termina.Extensions;
using Termina.Layout;
using Termina.Reactive;
using Termina.Rendering;
using Termina.Terminal;

namespace Termina.Demo.Gallery.Pages;

public sealed class GraphGalleryPage : ReactivePage<GraphGalleryViewModel>
{
private SelectionListNode<GraphStyleItem> _styleList = null!;
private readonly Random _random = new(42);

private GraphNode _demoGraph = null!;
private ProgressBarNode _progressBar = null!;
private double _progressValue;
private readonly List<double> _dataPoints = new();

public override void OnNavigatedTo()
{
base.OnNavigatedTo();

KeyBindings.Register(ConsoleKey.Escape, () => Navigate("/menu"));

KeyBindings.Register(ConsoleKey.Spacebar, () =>
{
var highlighted = _styleList.HighlightedItem;
if (highlighted != null)
ViewModel.SelectedStyle.Value = highlighted.Value.Style;
});

_styleList.SelectionConfirmed
.Subscribe(items =>
{
var item = items.FirstOrDefault();
if (item != null)
ViewModel.SelectedStyle.Value = item.Style;
})
.DisposeWith(Subscriptions);

ViewModel.SelectedStyle
.Subscribe(style => _demoGraph.WithStyle(style))
.DisposeWith(Subscriptions);

Observable.Interval(TimeSpan.FromMilliseconds(200), TimeProvider.System)
.Subscribe(_ => PushData())
.DisposeWith(Subscriptions);

Focus.PushFocus(_styleList);
}

private void PushData()
{
_dataPoints.Add(50 + _random.NextDouble() * 50 * Math.Sin(_dataPoints.Count * 0.15) + _random.NextDouble() * 20);
if (_dataPoints.Count > 120)
_dataPoints.RemoveAt(0);
_demoGraph.SetData(_dataPoints.ToArray());

_progressValue = (_progressValue + 0.008) % 1.01;
_progressBar.WithValue(_progressValue);
}

public override ILayoutNode BuildLayout()
{
_styleList = new SelectionListNode<GraphStyleItem>(
ViewModel.GraphStyles,
item => new SelectionItemContent()
.AddLine(
new StaticTextSegment(item.Name, Color.BrightCyan, decoration: TextDecoration.Bold),
new StaticTextSegment($" {item.Description}", Color.Gray)))
.WithMode(SelectionMode.Single)
.WithShowNumbers(true)
.WithHighlightColors(Color.Black, Color.Cyan)
.WithVisibleRows(6);

var gradient = Gradient.Create(Color.FromRgb(0, 100, 255), Color.FromRgb(0, 255, 100), Color.FromRgb(255, 255, 0));

_demoGraph = new GraphNode(intervalMs: 0)
.WithStyle(ViewModel.SelectedStyle.Value)
.WithGradient(gradient)
.WithRange(0, 100);

_progressBar = new ProgressBarNode()
.WithGradient(Gradient.Create(Color.FromRgb(255, 50, 50), Color.FromRgb(255, 200, 0), Color.FromRgb(50, 255, 50)))
.WithValue(0)
.WithLabel("{0:P0}");

var grid = new GridNode()
.WithColumns(SizeConstraint.Percentage(30), SizeConstraint.Percentage(70))
.WithRows(SizeConstraint.FillRemaining())
.WithGridLines(BorderStyle.Single)
.WithGridLineColor(Color.BrightBlack);

grid.SetCell(0, 0,
Layouts.Vertical()
.WithChild(
new TextNode("Graph Style")
.WithForeground(Color.BrightCyan)
.Bold()
.Height(2))
.WithChild(_styleList));

grid.SetCell(0, 1,
Layouts.Vertical()
.WithChild(
new TextNode("Live Graph")
.WithForeground(Color.BrightYellow)
.Bold()
.Height(2))
.WithChild(_demoGraph.Fill())
.WithChild(new EmptyNode().Height(1))
.WithChild(
new TextNode("Progress Bar with Gradient")
.WithForeground(Color.BrightYellow)
.Bold()
.Height(1))
.WithChild(_progressBar.Height(1)));

return Layouts.Vertical()
.WithChild(
new PanelNode()
.WithTitle("Graph & Progress Gallery")
.WithBorder(BorderStyle.Double)
.WithBorderColor(Color.BrightMagenta)
.WithContent(grid)
.Fill())
.WithChild(
new TextNode("[↑/↓] Navigate [Enter/Space] Switch Style [Esc] Menu")
.WithForeground(Color.BrightBlack)
.NoWrap()
.Height(1));
}
}
29 changes: 29 additions & 0 deletions demos/Termina.Demo.Gallery/Pages/GraphGalleryViewModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright (c) Petabridge, LLC. All rights reserved.
// Licensed under the Apache 2.0 license. See LICENSE file in the project root for full license information.

using R3;
using Termina.Layout;
using Termina.Reactive;

namespace Termina.Demo.Gallery.Pages;

public class GraphGalleryViewModel : ReactiveViewModel
{
public ReactiveProperty<GraphStyle> SelectedStyle { get; } = new(GraphStyle.Blocks);

public IReadOnlyList<GraphStyleItem> GraphStyles { get; } = new List<GraphStyleItem>
{
new("Blocks", GraphStyle.Blocks, "▁▂▃▄▅▆▇█ filled columns"),
new("Outline", GraphStyle.Outline, "Only the top edge is drawn"),
new("Braille", GraphStyle.Braille, "Double vertical resolution with braille dots"),
new("ASCII", GraphStyle.Ascii, "_ . - ~ ^ * # @ terminal fallback")
};

public override void Dispose()
{
SelectedStyle.Dispose();
base.Dispose();
}
}

public record GraphStyleItem(string Name, GraphStyle Style, string Description);
1 change: 1 addition & 0 deletions demos/Termina.Demo.Gallery/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
termina.RegisterRoute<AnimationsGalleryPage, AnimationsGalleryViewModel>("/animations", NavigationBehavior.PreserveState);
termina.RegisterRoute<FilePickerGalleryPage, FilePickerGalleryViewModel>("/filepicker", NavigationBehavior.PreserveState);
termina.RegisterRoute<ToastGalleryPage, ToastGalleryViewModel>("/toasts", NavigationBehavior.PreserveState);
termina.RegisterRoute<GraphGalleryPage, GraphGalleryViewModel>("/graphs", NavigationBehavior.PreserveState);
});

var host = builder.Build();
Expand Down
127 changes: 127 additions & 0 deletions docs/components/graph-node.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# GraphNode

A reactive, self-invalidating layout node that renders live scrolling graphs with optional gradient coloring. Implements `IAnimatedNode` so only the graph region repaints — not the parent layout.

## Basic Usage

```csharp
var graph = new GraphNode()
.WithColor(Color.Cyan)
.WithRange(0, 100);

graph.SetData([10, 40, 70, 100, 60, 30]);
```

## Graph Styles

Four rendering styles are available:

```csharp
new GraphNode().WithStyle(GraphStyle.Blocks) // ▁▂▃▄▅▆▇█ filled columns
new GraphNode().WithStyle(GraphStyle.Outline) // Only the top edge drawn
new GraphNode().WithStyle(GraphStyle.Braille) // Double vertical resolution
new GraphNode().WithStyle(GraphStyle.Ascii) // _ . - ~ ^ * # @ fallback
```

| Style | Characters | Best for |
|-------|-----------|----------|
| `Blocks` | `▁▂▃▄▅▆▇█` | General use, high contrast |
| `Outline` | Top edge only | Sparse data, sparkline feel |
| `Braille` | `⠀⢀⣀⣤⣶⣿` | Double vertical resolution per row |
| `Ascii` | `_ . - ~ ^ * # @` | Terminals without Unicode |

## Gradient Coloring

Apply a gradient that maps color by row height:

```csharp
var gradient = Gradient.Create(
Color.FromRgb(0, 100, 255), // blue at the bottom
Color.FromRgb(0, 255, 100), // green in the middle
Color.FromRgb(255, 255, 0)); // yellow at the top

var graph = new GraphNode()
.WithGradient(gradient)
.WithRange(0, 100);
```

For a single color, use the convenience method:

```csharp
new GraphNode().WithColor(Color.Green)
```

## Data-Driven Updates

`SetData` fires invalidation immediately so the graph repaints as soon as data arrives, independent of the internal timer interval:

```csharp
Observable.Interval(TimeSpan.FromMilliseconds(200), TimeProvider.System)
.Subscribe(_ =>
{
dataPoints.Add(GetNextValue());
graph.SetData(dataPoints.ToArray());
});
```

The graph renders the rightmost `width` data points (or `width * 2` for Braille style). Earlier data scrolls off the left edge.

## Internal Timer

The constructor accepts an interval for periodic self-invalidation. Set `intervalMs: 0` to disable the internal timer entirely and rely only on `SetData` for repaints:

```csharp
// Timer-driven refresh (default 500ms)
new GraphNode(intervalMs: 500)

// Data-driven only — no timer overhead
new GraphNode(intervalMs: 0)
```

## Testable Timing

Pass a `TimeProvider` for deterministic tests:

```csharp
var timeProvider = new FakeTimeProvider();
var graph = new GraphNode(intervalMs: 100, timeProvider: timeProvider);

graph.SetData([50, 100]);
timeProvider.Advance(TimeSpan.FromMilliseconds(100));
// graph has now self-invalidated
```

## API Reference

### Constructor

```csharp
public GraphNode(int intervalMs = 500, TimeProvider? timeProvider = null)
```

### Methods

| Method | Description |
|--------|-------------|
| `.WithStyle(GraphStyle)` | Set rendering style |
| `.WithGradient(Gradient)` | Apply gradient coloring by row |
| `.WithColor(Color)` | Single-color convenience |
| `.WithRange(double min, double max)` | Set data value range (default 0–100) |
| `.SetData(double[])` | Push data and trigger repaint |
| `.Start()` | Start internal timer |
| `.Stop()` | Stop internal timer |

### GraphStyle Enum

| Style | Description |
|-------|-------------|
| `Blocks` | Filled block columns (default) |
| `Outline` | Top edge only |
| `Braille` | Double resolution braille dots |
| `Ascii` | ASCII fallback characters |

## Source Code

::: details View GraphNode implementation
<<< @/../src/Termina/Layout/GraphNode.cs{csharp}
:::
2 changes: 2 additions & 0 deletions docs/components/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ Termina provides a set of built-in layout nodes (components) for building termin
| [TextNode](/components/text-node) | Renders styled text with word wrapping |
| [PanelNode](/components/panel-node) | Bordered container with title |
| [SpinnerNode](/components/spinner-node) | Animated loading indicator |
| [GraphNode](/components/graph-node) | Live scrolling graph with gradient coloring |
| [ProgressBarNode](/components/progress-bar-node) | Progress bar with gradient fill and label |
| [StreamingTextNode](/components/streaming-text-node) | Streaming text with scrolling |

## Input Components
Expand Down
Loading