Skip to content

Commit

Permalink
Add sparkline widget. (#2631)
Browse files Browse the repository at this point in the history
* Sparkline widget proof of concept.

* Address review comment.

Related comments: https://github.com/Textualize/textual/pull/2631\#discussion_r1202894414

* Blend background colours.

* Add widget sparkline.

* Add snapshot tests.

* Add documentation.

* Update roadmap.

* Address review feedback.

Relevant comments: https://github.com/Textualize/textual/pull/2631\#discussion_r1210394532, https://github.com/Textualize/textual/pull/2631\#discussion_r1210442013

* Improve docs.

Relevant comments: https://github.com/Textualize/textual/pull/2631\#issuecomment-1568529074

* Update snapshot app titles.

* Don't init summary function with None

Related comments: https://github.com/Textualize/textual/pull/2631\#discussion_r1211666076

* Apply suggestions from code review

Co-authored-by: Dave Pearson <[email protected]>

* Improve wording.

* Improve wording.

* Simplify example.

---------

Co-authored-by: Dave Pearson <[email protected]>
  • Loading branch information
rodrigogiraoserrao and davep authored Jun 1, 2023
1 parent 7049014 commit 78db024
Show file tree
Hide file tree
Showing 16 changed files with 918 additions and 3 deletions.
4 changes: 4 additions & 0 deletions docs/examples/widgets/sparkline.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Sparkline {
width: 100%;
margin: 2;
}
22 changes: 22 additions & 0 deletions docs/examples/widgets/sparkline.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import random
from statistics import mean

from textual.app import App, ComposeResult
from textual.widgets._sparkline import Sparkline

random.seed(73)
data = [random.expovariate(1 / 3) for _ in range(1000)]


class SparklineSummaryFunctionApp(App[None]):
CSS_PATH = "sparkline.css"

def compose(self) -> ComposeResult:
yield Sparkline(data, summary_function=max) # (1)!
yield Sparkline(data, summary_function=mean) # (2)!
yield Sparkline(data, summary_function=min) # (3)!


app = SparklineSummaryFunctionApp()
if __name__ == "__main__":
app.run()
8 changes: 8 additions & 0 deletions docs/examples/widgets/sparkline_basic.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Screen {
align: center middle;
}

Sparkline {
width: 3; /* (1)! */
margin: 2;
}
19 changes: 19 additions & 0 deletions docs/examples/widgets/sparkline_basic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from textual.app import App, ComposeResult
from textual.widgets._sparkline import Sparkline

data = [1, 2, 2, 1, 1, 4, 3, 1, 1, 8, 8, 2] # (1)!


class SparklineBasicApp(App[None]):
CSS_PATH = "sparkline_basic.css"

def compose(self) -> ComposeResult:
yield Sparkline( # (2)!
data, # (3)!
summary_function=max, # (4)!
)


app = SparklineBasicApp()
if __name__ == "__main__":
app.run()
74 changes: 74 additions & 0 deletions docs/examples/widgets/sparkline_colors.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
Sparkline {
width: 100%;
margin: 1;
}

#fst > .sparkline--max-color {
color: $success;
}
#fst > .sparkline--min-color {
color: $warning;
}

#snd > .sparkline--max-color {
color: $warning;
}
#snd > .sparkline--min-color {
color: $success;
}

#trd > .sparkline--max-color {
color: $error;
}
#trd > .sparkline--min-color {
color: $warning;
}

#frt > .sparkline--max-color {
color: $warning;
}
#frt > .sparkline--min-color {
color: $error;
}

#fft > .sparkline--max-color {
color: $accent;
}
#fft > .sparkline--min-color {
color: $accent 30%;
}

#sxt > .sparkline--max-color {
color: $accent 30%;
}
#sxt > .sparkline--min-color {
color: $accent;
}

#svt > .sparkline--max-color {
color: $error;
}
#svt > .sparkline--min-color {
color: $error 30%;
}

#egt > .sparkline--max-color {
color: $error 30%;
}
#egt > .sparkline--min-color {
color: $error;
}

#nnt > .sparkline--max-color {
color: $success;
}
#nnt > .sparkline--min-color {
color: $success 30%;
}

#tnt > .sparkline--max-color {
color: $success 30%;
}
#tnt > .sparkline--min-color {
color: $success;
}
24 changes: 24 additions & 0 deletions docs/examples/widgets/sparkline_colors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from textual.app import App, ComposeResult
from textual.widgets._sparkline import Sparkline


class SparklineColorsApp(App[None]):
CSS_PATH = "sparkline_colors.css"

def compose(self) -> ComposeResult:
nums = [10, 2, 30, 60, 45, 20, 7, 8, 9, 10, 50, 13, 10, 6, 5, 4, 3, 7, 20]
yield Sparkline(nums, summary_function=max, id="fst")
yield Sparkline(nums, summary_function=max, id="snd")
yield Sparkline(nums, summary_function=max, id="trd")
yield Sparkline(nums, summary_function=max, id="frt")
yield Sparkline(nums, summary_function=max, id="fft")
yield Sparkline(nums, summary_function=max, id="sxt")
yield Sparkline(nums, summary_function=max, id="svt")
yield Sparkline(nums, summary_function=max, id="egt")
yield Sparkline(nums, summary_function=max, id="nnt")
yield Sparkline(nums, summary_function=max, id="tnt")


