Skip to content
Open
66 changes: 37 additions & 29 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,18 +126,25 @@ script loaders are also supported.
<!DOCTYPE html>
<html>
<head>
<title>Open MCT</title>
<script src="dist/openmct.js"></script>
<title>Open MCT</title>
<script src="dist/openmct.js"></script>
<script>
openmct.install(openmct.plugins.LocalStorage());
openmct.install(openmct.plugins.MyItems());
openmct.install(openmct.plugins.UTCTimeSystem());
openmct.time.setTimeSystem('utc');
openmct.install(openmct.plugins.Espresso());

document.addEventListener('DOMContentLoaded', () => {
openmct.start();
})
</script>
</head>
<body>
<script>
openmct.install(openmct.plugins.LocalStorage());
openmct.install(openmct.plugins.MyItems());
openmct.install(openmct.plugins.UTCTimeSystem());
openmct.start();
</script>
<div id="app"></div>
</body>
</html>

```

The Open MCT library included above requires certain assets such as html
Expand All @@ -149,7 +156,7 @@ There are some plugins bundled with the application that provide UI,
persistence, and other default configuration which are necessary to be able to
do anything with the application initially. Any of these plugins can, in
principle, be replaced with a custom plugin. The included plugins are
documented in the [Included Plugins](#included-plugins) section.
documented in the [Included Plugins](#plugins) section.

## Types

Expand Down Expand Up @@ -424,24 +431,24 @@ var domainObject = {

## Telemetry API

The Open MCT telemetry API provides two main sets of interfaces-- one for
integrating telemetry data into Open MCT, and another for developing Open MCT
visualization plugins utilizing the telemetry API.
The Open MCT telemetry API provides two main sets of interfaces
1. For integrating telemetry data into Open MCT, and
2. For developing Open MCT visualization plugins utilizing the telemetry API.

The APIs for visualization plugins are still a work in progress and docs may
change at any time. However, the APIs for integrating telemetry metadata into
Open MCT are stable and documentation is included below.
The APIs for integrating telemetry metadata into Open MCT are stable and documentation is included below. However, the APIs for visualization plugins are still a work in progress and docs may change at any time.

### Integrating Telemetry Sources

There are two main tasks for integrating telemetry sources-- describing telemetry objects with relevant metadata, and then providing telemetry data for those objects. You'll use an [Object Provider](#object-providers) to provide objects with the necessary [Telemetry Metadata](#telemetry-metadata), and then register a [Telemetry Provider](#telemetry-providers) to retrieve telemetry data for those objects. Alternatively, you can register a telemetry metadata provider to provide the necessary telemetry metadata.
There are two main tasks for integrating telemetry sources
* Describing telemetry objects with relevant metadata. You'll use an [Object Provider](#object-providers) to provide objects with the necessary [Telemetry Metadata](#telemetry-metadata). Alternatively, you can register a telemetry metadata provider to provide the necessary telemetry metadata.
* Providing telemetry data for those objects. You'll register a [Telemetry Provider](#telemetry-providers) to retrieve telemetry data for those objects.

For a step-by-step guide to building a telemetry adapter, please see the
[Open MCT Tutorials](https://github.com/nasa/openmct-tutorial).

#### Telemetry Metadata

A telemetry object is a domain object with a telemetry property. To take an example from the tutorial, here is the telemetry object for the "fuel" measurement of the spacecraft:
A telemetry object is a domain object with a `telemetry` property. To take an example from the tutorial, here is the telemetry object for the "fuel" measurement of the spacecraft:

```json
{
Expand Down Expand Up @@ -478,23 +485,24 @@ A telemetry object is a domain object with a telemetry property. To take an exa
}
```

The most important part of the telemetry metadata is the `values` property-- this describes the attributes of telemetry datums (objects) that a telemetry provider returns. These descriptions must be provided for telemetry views to work properly.
The most important part of the telemetry metadata is the `values` property. This describes the attributes of telemetry datums (objects) that a telemetry provider returns. These descriptions must be provided for telemetry views to work properly.

##### Values

`telemetry.values` is an array of value description objects, which have the following fields:

attribute | type | flags | notes
--- | --- | --- | ---
`key` | string | required | unique identifier for this field.
`hints` | object | required | Hints allow views to intelligently select relevant attributes for display, and are required for most views to function. See section on "Value Hints" below.
`name` | string | optional | a human readable label for this field. If omitted, defaults to `key`.
`source` | string | optional | identifies the property of a datum where this value is stored. If omitted, defaults to `key`.
`format` | string | optional | a specific format identifier, mapping to a formatter. If omitted, uses a default formatter. For enumerations, use `enum`. For timestamps, use `utc` if you are using utc dates, otherwise use a key mapping to your custom date format.
`unit` | string | optional | the unit of this value, e.g. `km`, `seconds`, `parsecs`
`min` | number | optional | the minimum possible value of this measurement. Will be used by plots, gauges, etc to automatically set a min value.
`max` | number | optional | the maximum possible value of this measurement. Will be used by plots, gauges, etc to automatically set a max value.
`enumerations` | array | optional | for objects where `format` is `"enum"`, this array tracks all possible enumerations of the value. Each entry in this array is an object, with a `value` property that is the numerical value of the enumeration, and a `string` property that is the text value of the enumeration. ex: `{"value": 0, "string": "OFF"}`. If you use an enumerations array, `min` and `max` will be set automatically for you.
attribute | type | flags | notes
--- |---------|----------| ---
`key` | string | required | unique identifier for this field.
`hints` | object | required | Hints allow views to intelligently select relevant attributes for display, and are required for most views to function. See section on "Value Hints" below.
`name` | string | optional | a human readable label for this field. If omitted, defaults to `key`.
`source` | string | optional | identifies the property of a datum where this value is stored. If omitted, defaults to `key`.
`format` | string | optional | a specific format identifier, mapping to a formatter. If omitted, uses a default formatter. For enumerations, use `enum`. For timestamps, use `utc` if you are using utc dates, otherwise use a key mapping to your custom date format. For arrays use `number[]` or `string[]` See arrays below in the this table.
`unit` | string | optional | the unit of this value, e.g. `km`, `seconds`, `parsecs`
`min` | number | optional | the minimum possible value of this measurement. Will be used by plots, gauges, etc to automatically set a min value.
`max` | number | optional | the maximum possible value of this measurement. Will be used by plots, gauges, etc to automatically set a max value.
`enumerations` | array | optional | for objects where `format` is `"enum"`, this array tracks all possible enumerations of the value. Each entry in this array is an object, with a `value` property that is the numerical value of the enumeration, and a `string` property that is the text value of the enumeration. ex: `{"value": 0, "string": "OFF"}`. If you use an enumerations array, `min` and `max` will be set automatically for you.
`arrays` | string | optional | for objects where `format` is `"number[]" or "string[]"`. Will be used by plots, gauges, etc to automatically interpret values as arrays.

###### Value Hints

Expand Down
153 changes: 153 additions & 0 deletions e2e/tests/functional/plugins/plot/timeTicks.e2e.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { expect, test } from '@playwright/test';

import { createDomainObjectWithDefaults, setTimeConductorBounds } from '../../../../appActions.js';

test.describe('Time Tick Generation', () => {
// Test cases will go here
let sineWaveGeneratorObject;

test.beforeEach(async ({ page }) => {
// Open a browser, navigate to the main page, and wait until all networkevents to resolve
await page.goto('./', { waitUntil: 'domcontentloaded' });

sineWaveGeneratorObject = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator'
});

// Navigate to Sine Wave Generator
await page.goto(sineWaveGeneratorObject.url);
});

test('Plot time-series ticks are functionally correct over a period of 6 months, between two years', async ({
page
}) => {
const startDate = '2022-09-01';
const startTime = '22:00:00';
const endDate = '2023-03-01';
const endTime = '22:00:30';
await setTimeConductorBounds(page, { startDate, startTime, endDate, endTime });

await testYearTimeSeriesTicks(page);
});

test('Plot time-series ticks are functionally correct over a period of days', async ({
page
}) => {
const startDate = '2023-03-22';
const startTime = '00:00:00';
const endDate = '2023-04-20';
const endTime = '12:00:00';
await setTimeConductorBounds(page, { startDate, startTime, endDate, endTime });

await testDaysTimeSeriesTicks(page);
});

test('Plot time-series ticks are functionally correct over a period of hours', async ({
page
}) => {
const startDate = '2023-03-22';
const startTime = '01:15:00';
const endDate = '2023-03-22';
const endTime = '09:15:00';
await setTimeConductorBounds(page, { startDate, startTime, endDate, endTime });

await testHoursTimeSeriesTicks(page);
});

test('Plot time-series ticks are functionally correct over a period of minutes', async ({
page
}) => {
const startDate = '2023-03-22';
const startTime = '01:15:00';
const endDate = '2023-03-22';
const endTime = '01:35:00';
await setTimeConductorBounds(page, { startDate, startTime, endDate, endTime });

await testMinutesTimeSeriesTicks(page);
});

test('Plot time-series ticks are functionally correct over a period of seconds', async ({
page
}) => {
const startDate = '2023-03-22';
const startTime = '01:22:00';
const endDate = '2023-03-22';
const endTime = '01:23:00';
await setTimeConductorBounds(page, { startDate, startTime, endDate, endTime });

await testSecondsTimeSeriesTicks(page);
});
});

/**
* @param {import('@playwright/test').Page} page
*/
async function testYearTimeSeriesTicks(page) {
const xTicks = page.locator('.gl-plot-x-tick-label');
await expect(xTicks).toHaveCount(6);
await expect(xTicks.nth(0)).toHaveText('2022-09-01 22:00:00');
await expect(xTicks.nth(1)).toHaveText('2022-10-01 22:00:00');
await expect(xTicks.nth(2)).toHaveText('2022-11-01 22:00:00');
await expect(xTicks.nth(3)).toHaveText('2022-12-01 23:00:00');
await expect(xTicks.nth(4)).toHaveText('2023-01-01 23:00:00');
await expect(xTicks.nth(5)).toHaveText('2023-02-01 23:00:00');
}

async function testDaysTimeSeriesTicks(page) {
const xTicks = page.locator('.gl-plot-x-tick-label');
await expect(xTicks).toHaveCount(10);
await expect(xTicks.nth(0)).toHaveText('2023-03-24');
await expect(xTicks.nth(1)).toHaveText('2023-03-27');
await expect(xTicks.nth(2)).toHaveText('2023-03-30');
await expect(xTicks.nth(3)).toHaveText('2023-04-02');
await expect(xTicks.nth(4)).toHaveText('2023-04-05');
await expect(xTicks.nth(5)).toHaveText('2023-04-08');
await expect(xTicks.nth(6)).toHaveText('2023-04-11');
await expect(xTicks.nth(7)).toHaveText('2023-04-14');
await expect(xTicks.nth(8)).toHaveText('2023-04-17');
await expect(xTicks.nth(9)).toHaveText('2023-04-20');
}

async function testHoursTimeSeriesTicks(page) {
const xTicks = page.locator('.gl-plot-x-tick-label');
await expect(xTicks).toHaveCount(8);
await expect(xTicks.nth(0)).toHaveText('02:00:00');
await expect(xTicks.nth(1)).toHaveText('03:00:00');
await expect(xTicks.nth(2)).toHaveText('04:00:00');
await expect(xTicks.nth(3)).toHaveText('05:00:00');
await expect(xTicks.nth(4)).toHaveText('06:00:00');
await expect(xTicks.nth(5)).toHaveText('07:00:00');
await expect(xTicks.nth(6)).toHaveText('08:00:00');
await expect(xTicks.nth(7)).toHaveText('09:00:00');
}

async function testMinutesTimeSeriesTicks(page) {
const xTicks = page.locator('.gl-plot-x-tick-label');
await expect(xTicks).toHaveCount(10);
await expect(xTicks.nth(0)).toHaveText('01:16:00');
await expect(xTicks.nth(1)).toHaveText('01:18:00');
await expect(xTicks.nth(2)).toHaveText('01:20:00');
await expect(xTicks.nth(3)).toHaveText('01:22:00');
await expect(xTicks.nth(4)).toHaveText('01:24:00');
await expect(xTicks.nth(5)).toHaveText('01:26:00');
await expect(xTicks.nth(6)).toHaveText('01:28:00');
await expect(xTicks.nth(7)).toHaveText('01:30:00');
await expect(xTicks.nth(8)).toHaveText('01:32:00');
await expect(xTicks.nth(9)).toHaveText('01:34:00');
}

async function testSecondsTimeSeriesTicks(page) {
const xTicks = page.locator('.gl-plot-x-tick-label');
await expect(xTicks).toHaveCount(11);
await expect(xTicks.nth(0)).toHaveText('01:22:00');
await expect(xTicks.nth(1)).toHaveText('01:22:06');
await expect(xTicks.nth(2)).toHaveText('01:22:12');
await expect(xTicks.nth(3)).toHaveText('01:22:18');
await expect(xTicks.nth(4)).toHaveText('01:22:24');
await expect(xTicks.nth(5)).toHaveText('01:22:30');
await expect(xTicks.nth(6)).toHaveText('01:22:36');
await expect(xTicks.nth(7)).toHaveText('01:22:42');
await expect(xTicks.nth(8)).toHaveText('01:22:48');
await expect(xTicks.nth(9)).toHaveText('01:22:54');
await expect(xTicks.nth(10)).toHaveText('01:23:00');
}
10 changes: 9 additions & 1 deletion src/plugins/plot/MctPlot.vue
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
v-show="gridLines && !options.compact"
:axis-type="'xAxis'"
:position="'right'"
:is-utc="isUtc"
/>

<MctTicks
Expand Down Expand Up @@ -295,7 +296,8 @@ export default {
yAxes: [],
hiddenYAxisIds: [],
yAxisListWithRange: [],
config: {}
config: {},
isUtc: this.openmct.time.getTimeSystem().isUTCBased
};
},
computed: {
Expand Down Expand Up @@ -542,12 +544,14 @@ export default {
this.updateMode();
this.updateDisplayBounds(this.timeContext.getBounds());
this.timeContext.on('modeChanged', this.updateMode);
this.timeContext.on('timeSystemChanged', this.setUtc);
this.timeContext.on('boundsChanged', this.updateDisplayBounds);
this.synchronized(true);
},
stopFollowingTimeContext() {
if (this.timeContext) {
this.timeContext.off('modeChanged', this.updateMode);
this.timeContext.off('timeSystemChanged', this.setUtc);
this.timeContext.off('boundsChanged', this.updateDisplayBounds);
}
},
Expand Down Expand Up @@ -768,6 +772,10 @@ export default {
this.isRealTime = this.timeContext.isRealTime();
},

setUtc(timeSystem) {
this.isUtc = timeSystem.isUTCBased;
},

/**
* Track latest display bounds. Forces update when not receiving ticks.
*/
Expand Down
16 changes: 13 additions & 3 deletions src/plugins/plot/MctTicks.vue
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ import { inject } from 'vue';
import { useAlignment } from '../../ui/composables/alignmentContext.js';
import configStore from './configuration/ConfigStore.js';
import eventHelpers from './lib/eventHelpers.js';
import { getFormattedTicks, getLogTicks, ticks } from './tickUtils.js';
import { getFormattedTicks, getLogTicks, getTimeTicks, ticks } from './tickUtils.js';

const SECONDARY_TICK_NUMBER = 2;

Expand All @@ -96,7 +96,7 @@ export default {
tickCount: {
type: Number,
default() {
return 6;
return 10;
}
},
axisId: {
Expand All @@ -105,6 +105,12 @@ export default {
return null;
}
},
isUtc: {
type: Boolean,
default() {
return false;
}
},
position: {
required: true,
type: String,
Expand All @@ -128,7 +134,9 @@ export default {
},
data() {
return {
ticks: []
ticks: [],
interval: undefined,
min: undefined
};
},
mounted() {
Expand Down Expand Up @@ -219,6 +227,8 @@ export default {

if (this.axisType === 'yAxis' && this.axis.get('logMode')) {
return getLogTicks(range.min, range.max, number, SECONDARY_TICK_NUMBER);
} else if (this.isUtc) {
return getTimeTicks(range.min, range.max, number);
} else {
return ticks(range.min, range.max, number);
}
Expand Down
6 changes: 4 additions & 2 deletions src/plugins/plot/axis/XAxis.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

<template>
<div v-if="loaded" class="gl-plot-axis-area gl-plot-x has-local-controls">
<MctTicks :axis-type="'xAxis'" :position="'left'" />
<MctTicks :axis-type="'xAxis'" :position="'left'" :is-utc="isUtc" />

<div class="gl-plot-label gl-plot-x-label" :class="{ 'icon-gear': isEnabledXKeyToggle() }">
{{ xAxisLabel }}
Expand Down Expand Up @@ -65,7 +65,8 @@ export default {
xKeyOptions: [],
xAxis: {},
loaded: false,
xAxisLabel: ''
xAxisLabel: '',
isUtc: this.openmct.time.getTimeSystem().isUTCBased
};
},
mounted() {
Expand Down Expand Up @@ -119,6 +120,7 @@ export default {
this.xAxis.resetSeries();
this.setUpXAxisOptions();
}
this.isUtc = timeSystem.isUTCBased;
},
setUpXAxisOptions() {
const xAxisKey = this.xAxis.get('key');
Expand Down
Loading
Loading