Skip to content

Commit 78db024

Browse files
Add sparkline widget. (#2631)
* 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]>
1 parent 7049014 commit 78db024

File tree

16 files changed

+918
-3
lines changed

16 files changed

+918
-3
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Sparkline {
2+
width: 100%;
3+
margin: 2;
4+
}

docs/examples/widgets/sparkline.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import random
2+
from statistics import mean
3+
4+
from textual.app import App, ComposeResult
5+
from textual.widgets._sparkline import Sparkline
6+
7+
random.seed(73)
8+
data = [random.expovariate(1 / 3) for _ in range(1000)]
9+
10+
11+
class SparklineSummaryFunctionApp(App[None]):
12+
CSS_PATH = "sparkline.css"
13+
14+
def compose(self) -> ComposeResult:
15+
yield Sparkline(data, summary_function=max) # (1)!
16+
yield Sparkline(data, summary_function=mean) # (2)!
17+
yield Sparkline(data, summary_function=min) # (3)!
18+
19+
20+
app = SparklineSummaryFunctionApp()
21+
if __name__ == "__main__":
22+
app.run()
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Screen {
2+
align: center middle;
3+
}
4+
5+
Sparkline {
6+
width: 3; /* (1)! */
7+
margin: 2;
8+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from textual.app import App, ComposeResult
2+
from textual.widgets._sparkline import Sparkline
3+
4+
data = [1, 2, 2, 1, 1, 4, 3, 1, 1, 8, 8, 2] # (1)!
5+
6+
7+
class SparklineBasicApp(App[None]):
8+
CSS_PATH = "sparkline_basic.css"
9+
10+
def compose(self) -> ComposeResult:
11+
yield Sparkline( # (2)!
12+
data, # (3)!
13+
summary_function=max, # (4)!
14+
)
15+
16+
17+
app = SparklineBasicApp()
18+
if __name__ == "__main__":
19+
app.run()
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
Sparkline {
2+
width: 100%;
3+
margin: 1;
4+
}
5+
6+
#fst > .sparkline--max-color {
7+
color: $success;
8+
}
9+
#fst > .sparkline--min-color {
10+
color: $warning;
11+
}
12+
13+
#snd > .sparkline--max-color {
14+
color: $warning;
15+
}
16+
#snd > .sparkline--min-color {
17+
color: $success;
18+
}
19+
20+
#trd > .sparkline--max-color {
21+
color: $error;
22+
}
23+
#trd > .sparkline--min-color {
24+
color: $warning;
25+
}
26+
27+
#frt > .sparkline--max-color {
28+
color: $warning;
29+
}
30+
#frt > .sparkline--min-color {
31+
color: $error;
32+
}
33+
34+
#fft > .sparkline--max-color {
35+
color: $accent;
36+
}
37+
#fft > .sparkline--min-color {
38+
color: $accent 30%;
39+
}
40+
41+
#sxt > .sparkline--max-color {
42+
color: $accent 30%;
43+
}
44+
#sxt > .sparkline--min-color {
45+
color: $accent;
46+
}
47+
48+
#svt > .sparkline--max-color {
49+
color: $error;
50+
}
51+
#svt > .sparkline--min-color {
52+
color: $error 30%;
53+
}
54+
55+
#egt > .sparkline--max-color {
56+
color: $error 30%;
57+
}
58+
#egt > .sparkline--min-color {
59+
color: $error;
60+
}
61+
62+
#nnt > .sparkline--max-color {
63+
color: $success;
64+
}
65+
#nnt > .sparkline--min-color {
66+
color: $success 30%;
67+
}
68+
69+
#tnt > .sparkline--max-color {
70+
color: $success 30%;
71+
}
72+
#tnt > .sparkline--min-color {
73+
color: $success;
74+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from textual.app import App, ComposeResult
2+
from textual.widgets._sparkline import Sparkline
3+
4+
5+
class SparklineColorsApp(App[None]):
6+
CSS_PATH = "sparkline_colors.css"
7+
8+
def compose(self) -> ComposeResult:
9+
nums = [10, 2, 30, 60, 45, 20, 7, 8, 9, 10, 50, 13, 10, 6, 5, 4, 3, 7, 20]
10+
yield Sparkline(nums, summary_function=max, id="fst")
11+
yield Sparkline(nums, summary_function=max, id="snd")
12+
yield Sparkline(nums, summary_function=max, id="trd")
13+
yield Sparkline(nums, summary_function=max, id="frt")
14+
yield Sparkline(nums, summary_function=max, id="fft")
15+
yield Sparkline(nums, summary_function=max, id="sxt")
16+
yield Sparkline(nums, summary_function=max, id="svt")
17+
yield Sparkline(nums, summary_function=max, id="egt")
18+
yield Sparkline(nums, summary_function=max, id="nnt")
19+
yield Sparkline(nums, summary_function=max, id="tnt")
20+
21+
22+
app = SparklineColorsApp()
23+
if __name__ == "__main__":
24+
app.run()