app = SparklineColorsApp()
if __name__ == "__main__":
app.run()
4 changes: 2 additions & 2 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ Widgets are key to making user-friendly interfaces. The builtin widgets should c
* [ ] Braille
* [ ] Sixels, and other image extensions
- [x] Input
* [ ] Validation
* [x] Validation
* [ ] Error / warning states
* [ ] Template types: IP address, physical units (weight, volume), currency, credit card etc
- [X] Select control (pull-down)
Expand All @@ -72,7 +72,7 @@ Widgets are key to making user-friendly interfaces. The builtin widgets should c
- [X] Progress bars
* [ ] Style variants (solid, thin etc)
- [X] Radio boxes
- [ ] Spark-lines
- [X] Spark-lines
- [X] Switch
- [X] Tabs
- [ ] TextArea (multi-line input)
Expand Down
9 changes: 9 additions & 0 deletions docs/widget_gallery.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,15 @@ Select multiple values from a list of options.
```{.textual path="docs/examples/widgets/selection_list_selections.py" press="down,down,down"}
```

## Sparkline

Display numerical data.

[Sparkline reference](./widgets/sparkline.md){ .md-button .md-button--primary }

```{.textual path="docs/examples/widgets/sparkline.py" lines="11"}
```

## Static

Displays simple static content. Typically used as a base class.
Expand Down
112 changes: 112 additions & 0 deletions docs/widgets/sparkline.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Sparkline

!!! tip "Added in version 0.27.0"

A widget that is used to visually represent numerical data.

- [ ] Focusable
- [ ] Container

## Examples

### Basic example

The example below illustrates the relationship between the data, its length, the width of the sparkline, and the number of bars displayed.

!!! tip

The sparkline data is split into equally-sized chunks.
Each chunk is represented by a bar and the width of the sparkline dictates how many bars there are.

=== "Output"

```{.textual path="docs/examples/widgets/sparkline_basic.py" lines="5" columns="30"}
```

=== "sparkline_basic.py"

```python hl_lines="4 11 12 13"
--8<-- "docs/examples/widgets/sparkline_basic.py"
```

1. We have 12 data points.
2. This sparkline will have its width set to 3 via CSS.
3. The data (12 numbers) will be split across 3 bars, so 4 data points are associated with each bar.
4. Each bar will represent its largest value.
The largest value of each chunk is 2, 4, and 8, respectively.
That explains why the first bar is half the height of the second and the second bar is half the height of the third.

=== "sparkline_basic.css"

```sass
--8<-- "docs/examples/widgets/sparkline_basic.css"
```

1. By setting the width to 3 we get three buckets.

### Different summary functions

The example below shows a sparkline widget with different summary functions.
The summary function is what determines the height of each bar.

=== "Output"

```{.textual path="docs/examples/widgets/sparkline.py" lines="11"}
```

=== "sparkline.py"

```python hl_lines="15-17"
--8<-- "docs/examples/widgets/sparkline.py"
```

1. Each bar will show the largest value of that bucket.
2. Each bar will show the mean value of that bucket.
3. Each bar will show the smaller value of that bucket.

=== "sparkline.css"

```sass
--8<-- "docs/examples/widgets/sparkline.css"
```

### Changing the colors

The example below shows how to use component classes to change the colors of the sparkline.

=== "Output"

```{.textual path="docs/examples/widgets/sparkline_colors.py" lines=22}
```

=== "sparkline_colors.py"

```python
--8<-- "docs/examples/widgets/sparkline_colors.py"
```

=== "sparkline_colors.css"

```sass
--8<-- "docs/examples/widgets/sparkline_colors.css"
```


## Reactive Attributes

| Name | Type | Default | Description |
| --------- | ----- | ----------- | -------------------------------------------------- |
| `data` | `Sequence[float] | None` | `None` | The data represented by the sparkline. |
| `summary_function` | `Callable[[Sequence[float]], float]` | `max` | The function that computes the height of each bar. |


## Messages

This widget sends no messages.

---


::: textual.widgets.Sparkline
options:
heading_level: 2
1 change: 1 addition & 0 deletions mkdocs-nav.yml
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ nav:
- "widgets/radioset.md"
- "widgets/select.md"
- "widgets/selection_list.md"
- "widgets/sparkline.md"
- "widgets/static.md"
- "widgets/switch.md"
- "widgets/tabbed_content.md"
Expand Down
4 changes: 3 additions & 1 deletion src/textual/renderables/sparkline.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ def last(l: Sequence[T]) -> T:
console.print(f"data = {nums}\n")
for f in funcs:
console.print(
f"{f.__name__}:\t", Sparkline(nums, width=12, summary_function=f), end=""
f"{f.__name__}:\t",
Sparkline(nums, width=12, summary_function=f),
end="",
)
console.print("\n")
2 changes: 2 additions & 0 deletions src/textual/widgets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from ._radio_set import RadioSet
from ._select import Select
from ._selection_list import SelectionList
from ._sparkline import Sparkline
from ._static import Static
from ._switch import Switch
from ._tabbed_content import TabbedContent, TabPane
Expand Down Expand Up @@ -64,6 +65,7 @@
"RadioSet",
"Select",
"SelectionList",
"Sparkline",
"Static",
"Switch",
"Tab",
Expand Down
1 change: 1 addition & 0 deletions src/textual/widgets/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ from ._radio_button import RadioButton as RadioButton
from ._radio_set import RadioSet as RadioSet
from ._select import Select as Select
from ._selection_list import SelectionList as SelectionList
from ._sparkline import Sparkline as Sparkline
from ._static import Static as Static
from ._switch import Switch as Switch
from ._tabbed_content import TabbedContent as TabbedContent
Expand Down
Loading

0 comments on commit 78db024

Please sign in to comment.