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. -Editing axis range +Editing axis range ### 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`. ::: -Editing unit conversion +Editing unit conversion ### 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 @@ + + + + + + + + + + + + + +