docs/roadmap.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ Widgets are key to making user-friendly interfaces. The builtin widgets should c
5858
* [ ] Braille
5959
* [ ] Sixels, and other image extensions
6060
- [x] Input
61-
* [ ] Validation
61+
* [x] Validation
6262
* [ ] Error / warning states
6363
* [ ] Template types: IP address, physical units (weight, volume), currency, credit card etc
6464
- [X] Select control (pull-down)
@@ -72,7 +72,7 @@ Widgets are key to making user-friendly interfaces. The builtin widgets should c
7272
- [X] Progress bars
7373
* [ ] Style variants (solid, thin etc)
7474
- [X] Radio boxes
75-
- [ ] Spark-lines
75+
- [X] Spark-lines
7676
- [X] Switch
7777
- [X] Tabs
7878
- [ ] TextArea (multi-line input)

docs/widget_gallery.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,15 @@ Select multiple values from a list of options.
216216
```{.textual path="docs/examples/widgets/selection_list_selections.py" press="down,down,down"}
217217
```
218218

219+
## Sparkline
220+
221+
Display numerical data.
222+
223+
[Sparkline reference](./widgets/sparkline.md){ .md-button .md-button--primary }
224+
225+
```{.textual path="docs/examples/widgets/sparkline.py" lines="11"}
226+
```
227+
219228
## Static
220229

221230
Displays simple static content. Typically used as a base class.

docs/widgets/sparkline.md

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# Sparkline
2+
3+
!!! tip "Added in version 0.27.0"
4+
5+
A widget that is used to visually represent numerical data.
6+
7+
- [ ] Focusable
8+
- [ ] Container
9+
10+
## Examples
11+
12+
### Basic example
13+
14+
The example below illustrates the relationship between the data, its length, the width of the sparkline, and the number of bars displayed.
15+
16+
!!! tip
17+
18+
The sparkline data is split into equally-sized chunks.
19+
Each chunk is represented by a bar and the width of the sparkline dictates how many bars there are.
20+
21+
=== "Output"
22+
23+
```{.textual path="docs/examples/widgets/sparkline_basic.py" lines="5" columns="30"}
24+
```
25+
26+
=== "sparkline_basic.py"
27+
28+
```python hl_lines="4 11 12 13"
29+
--8<-- "docs/examples/widgets/sparkline_basic.py"
30+
```
31+
32+
1. We have 12 data points.
33+
2. This sparkline will have its width set to 3 via CSS.
34+
3. The data (12 numbers) will be split across 3 bars, so 4 data points are associated with each bar.
35+
4. Each bar will represent its largest value.
36+
The largest value of each chunk is 2, 4, and 8, respectively.
37+
That explains why the first bar is half the height of the second and the second bar is half the height of the third.
38+
39+
=== "sparkline_basic.css"
40+
41+
```sass
42+
--8<-- "docs/examples/widgets/sparkline_basic.css"
43+
```
44+
45+
1. By setting the width to 3 we get three buckets.
46+
47+
### Different summary functions
48+
49+
The example below shows a sparkline widget with different summary functions.
50+
The summary function is what determines the height of each bar.
51+
52+
=== "Output"
53+
54+
```{.textual path="docs/examples/widgets/sparkline.py" lines="11"}
55+
```
56+
57+
=== "sparkline.py"
58+
59+
```python hl_lines="15-17"
60+
--8<-- "docs/examples/widgets/sparkline.py"
61+
```
62+
63+
1. Each bar will show the largest value of that bucket.
64+
2. Each bar will show the mean value of that bucket.
65+
3. Each bar will show the smaller value of that bucket.
66+
67+
=== "sparkline.css"
68+
69+
```sass
70+
--8<-- "docs/examples/widgets/sparkline.css"
71+
```
72+
73+
### Changing the colors
74+
75+
The example below shows how to use component classes to change the colors of the sparkline.
76+
77+
=== "Output"
78+
79+
```{.textual path="docs/examples/widgets/sparkline_colors.py" lines=22}
80+
```
81+
82+
=== "sparkline_colors.py"
83+
84+
```python
85+
--8<-- "docs/examples/widgets/sparkline_colors.py"
86+
```
87+
88+
=== "sparkline_colors.css"
89+
90+
```sass
91+
--8<-- "docs/examples/widgets/sparkline_colors.css"
92+
```
93+
94+
95+
## Reactive Attributes
96+
97+
| Name | Type | Default | Description |
98+
| --------- | ----- | ----------- | -------------------------------------------------- |
99+
| `data` | `Sequence[float] | None` | `None` | The data represented by the sparkline. |
100+
| `summary_function` | `Callable[[Sequence[float]], float]` | `max` | The function that computes the height of each bar. |
101+
102+
103+
## Messages
104+
105+
This widget sends no messages.
106+
107+
---
108+
109+
110+
::: textual.widgets.Sparkline
111+
options:
112+
heading_level: 2

mkdocs-nav.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ nav:
152152
- "widgets/radioset.md"
153153
- "widgets/select.md"
154154
- "widgets/selection_list.md"
155+
- "widgets/sparkline.md"
155156
- "widgets/static.md"
156157
- "widgets/switch.md"
157158
- "widgets/tabbed_content.md"

0 commit comments

Comments
 (0)