Skip to content

Commit 0a236be

Browse files
committed
Add area chart, stacked chart and manual configurability of chart style for ChartWidget
1 parent 20ebc00 commit 0a236be

File tree

4 files changed

+79
-9
lines changed

4 files changed

+79
-9
lines changed

docs/web/log_widgets.rst

+7
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,10 @@ Web UI Widgets for showing Logs
1818
:members:
1919
:member-order:
2020

21+
.. autoclass:: ChartPlotStyle
22+
:members:
23+
:member-order:
24+
25+
.. autoclass:: ChartLineInterpolation
26+
:members:
27+
:member-order:

example/ui_logging_showcase.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from shc.interfaces.in_memory_data_logging import InMemoryDataLogVariable
1515
import shc.web.log_widgets
1616
from shc.data_logging import AggregationMethod
17-
from shc.web.log_widgets import ChartDataSpec, LogListDataSpec
17+
from shc.web.log_widgets import ChartDataSpec, LogListDataSpec, ChartPlotStyle, ChartLineInterpolation
1818
from shc.web.widgets import icon
1919

2020
random_float_log = InMemoryDataLogVariable(float, keep=datetime.timedelta(minutes=10))
@@ -74,6 +74,14 @@ async def new_bool(_v, _o):
7474
ChartDataSpec(random_float_log, "random float", scale_factor=10)
7575
]))
7676

