diff --git a/docsSite/docs/tab-reference/img/line-graph-2.png b/docsSite/docs/tab-reference/img/line-graph-2.png
index 2fd31e6a..4650800d 100644
Binary files a/docsSite/docs/tab-reference/img/line-graph-2.png and b/docsSite/docs/tab-reference/img/line-graph-2.png differ
diff --git a/docsSite/docs/tab-reference/img/line-graph-3.png b/docsSite/docs/tab-reference/img/line-graph-3.png
index 001cc7e0..2fd31e6a 100644
Binary files a/docsSite/docs/tab-reference/img/line-graph-3.png and b/docsSite/docs/tab-reference/img/line-graph-3.png differ
diff --git a/docsSite/docs/tab-reference/img/line-graph-4.png b/docsSite/docs/tab-reference/img/line-graph-4.png
new file mode 100644
index 00000000..001cc7e0
Binary files /dev/null and b/docsSite/docs/tab-reference/img/line-graph-4.png differ
diff --git a/docsSite/docs/tab-reference/line-graph.md b/docsSite/docs/tab-reference/line-graph.md
index cf997e6f..8facda2d 100644
--- a/docsSite/docs/tab-reference/line-graph.md
+++ b/docsSite/docs/tab-reference/line-graph.md
@@ -2,8 +2,8 @@
sidebar_position: 1
---
-import Image2 from './img/line-graph-2.png';
import Image3 from './img/line-graph-3.png';
+import Image4 from './img/line-graph-4.png';
# 📉 Line Graph
@@ -29,11 +29,17 @@ To get started, drag a field to one of the three sections (left, right, or discr
The color and line style of each field can be customized by clicking the colored icon or right-clicking on the field name.
:::
+:::tip
+Data from the WPILib [persistent alerts](https://docs.wpilib.org/en/latest/docs/software/telemetry/persistent-alerts.html) API can be visualized by adding the alerts group as a discrete field. An example visualization is shown below.
+
+![Alerts visualization](./img/line-graph-2.png)
+:::
+
### Adjusting Axes
By default, each axis adjusts its range based on the visible data. To disable auto-ranging and lock the range to its current min and max, click the three dots near the axis title and then `Lock Axis`. To manually adjust the range, choose `Edit Range...` and enter the desired values.
-
+
### Unit Conversion
@@ -43,7 +49,7 @@ To adjust the units for an axis, click the three dots near the axis title and th
To quickly enable or disable unit conversion, click the three dots near the axis title and choose `Recent Presets` or `Reset Units`.
:::
-
+
### Integration & Differentiation
diff --git a/docsSite/docs/whats-new/index.md b/docsSite/docs/whats-new/index.md
index d0991cf1..494f435d 100644
--- a/docsSite/docs/whats-new/index.md
+++ b/docsSite/docs/whats-new/index.md
@@ -55,6 +55,8 @@ New **integration** and **differentiation** options, along with a **streamlined
Want to visualize even more data at once? Try **popping out the line graph** to a separate window!
+Users of the WPILib [persistent alerts](https://docs.wpilib.org/en/latest/docs/software/telemetry/persistent-alerts.html) API can now visualize errors, warnings, and info messages as a compact waterfall chart.
+
![Line graph styles](./img/line-graph-styles.png)
### 📊 Redesigned Statistics View
diff --git a/src/hub/SourceList.ts b/src/hub/SourceList.ts
index 9049af2f..a8643622 100644
--- a/src/hub/SourceList.ts
+++ b/src/hub/SourceList.ts
@@ -996,10 +996,10 @@ export default class SourceList {
let logType = window.log.getType(state.logKey);
let structuredType = window.log.getStructuredType(state.logKey);
if (logType !== null) {
- let value: any;
+ let value: any = null;
if (logType === LoggableType.Number && this.getNumberPreview !== undefined) {
value = this.getNumberPreview(state.logKey, time);
- } else {
+ } else if (logType !== LoggableType.Empty) {
value = getOrDefault(window.log, state.logKey, logType, time, null);
}
if (value !== null || logType === LoggableType.Empty) {
@@ -1159,6 +1159,40 @@ export default class SourceList {
let count = mechanismState.lines.length;
text = count.toString() + " segment" + (count === 1 ? "" : "s");
}
+ } else if (structuredType === "Alerts") {
+ let errorCount: number = getOrDefault(
+ window.log,
+ state.logKey + "/errors",
+ LoggableType.StringArray,
+ time,
+ []
+ ).length;
+ let warningCount: number = getOrDefault(
+ window.log,
+ state.logKey + "/warnings",
+ LoggableType.StringArray,
+ time,
+ []
+ ).length;
+ let infoCount: number = getOrDefault(
+ window.log,
+ state.logKey + "/infos",
+ LoggableType.StringArray,
+ time,
+ []
+ ).length;
+ text =
+ errorCount.toString() +
+ " error" +
+ (errorCount === 1 ? "" : "s") +
+ ", " +
+ warningCount.toString() +
+ " warning" +
+ (warningCount === 1 ? "" : "s") +
+ ", " +
+ infoCount.toString() +
+ " info" +
+ (infoCount === 1 ? "" : "s");
} else if (
logType === LoggableType.BooleanArray ||
logType === LoggableType.NumberArray ||
diff --git a/src/hub/controllers/LineGraphController.ts b/src/hub/controllers/LineGraphController.ts
index be2f63b0..2af95d70 100644
--- a/src/hub/controllers/LineGraphController.ts
+++ b/src/hub/controllers/LineGraphController.ts
@@ -5,6 +5,8 @@ import { AKIT_TIMESTAMP_KEYS, getEnabledKey, getLogValueText } from "../../share
import { LogValueSetNumber } from "../../shared/log/LogValueSets";
import {
LineGraphRendererCommand,
+ LineGraphRendererCommand_Alert,
+ LineGraphRendererCommand_AlertSet,
LineGraphRendererCommand_DiscreteField,
LineGraphRendererCommand_NumericField
} from "../../shared/renderers/LineGraphRenderer";
@@ -468,7 +470,7 @@ export default class LineGraphController implements TabController {
// Add discrete fields
this.discreteSourceList.getState().forEach((fieldItem) => {
- if (!fieldItem.visible) return;
+ if (!fieldItem.visible || fieldItem.type === "alerts") return;
let data = window.log.getRange(fieldItem.logKey, timeRange[0], timeRange[1]);
if (data === undefined) return;
@@ -504,6 +506,76 @@ export default class LineGraphController implements TabController {
});
});
+ // Process alerts
+ let alerts: LineGraphRendererCommand_AlertSet = [];
+ (["error", "warning", "info"] as const).forEach((alertType) => {
+ this.discreteSourceList.getState().forEach((fieldItem) => {
+ if (!fieldItem.visible || fieldItem.type !== "alerts") return;
+
+ let valueSet = window.log.getStringArray(fieldItem.logKey + "/" + alertType + "s", -Infinity, Infinity);
+ if (valueSet === undefined) return;
+
+ let allAlerts: LineGraphRendererCommand_Alert[] = [];
+ for (let i = 0; i < valueSet.values.length; i++) {
+ // Add new alerts
+ new Set(valueSet.values[i]).forEach((alertText) => {
+ let currentCount = valueSet!.values[i].filter((x) => x === alertText).length;
+ let activeCount = allAlerts.filter((x) => x.text === alertText && !isFinite(x.range[1])).length;
+ if (currentCount > activeCount) {
+ for (let count = 0; count < currentCount - activeCount; count++) {
+ allAlerts.push({
+ type: alertType,
+ text: alertText,
+ range: [valueSet!.timestamps[i], Infinity]
+ });
+ }
+ }
+ });
+
+ // Clear inactive alerts
+ new Set(allAlerts.map((x) => x.text)).forEach((alertText) => {
+ let currentCount = valueSet!.values[i].filter((x) => x === alertText).length;
+ let activeCount = allAlerts.filter((x) => x.text === alertText && !isFinite(x.range[1])).length;
+ if (activeCount > currentCount) {
+ for (let count = 0; count < activeCount - currentCount; count++) {
+ allAlerts.find((alert) => alert.text === alertText && !isFinite(alert.range[1]))!.range[1] =
+ valueSet!.timestamps[i];
+ }
+ }
+ });
+ }
+
+ // Clear all remaining active alerts
+ allAlerts.forEach((alert) => {
+ if (alert.range[1] === Infinity) {
+ alert.range[1] = timeRange[1];
+ }
+ });
+
+ // Add alerts to main set
+ allAlerts.forEach((alert) => {
+ let row = -1;
+ do {
+ row++;
+ while (alerts.length <= row) {
+ alerts.push([]);
+ }
+ } while (!alerts[row].every((other) => other.range[1] <= alert.range[0] || other.range[0] >= alert.range[1]));
+ alerts[row].push(alert);
+ });
+ });
+ });
+
+ // Remove offscreen alerts
+ alerts = alerts.map((row) =>
+ row.filter(
+ (alert) =>
+ !(alert.range[0] > timeRange[1] && alert.range[1] > timeRange[1]) &&
+ !(alert.range[0] < timeRange[0] && alert.range[1] < timeRange[0])
+ )
+ );
+ alerts = alerts.filter((row) => row.length > 0);
+
// Get numeric ranges
let calcRange = (dataRange: [number, number], lockedRange: [number, number] | null): [number, number] => {
let range: [number, number];
@@ -546,7 +618,8 @@ export default class LineGraphController implements TabController {
: "left",
leftFields: leftFieldsCommand,
rightFields: rightFieldsCommand,
- discreteFields: discreteFieldsCommand
+ discreteFields: discreteFieldsCommand,
+ alerts: alerts
};
}
diff --git a/src/hub/controllers/LineGraphController_Config.ts b/src/hub/controllers/LineGraphController_Config.ts
index 59fa8b54..091ad30e 100644
--- a/src/hub/controllers/LineGraphController_Config.ts
+++ b/src/hub/controllers/LineGraphController_Config.ts
@@ -127,6 +127,16 @@ export const LineGraphController_DiscreteConfig: SourceListConfig = {
values: GraphColors
}
]
+ },
+ {
+ key: "alerts",
+ display: "Alerts",
+ symbol: "list.bullet",
+ showInTypeName: false,
+ color: "#ffaa00",
+ sourceTypes: ["Alerts"],
+ showDocs: true,
+ options: []
}
]
};
diff --git a/src/shared/renderers/LineGraphRenderer.ts b/src/shared/renderers/LineGraphRenderer.ts
index faf4f33f..2904a59a 100644
--- a/src/shared/renderers/LineGraphRenderer.ts
+++ b/src/shared/renderers/LineGraphRenderer.ts
@@ -1,6 +1,7 @@
import ScrollSensor from "../../hub/ScrollSensor";
+import { ensureThemeContrast } from "../Colors";
import { SelectionMode } from "../Selection";
-import { ValueScaler, calcAxisStepSize, clampValue, cleanFloat, scaleValue, shiftColor } from "../util";
+import { calcAxisStepSize, clampValue, cleanFloat, scaleValue, shiftColor, ValueScaler } from "../util";
import TabRenderer from "./TabRenderer";
export default class LineGraphRenderer implements TabRenderer {
@@ -123,8 +124,8 @@ export default class LineGraphRenderer implements TabRenderer {
this.SCROLL_OVERLAY.style.top = graphTop.toString() + "px";
let graphHeight = height - graphTop - 35;
if (graphHeight < 1) graphHeight = 1;
- let graphHeightOpen =
- graphHeight - command.discreteFields.length * 20 - (command.discreteFields.length > 0 ? 5 : 0);
+ let discreteRowCount = command.discreteFields.length + command.alerts.length;
+ let graphHeightOpen = graphHeight - discreteRowCount * 20 - (discreteRowCount > 0 ? 5 : 0);
if (graphHeightOpen < 1) graphHeightOpen = 1;
// Calculate Y step sizes
@@ -225,6 +226,36 @@ export default class LineGraphRenderer implements TabRenderer {
}
});
+ // Render alerts
+ command.alerts.forEach((alertsRow, rowIndex) => {
+ let topY = graphTop + graphHeight - 20 - (command.discreteFields.length + rowIndex) * 20;
+ alertsRow.forEach((alert) => {
+ let startX = scaleValue(alert.range[0], timeRange, [graphLeft, graphLeft + graphWidth]);
+ let endX = scaleValue(alert.range[1], timeRange, [graphLeft, graphLeft + graphWidth]);
+
+ // Draw shape
+ switch (alert.type) {
+ case "error":
+ context.fillStyle = ensureThemeContrast("#ff0000");
+ break;
+ case "warning":
+ context.fillStyle = ensureThemeContrast("#ffaa00");
+ break;
+ case "info":
+ context.fillStyle = ensureThemeContrast("#00ff00");
+ break;
+ }
+ context.fillRect(startX, topY, endX - startX, 15);
+
+ // Draw text
+ let adjustedStartX = startX < graphLeft ? graphLeft : startX;
+ if (endX - adjustedStartX > 10) {
+ context.fillStyle = "black";
+ context.fillText(alert.text, adjustedStartX + 5, topY + 15 / 2, endX - adjustedStartX - 10);
+ }
+ });
+ });
+
// Render continuous data
const xScaler = new ValueScaler(timeRange, [graphLeft, graphLeft + graphWidth]);
let drawNumericFields = (fields: LineGraphRendererCommand_NumericField[], yRange: [number, number]) => {
@@ -607,6 +638,7 @@ export type LineGraphRendererCommand = {
leftFields: LineGraphRendererCommand_NumericField[];
rightFields: LineGraphRendererCommand_NumericField[];
discreteFields: LineGraphRendererCommand_DiscreteField[];
+ alerts: LineGraphRendererCommand_AlertSet;
};
export type LineGraphRendererCommand_NumericField = {
@@ -624,3 +656,11 @@ export type LineGraphRendererCommand_DiscreteField = {
type: "stripes" | "graph";
toggleReference: boolean;
};
+
+export type LineGraphRendererCommand_AlertSet = LineGraphRendererCommand_Alert[][];
+
+export type LineGraphRendererCommand_Alert = {
+ type: "error" | "warning" | "info";
+ text: string;
+ range: [number, number];
+};
diff --git a/www/symbols/sourceList/list.bullet.svg b/www/symbols/sourceList/list.bullet.svg
new file mode 100644
index 00000000..10298e1d
--- /dev/null
+++ b/www/symbols/sourceList/list.bullet.svg
@@ -0,0 +1,16 @@
+
+
+
+