Skip to content

Commit 76b22a7

Browse files
scottbellcharlesh88shefalijoshiakhenry
authored
Discrete Event Visualization in Timeline (#7967)
Implements timeline visualization of discrete events such as Events and Commands * Closes #7960 Co-authored-by: Charles Hacskaylo <[email protected]> Co-authored-by: Shefali <[email protected]> Co-authored-by: Andrew Henry <[email protected]> Co-authored-by: Charles Hacskaylo <[email protected]>
1 parent bd27bf9 commit 76b22a7

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+1823
-201
lines changed

e2e/tests/functional/planning/ganttChart.e2e.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ test.describe('Gantt Chart', () => {
126126
await page.goto(ganttChart.url);
127127

128128
// Assert that the Plan's status is displayed as draft
129-
expect(await page.locator('.u-contents.c-swimlane.is-status--draft').count()).toBe(
129+
expect(await page.locator('.c-swimlane.is-status--draft').count()).toBe(
130130
Object.keys(testPlan1).length
131131
);
132132
});
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*****************************************************************************
2+
* Open MCT, Copyright (c) 2014-2024, United States Government
3+
* as represented by the Administrator of the National Aeronautics and Space
4+
* Administration. All rights reserved.
5+
*
6+
* Open MCT is licensed under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
* http://www.apache.org/licenses/LICENSE-2.0.
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14+
* License for the specific language governing permissions and limitations
15+
* under the License.
16+
*
17+
* Open MCT includes source code licensed under additional open source
18+
* licenses. See the Open Source Licenses file (LICENSES.md) included with
19+
* this source code distribution or the Licensing information page available
20+
* at runtime from the About dialog for additional information.
21+
*****************************************************************************/
22+
23+
import { createDomainObjectWithDefaults, setTimeConductorBounds } from '../../../../appActions.js';
24+
import { expect, test } from '../../../../pluginFixtures.js';
25+
26+
test.describe('Event Timeline View', () => {
27+
let eventTimelineView;
28+
let eventGenerator1;
29+
30+
test.beforeEach(async ({ page }) => {
31+
await page.goto('./', { waitUntil: 'domcontentloaded' });
32+
33+
eventTimelineView = await createDomainObjectWithDefaults(page, {
34+
type: 'Time Strip'
35+
});
36+
37+
await createDomainObjectWithDefaults(page, {
38+
type: 'Sine Wave Generator',
39+
parent: eventTimelineView.uuid
40+
});
41+
42+
eventGenerator1 = await createDomainObjectWithDefaults(page, {
43+
type: 'Event Message Generator',
44+
parent: eventTimelineView.uuid
45+
});
46+
47+
await createDomainObjectWithDefaults(page, {
48+
type: 'Event Message Generator with Acknowledge',
49+
parent: eventTimelineView.uuid
50+
});
51+
52+
await setTimeConductorBounds(page, {
53+
startDate: '2024-01-01',
54+
endDate: '2024-01-01',
55+
startTime: '01:01:00',
56+
endTime: '01:04:00'
57+
});
58+
});
59+
60+
test('Ensure we can build a Time Strip with event', async ({ page }) => {
61+
await page.goto(eventTimelineView.url);
62+
63+
// click on an event
64+
await page
65+
.getByLabel(eventTimelineView.name)
66+
.getByLabel(/PROGRAM ALARM/)
67+
.click();
68+
69+
// click on the event inspector tab
70+
await page.getByRole('tab', { name: 'Event' }).click();
71+
72+
// ensure the event inspector has the the same event
73+
await expect(page.getByText(/PROGRAM ALARM/)).toBeVisible();
74+
75+
// count the event lines
76+
const eventWrappersContainer = page.locator('.c-events-tsv__container');
77+
const eventWrappers = eventWrappersContainer.locator('.c-events-tsv__event-line');
78+
const expectedEventWrappersCount = 25;
79+
await expect(eventWrappers).toHaveCount(expectedEventWrappersCount);
80+
81+
// click on another event
82+
await page
83+
.getByLabel(eventTimelineView.name)
84+
.getByLabel(/pegged/)
85+
.click();
86+
87+
// ensure the tooltip shows up
88+
await expect(
89+
page.getByRole('tooltip').getByText(/pegged on horizontal velocity/)
90+
).toBeVisible();
91+
92+
// and that event appears in the inspector
93+
await expect(
94+
page.getByLabel('Inspector Views').getByText(/pegged on horizontal velocity/)
95+
).toBeVisible();
96+
97+
// turn on extended lines
98+
await page
99+
.getByRole('button', {
100+
name: `Toggle extended event lines overlay for ${eventGenerator1.name}`
101+
})
102+
.click();
103+
104+
// count the extended lines
105+
const overlayLinesContainer = page.locator('.c-timeline__overlay-lines');
106+
const extendedLines = overlayLinesContainer.locator('.c-timeline__event-line--extended');
107+
const expectedCount = 25;
108+
await expect(extendedLines).toHaveCount(expectedCount);
109+
});
110+
});
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*****************************************************************************
2+
* Open MCT, Copyright (c) 2014-2024, United States Government
3+
* as represented by the Administrator of the National Aeronautics and Space
4+
* Administration. All rights reserved.
5+
*
6+
* Open MCT is licensed under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
* http://www.apache.org/licenses/LICENSE-2.0.
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14+
* License for the specific language governing permissions and limitations
15+
* under the License.
16+
*
17+
* Open MCT includes source code licensed under additional open source
18+
* licenses. See the Open Source Licenses file (LICENSES.md) included with
19+
* this source code distribution or the Licensing information page available
20+
* at runtime from the About dialog for additional information.
21+
*****************************************************************************/
22+
23+
export const SEVERITY_CSS = {
24+
WATCH: 'is-event--yellow',
25+
WARNING: 'is-event--yellow',
26+
DISTRESS: 'is-event--red',
27+
CRITICAL: 'is-event--red',
28+
SEVERE: 'is-event--purple'
29+
};
30+
31+
const NOMINAL_SEVERITY = {
32+
cssClass: 'is-event--no-style',
33+
name: 'NOMINAL'
34+
};
35+
36+
/**
37+
* @typedef {Object} EvaluationResult
38+
* @property {string} cssClass CSS class information
39+
* @property {string} name a severity name
40+
*/
41+
export default class EventLimitProvider {
42+
constructor(openmct) {
43+
this.openmct = openmct;
44+
}
45+
46+
getLimitEvaluator(domainObject) {
47+
const self = this;
48+
49+
return {
50+
/**
51+
* Evaluates a telemetry datum for severity.
52+
*
53+
* @param {Datum} datum the telemetry datum from the historical or realtime plugin ({@link Datum})
54+
* @param {object} valueMetadata metadata about the telemetry datum
55+
*
56+
* @returns {EvaluationResult} ({@link EvaluationResult})
57+
*/
58+
evaluate: function (datum, valueMetadata) {
59+
// prevent applying the class to the tr, only to td
60+
if (!valueMetadata) {
61+
return;
62+
}
63+
64+
if (datum.severity in SEVERITY_CSS) {
65+
return self.getSeverity(datum, valueMetadata);
66+
}
67+
68+
return NOMINAL_SEVERITY;
69+
}
70+
};
71+
}
72+
getSeverity(datum, valueMetadata) {
73+
if (!valueMetadata) {
74+
return;
75+
}
76+
77+
const severityValue = datum.severity;
78+
79+
return {
80+
cssClass: SEVERITY_CSS[severityValue],
81+
name: severityValue
82+
};
83+
}
84+
85+
supportsLimits(domainObject) {
86+
return domainObject.type === 'eventGenerator';
87+
}
88+
}

example/eventGenerator/EventMetadataProvider.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,12 @@ class EventMetadataProvider {
4141
{
4242
key: 'message',
4343
name: 'Message',
44-
format: 'string'
44+
format: 'string',
45+
hints: {
46+
// this is used in the EventTimelineView to provide a title for the event
47+
// label can be changed to other properties for the title (e.g., the `name` property)
48+
label: 0
49+
}
4550
}
4651
]
4752
}

example/eventGenerator/EventTelemetryProvider.js

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,23 +24,35 @@
2424
* Module defining EventTelemetryProvider. Created by chacskaylo on 06/18/2015.
2525
*/
2626

27+
import { SEVERITY_CSS } from './EventLimitProvider.js';
2728
import messages from './transcript.json';
2829

30+
const DUR_MIN = 1000;
31+
const DUR_MAX = 10000;
2932
class EventTelemetryProvider {
3033
constructor() {
3134
this.defaultSize = 25;
3235
}
3336

3437
generateData(firstObservedTime, count, startTime, duration, name) {
3538
const millisecondsSinceStart = startTime - firstObservedTime;
36-
const utc = startTime + count * duration;
39+
const randStartDelay = Math.max(DUR_MIN, Math.random() * DUR_MAX);
40+
const utc = startTime + randStartDelay + count * duration;
3741
const ind = count % messages.length;
3842
const message = messages[ind] + ' - [' + millisecondsSinceStart + ']';
43+
// pick a random severity level + 1 for an undefined level so we can do nominal
44+
const severity =
45+
Math.random() > 0.4
46+
? Object.keys(SEVERITY_CSS)[
47+
Math.floor(Math.random() * Object.keys(SEVERITY_CSS).length + 1)
48+
]
49+
: undefined;
3950

4051
return {
4152
name,
4253
utc,
43-
message
54+
message,
55+
severity
4456
};
4557
}
4658

@@ -53,7 +65,7 @@ class EventTelemetryProvider {
5365
}
5466

5567
subscribe(domainObject, callback) {
56-
const duration = domainObject.telemetry.duration * 1000;
68+
const duration = domainObject.telemetry.duration * DUR_MIN;
5769
const firstObservedTime = Date.now();
5870
let count = 0;
5971

@@ -78,7 +90,7 @@ class EventTelemetryProvider {
7890
request(domainObject, options) {
7991
let start = options.start;
8092
const end = Math.min(Date.now(), options.end); // no future values
81-
const duration = domainObject.telemetry.duration * 1000;
93+
const duration = domainObject.telemetry.duration * DUR_MIN;
8294
const size = options.size ? options.size : this.defaultSize;
8395
const data = [];
8496
const firstObservedTime = options.start;

example/eventGenerator/plugin.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
* this source code distribution or the Licensing information page available
2020
* at runtime from the About dialog for additional information.
2121
*****************************************************************************/
22+
import EventLimitProvider from './EventLimitProvider.js';
2223
import EventMetadataProvider from './EventMetadataProvider.js';
2324
import EventTelemetryProvider from './EventTelemetryProvider.js';
2425
import EventWithAcknowledgeTelemetryProvider from './EventWithAcknowledgeTelemetryProvider.js';
@@ -54,5 +55,7 @@ export default function EventGeneratorPlugin(options) {
5455
});
5556

5657
openmct.telemetry.addProvider(new EventWithAcknowledgeTelemetryProvider());
58+
59+
openmct.telemetry.addProvider(new EventLimitProvider(openmct));
5760
};
5861
}

index.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,8 @@
113113
creatable: true
114114
})
115115
);
116-
openmct.install(openmct.plugins.Timeline());
116+
const timeLinePlugin = openmct.plugins.Timeline();
117+
openmct.install(timeLinePlugin);
117118
openmct.install(openmct.plugins.Hyperlink());
118119
openmct.install(openmct.plugins.UTCTimeSystem());
119120
openmct.install(
@@ -234,6 +235,7 @@
234235
openmct.install(openmct.plugins.Timelist());
235236
openmct.install(openmct.plugins.BarChart());
236237
openmct.install(openmct.plugins.ScatterPlot());
238+
openmct.install(openmct.plugins.EventTimestripPlugin(timeLinePlugin.extendedLinesBus));
237239
document.addEventListener('DOMContentLoaded', function () {
238240
openmct.start();
239241
});

src/api/telemetry/TelemetryAPI.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,11 +296,18 @@ export default class TelemetryAPI {
296296
* @returns {boolean} True if the object has numeric telemetry, false otherwise
297297
*/
298298
hasNumericTelemetry(domainObject) {
299-
if (!Object.prototype.hasOwnProperty.call(domainObject, 'telemetry')) {
299+
const hasTelemetry = this.openmct.telemetry.isTelemetryObject(domainObject);
300+
301+
if (!hasTelemetry) {
300302
return false;
301303
}
302304

303305
const metadata = this.openmct.telemetry.getMetadata(domainObject);
306+
307+
if (!metadata) {
308+
return false;
309+
}
310+
304311
const rangeValues = metadata.valuesForHints(['range']);
305312
const domains = metadata.valuesForHints(['domain']);
306313

src/api/tooltips/ToolTip.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,11 @@ import TooltipComponent from './components/TooltipComponent.vue';
2727

2828
class Tooltip extends EventEmitter {
2929
constructor(
30-
{ toolTipText, toolTipLocation, parentElement } = {
30+
{ toolTipText, toolTipLocation, parentElement, cssClasses } = {
3131
tooltipText: '',
3232
toolTipLocation: 'below',
33-
parentElement: null
33+
parentElement: null,
34+
cssClasses: []
3435
}
3536
) {
3637
super();
@@ -42,7 +43,8 @@ class Tooltip extends EventEmitter {
4243
provide: {
4344
toolTipText,
4445
toolTipLocation,
45-
parentElement
46+
parentElement,
47+
cssClasses
4648
},
4749
template: '<tooltip-component toolTipText="toolTipText"></tooltip-component>'
4850
});

src/api/tooltips/ToolTipAPI.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,11 @@ class TooltipAPI {
8080
* @property {string} tooltipText text to show in the tooltip
8181
* @property {TOOLTIP_LOCATIONS} tooltipLocation location to show the tooltip relative to the parentElement
8282
* @property {HTMLElement} parentElement reference to the DOM node we're adding the tooltip to
83+
* @property {Array} cssClasses css classes to use with the tool tip element
8384
*/
8485

8586
/**
86-
* Tooltips take an options object that consists of the string, tooltipLocation, and parentElement
87+
* Tooltips take an options object that consists of the string, tooltipLocation, a parentElement, and an array of cssClasses
8788
* @param {TooltipOptions} options
8889
*/
8990
tooltip(options) {

0 commit comments

Comments
 (0)