77+
index_page.add_item(shc.web.log_widgets.ChartWidget(datetime.timedelta(minutes=5), [
78+
ChartDataSpec(random_float_log, "random float", scale_factor=10, stack_group="a",
79+
line_interpolation=ChartLineInterpolation.STEP_BEFORE, plot_style=ChartPlotStyle.AREA),
80+
ChartDataSpec(random_float_log, "random float", scale_factor=10, stack_group="a",
81+
line_interpolation=ChartLineInterpolation.STEP_BEFORE, plot_style=ChartPlotStyle.AREA),
82+
ChartDataSpec(random_float_log, "random float", scale_factor=15, plot_style=ChartPlotStyle.LINE),
83+
]))
84+
7785
index_page.add_item(shc.web.log_widgets.ChartWidget(datetime.timedelta(minutes=5), [
7886
ChartDataSpec(random_float_log, "avg", aggregation=AggregationMethod.AVERAGE, unit_symbol="°C"),
7987
ChartDataSpec(random_float_log, "min", aggregation=AggregationMethod.MINIMUM, unit_symbol="°C"),

shc/web/log_widgets.py

+44-3
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"""
2020
from dataclasses import dataclass
2121
import datetime
22+
import enum
2223
from typing import Iterable, Optional, Generic, Union, Callable, Tuple, List
2324

2425
from markupsafe import Markup
@@ -118,6 +119,32 @@ def get_connectors(self) -> Iterable[WebUIConnector]:
118119
return self.connectors
119120

120121

122+
class ChartPlotStyle(enum.Enum):
123+
#: Uses `LINE_DOTS` for aggregated values, otherwise `LINE_FILLED`
124+
AUTO = "auto"
125+
#: A simple line plot, no dots, no background filling
126+
LINE = "line"
127+
#: A line plot with dots, no background filling
128+
LINE_DOTS = "line_dots"
129+
#: A pretty line plot with slightly colored background area
130+
LINE_FILLED = "line_filled"
131+
#: An area plot without visible line but stronger colored filled area
132+
AREA = "area"
133+
134+
135+
class ChartLineInterpolation(enum.Enum):
136+
#: Uses `SMOOTH` for aggregated values, otherwise `LINEAR`
137+
AUTO = "auto"
138+
#: Use bezier line interpolation
139+
SMOOTH = "smooth"
140+
#: Use linear interpolation (straight lines between data points)
141+
LINEAR = "linear"
142+
#: Use stepped graph, beginning a horizontal line at each data point
143+
STEP_BEFORE = "before"
144+
#: Use stepped graph with horizontal lines ending at each data point
145+
STEP_AFTER = "after"
146+
147+
121148
@dataclass
122149
class ChartDataSpec:
123150
"""Specification of one data log source and the formatting of its datapoints within a :class:`ChartWidget`"""
@@ -134,6 +161,12 @@ class ChartDataSpec:
134161
scale_factor: float = 1.0
135162
#: Unit symbol to be shown after the value in the Chart tooltip
136163
unit_symbol: str = ""
164+
#: If not None, this data row will be stacked upon (values added to) previous data rows with the same `stack_group`
165+
stack_group: Optional[str] = None
166+
#: Select different plot styles (area vs. line plot, dots on/off)
167+
plot_style: ChartPlotStyle = ChartPlotStyle.AUTO
168+
#: Select different line interpolation styles (smoothed, linear, stepped)
169+
line_interpolation: ChartLineInterpolation = ChartLineInterpolation.AUTO
137170

138171

139172
class ChartWidget(WebPageItem):
@@ -213,14 +246,22 @@ def __init__(self, interval: datetime.timedelta, data_spec: List[ChartDataSpec]
213246
aggregation_interval, align_to=self.align_ticks_to,
214247
converter=None if spec.scale_factor == 1.0 else lambda x: x*spec.scale_factor,
215248
include_previous=True)
249+
line_interpolation = spec.line_interpolation
250+
if line_interpolation == ChartLineInterpolation.AUTO:
251+
line_interpolation = ChartLineInterpolation.SMOOTH if is_aggregated else ChartLineInterpolation.LINEAR
252+
plot_style = spec.plot_style
253+
if plot_style == ChartPlotStyle.AUTO:
254+
plot_style = ChartPlotStyle.LINE_DOTS if is_aggregated else ChartPlotStyle.LINE_FILLED
216255
self.connectors.append(connector)
217256
self.row_specs.append({'id': id(connector),
218-
'stepped_graph': is_aggregated,
219-
'show_points': is_aggregated,
220257
'color': spec.color if spec.color is not None else self.COLORS[i % len(self.COLORS)],
221258
'label': spec.label,
222259
'unit_symbol': spec.unit_symbol,
223-
'extend_graph_to_now': not is_aggregated})
260+
'extend_graph_to_now': not is_aggregated,
261+
'stack_group': spec.stack_group,
262+
'style': plot_style.value,
263+
'interpolation': line_interpolation.value,
264+
})
224265

225266
async def render(self) -> str:
226267
return await jinja_env.get_template('log/chart.htm').render_async(

web_ui_src/widgets/log_widgets.js

+19-5
Original file line numberDiff line numberDiff line change
@@ -150,22 +150,33 @@ function LineChartWidget(domElement, _writeValue) {
150150
// datasets (and subscribeIds)
151151
let datasets = [];
152152
let dataMap = new Map(); // maps the Python datapoint/object id to the associated chart dataset's data list
153+
let numStacked = 0;
153154
for (const spec of seriesSpec) {
154155
this.subscribeIds.push(spec.id);
155156
let data = [];
156157
datasets.push({
157158
data: data,
158159
label: spec.label,
159-
backgroundColor: `rgba(${spec.color[0]}, ${spec.color[1]}, ${spec.color[2]}, 0.1)`,
160+
backgroundColor: (spec.style == "area"
161+
? `rgba(${spec.color[0]}, ${spec.color[1]}, ${spec.color[2]}, 0.5)`
162+
: `rgba(${spec.color[0]}, ${spec.color[1]}, ${spec.color[2]}, 0.1)`),
160163
borderColor: `rgba(${spec.color[0]}, ${spec.color[1]}, ${spec.color[2]}, .5)`,
161164
pointBackgroundColor: `rgba(${spec.color[0]}, ${spec.color[1]}, ${spec.color[2]}, 0.5)`,
162165
pointBorderColor: `rgba(${spec.color[0]}, ${spec.color[1]}, ${spec.color[2]}, 0.5)`,
163-
stepped: spec.stepped_graph ? undefined : 'before',
164-
pointRadius: spec.show_points ? 3 : 0,
165-
pointHitRadius: 3,
166-
tension: 0.2,
166+
stepped: ["before", "after"].includes(spec.interpolation) ? spec.interpolation : undefined,
167+
pointRadius: spec.style == "line_dots" ? 3 : 0,
168+
pointHitRadius: 5,
169+
tension: spec.interpolation == "smooth" ? 0.2 : 0.0,
170+
stack: spec.stack_group,
171+
fill: (spec.style == "area" || spec.style == "line_filled"
172+
? (spec.stack_group !== null && numStacked > 0 ? "-1" : "origin")
173+
: undefined),
174+
showLine: ["line", "line_filled", "line_dots"].includes(spec.style),
167175
});
168176
dataMap.set(spec.id, [data, spec]);
177+
if (spec.stack_group !== null) {
178+
numStacked++;
179+
}
169180
}
170181

171182
// Initialize chart
@@ -206,6 +217,9 @@ function LineChartWidget(domElement, _writeValue) {
206217
ticks: {
207218
source: 'labels',
208219
}
220+
},
221+
y: {
222+
stacked: numStacked > 0 ? true : false,
209223
}
210224
},
211225
responsive: true

0 commit comments

Comments
 (0)