better graphing

This commit is contained in:
2026-08-22 09:21:17 +02:00
parent 88fc09d2bd
commit 4d04d961a3
3 changed files with 40 additions and 7 deletions
+4
View File
@@ -402,6 +402,10 @@ Each graph contains a filled temperature area, a solid humidity line, and a
dotted battery line. Initially, most of each graph is blank because no historic dotted battery line. Initially, most of each graph is blank because no historic
samples existed before the RRD was created. samples existed before the RRD was created.
Temperature uses the left axis with a default range of 030°C. Humidity and
battery use the right axis with a default range of 0100%. These ranges and
labels can be changed in `config.yaml` under `graph.axes`.
The collector also generates `/var/www/html/sensors/index.html`. It displays The collector also generates `/var/www/html/sensors/index.html`. It displays
six graphs at a time and has controls for switching between the daily, monthly, six graphs at a time and has controls for switching between the daily, monthly,
and two-year periods. It remembers the selected period and refreshes the PNGs and two-year periods. It remembers the selected period and refreshes the PNGs
+9 -2
View File
@@ -72,9 +72,16 @@ graph:
width: 1000 width: 1000
height: 360 height: 360
end: now end: now
vertical_label: degrees C / percent
slope_mode: true slope_mode: true
alt_autoscale: true axes:
temperature:
minimum: 0
maximum: 30
label: degrees C
percentage:
minimum: 0
maximum: 100
label: percent
periods: periods:
day: day:
start: end-1d start: end-1d
+27 -5
View File
@@ -194,11 +194,26 @@ def render_graphs(sensor: Sensor, rrd_path: Path, config: Config) -> None:
temperature = graph["temperature"] temperature = graph["temperature"]
humidity = graph["humidity"] humidity = graph["humidity"]
battery = graph["battery"] battery = graph["battery"]
temperature_axis = graph["axes"]["temperature"]
percentage_axis = graph["axes"]["percentage"]
temperature_minimum = float(temperature_axis["minimum"])
temperature_maximum = float(temperature_axis["maximum"])
percentage_minimum = float(percentage_axis["minimum"])
percentage_maximum = float(percentage_axis["maximum"])
temperature_span = temperature_maximum - temperature_minimum
percentage_span = percentage_maximum - percentage_minimum
if temperature_span <= 0 or percentage_span <= 0:
raise SystemExit("Graph axis maximums must be greater than their minimums")
percentage_to_temperature = temperature_span / percentage_span
right_axis_scale = percentage_span / temperature_span
right_axis_shift = percentage_minimum - temperature_minimum * right_axis_scale
percentage_transform = (
f"{percentage_minimum},-,{percentage_to_temperature},*,{temperature_minimum},+"
)
optional_flags = [] optional_flags = []
if graph.get("slope_mode", False): if graph.get("slope_mode", False):
optional_flags.append("--slope-mode") optional_flags.append("--slope-mode")
if graph.get("alt_autoscale", False):
optional_flags.append("--alt-autoscale")
for suffix, period in graph["periods"].items(): for suffix, period in graph["periods"].items():
output = config.graph_dir / f"{sensor.slug}_{suffix}.png" output = config.graph_dir / f"{sensor.slug}_{suffix}.png"
@@ -206,15 +221,22 @@ def render_graphs(sensor: Sensor, rrd_path: Path, config: Config) -> None:
"graph", str(output), "--start", str(period["start"]), "graph", str(output), "--start", str(period["start"]),
"--end", str(graph["end"]), "--end", str(graph["end"]),
"--title", f"{sensor.name} - {period['label']}", "--title", f"{sensor.name} - {period['label']}",
"--vertical-label", str(graph["vertical_label"]), "--vertical-label", str(temperature_axis["label"]),
"--right-axis", f"{right_axis_scale}:{right_axis_shift}",
"--right-axis-label", str(percentage_axis["label"]),
"--lower-limit", str(temperature_minimum),
"--upper-limit", str(temperature_maximum),
"--rigid",
"--width", str(graph["width"]), "--height", str(graph["height"]), "--width", str(graph["width"]), "--height", str(graph["height"]),
*optional_flags, *optional_flags,
f"DEF:temp={rrd_path}:temperature:AVERAGE", f"DEF:temp={rrd_path}:temperature:AVERAGE",
f"DEF:humidity={rrd_path}:humidity:AVERAGE", f"DEF:humidity={rrd_path}:humidity:AVERAGE",
f"DEF:battery={rrd_path}:battery:AVERAGE", f"DEF:battery={rrd_path}:battery:AVERAGE",
f"CDEF:humidity_scaled=humidity,{percentage_transform}",
f"CDEF:battery_scaled=battery,{percentage_transform}",
f"AREA:temp#{temperature['color']}:{temperature['legend']}", f"AREA:temp#{temperature['color']}:{temperature['legend']}",
f"LINE{humidity['line_width']}:humidity#{humidity['color']}:{humidity['legend']}", f"LINE{humidity['line_width']}:humidity_scaled#{humidity['color']}:{humidity['legend']}",
f"LINE{battery['line_width']}:battery#{battery['color']}:{battery['legend']}:dashes={battery['dashes']}", f"LINE{battery['line_width']}:battery_scaled#{battery['color']}:{battery['legend']}:dashes={battery['dashes']}",
f"GPRINT:temp:LAST:{temperature['last_value_format']}", f"GPRINT:temp:LAST:{temperature['last_value_format']}",
f"GPRINT:humidity:LAST:{humidity['last_value_format']}", f"GPRINT:humidity:LAST:{humidity['last_value_format']}",
f"GPRINT:battery:LAST:{battery['last_value_format']}", f"GPRINT:battery:LAST:{battery['last_value_format']}",