Record flags in history and draw them as steps
A bool was excluded from the history as "not a measurement", so a true/false port had no curve in the node panel and none on an edge — only the word. It is recorded as 0/1 now and drawn as steps, since a bezier through two states slopes through readings that never happened. The axis is pinned to 0..1, so a flag that was never on sits at the floor rather than mid-box. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -837,7 +837,7 @@ def read_message_history(
|
||||
"""The recent values of one message, for plotting.
|
||||
|
||||
``message`` may be given bare or qualified; a message that never carried a
|
||||
number comes back with an empty series.
|
||||
number or a flag comes back with an empty series.
|
||||
"""
|
||||
_read_flow(controller, name)
|
||||
key = qualify(name, message)
|
||||
|
||||
@@ -102,7 +102,7 @@ async def publish_message(
|
||||
|
||||
@router.get("/{name}/history", response_model=MessagePoints)
|
||||
def read_message_history(name: str, controller: FlowControllerDep) -> Any:
|
||||
"""The series behind a chart. Numbers only — nothing else plots."""
|
||||
"""The series behind a chart. Numbers and flags — nothing else plots."""
|
||||
if controller.pipeline is None:
|
||||
return MessagePoints(message=name, numeric=False, points=[])
|
||||
series = controller.state.history(name)
|
||||
|
||||
@@ -24,16 +24,20 @@ import redis
|
||||
#: not JSON at all, and a datetime serialises rather than raising.
|
||||
_JSON_OPTS = orjson.OPT_NON_STR_KEYS
|
||||
|
||||
# A sparkline only means something for numbers, so the history keeps the values
|
||||
# it can plot and nothing else. 120 points fill a panel-wide chart while leaving
|
||||
# Redis a cache rather than a time-series database.
|
||||
# A sparkline only means something for a value that can be placed on an axis,
|
||||
# so the history keeps those and nothing else. 120 points fill a panel-wide
|
||||
# chart while leaving Redis a cache rather than a time-series database.
|
||||
HISTORY_LIMIT = 120
|
||||
|
||||
|
||||
def as_number(value: Any) -> float | None:
|
||||
"""The plottable form of a value, or None if it is not a number."""
|
||||
# bool is an int subclass; a flag is not a measurement.
|
||||
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
||||
"""The plottable form of a value, or None if it cannot be placed on an axis."""
|
||||
# A flag is not a measurement, but when it was on is worth seeing, and 0/1
|
||||
# is what a step curve is drawn from. bool is an int subclass, so it would
|
||||
# fall through the isinstance below either way.
|
||||
if isinstance(value, bool):
|
||||
return 1.0 if value else 0.0
|
||||
if not isinstance(value, (int, float)):
|
||||
return None
|
||||
return float(value)
|
||||
|
||||
|
||||
@@ -45,12 +45,16 @@ def test_the_series_is_capped():
|
||||
assert points[-1] == (139.0, 139.0)
|
||||
|
||||
|
||||
def test_only_numbers_are_recorded():
|
||||
def test_numbers_and_flags_are_recorded_and_text_is_not():
|
||||
"""A flag plots as the step between 0 and 1; text has no axis to sit on."""
|
||||
state = MemoryState()
|
||||
state.append_history({"f.text": "warm", "f.flag": True, "f.temp": 21}, ts=1.0)
|
||||
state.append_history(
|
||||
{"f.text": "warm", "f.flag": True, "f.off": False, "f.temp": 21}, ts=1.0
|
||||
)
|
||||
|
||||
assert state.history("f.text") == []
|
||||
assert state.history("f.flag") == []
|
||||
assert state.history("f.flag") == [(1.0, 1.0)]
|
||||
assert state.history("f.off") == [(1.0, 0.0)]
|
||||
assert state.history("f.temp") == [(1.0, 21.0)]
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user