Fix stacked plots legend (#6199)

* Add listeners to remove stacked plot series and make keys unique

* don't add overlay plots to stacked plot legends

* Ensure series colors are drawn correctly in the plot legend

* Remove legend from mct plot. Remove series reactivity from stackd plot and add them to the legend instead.

* Clean up stacked plots so that the plot legend needs fewer props
Also make sure that plot selection inside a stacked plot works - this had regressed due to plot annotations

* Fix console error in plot elements pool and plot legend - reset arrays to empty
* Ensure color in the y axis swatch updates correctly

* Fix small issues with removing objects from STacked plots

* Fix selection for annotations and also select stacked plot child items

* fix notebook tagging

* remove unused annotation editor and change selection to single object

* remove reference to deleted css

* fix e2e tests

* Fix small typos into the selection context for Notebooks.

* Add a typ that identifies that an annotation selection is coming from a search result

---------

Co-authored-by: Scott Bell <scott@traclabs.com>
This commit is contained in:
Shefali Joshi 2023-02-01 10:14:02 -08:00 committed by GitHub
parent 393c801426
commit f570424357
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 416 additions and 384 deletions

View File

@ -41,6 +41,7 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => {
});
test('Inspect Notebook Entry Network Requests', async ({ page }) => {
await page.getByText('Annotations').click();
// Expand sidebar
await page.locator('.c-notebook__toggle-nav-button').click();
@ -162,20 +163,20 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => {
await page.locator('[aria-label="Notebook Entry Input"] >> nth=2').press('Enter');
// Add three tags
await page.hover(`button:has-text("Add Tag") >> nth=2`);
await page.locator(`button:has-text("Add Tag") >> nth=2`).click();
await page.hover(`button:has-text("Add Tag")`);
await page.locator(`button:has-text("Add Tag")`).click();
await page.locator('[placeholder="Type to select tag"]').click();
await page.locator('[aria-label="Autocomplete Options"] >> text=Science').click();
await page.waitForSelector('[aria-label="Tag"]:has-text("Science")');
await page.hover(`button:has-text("Add Tag") >> nth=2`);
await page.locator(`button:has-text("Add Tag") >> nth=2`).click();
await page.hover(`button:has-text("Add Tag")`);
await page.locator(`button:has-text("Add Tag")`).click();
await page.locator('[placeholder="Type to select tag"]').click();
await page.locator('[aria-label="Autocomplete Options"] >> text=Drilling').click();
await page.waitForSelector('[aria-label="Tag"]:has-text("Drilling")');
await page.hover(`button:has-text("Add Tag") >> nth=2`);
await page.locator(`button:has-text("Add Tag") >> nth=2`).click();
await page.hover(`button:has-text("Add Tag")`);
await page.locator(`button:has-text("Add Tag")`).click();
await page.locator('[placeholder="Type to select tag"]').click();
await page.locator('[aria-label="Autocomplete Options"] >> text=Driving').click();
await page.waitForSelector('[aria-label="Tag"]:has-text("Driving")');
@ -231,6 +232,7 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => {
type: 'issue',
description: 'https://github.com/akhenry/openmct-yamcs/issues/69'
});
await page.getByText('Annotations').click();
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
await page.locator('[aria-label="Notebook Entry Input"]').click();
await page.locator('[aria-label="Notebook Entry Input"]').fill(`First Entry`);

View File

@ -381,7 +381,7 @@ export default {
});
},
updateSelection(selection) {
if (selection?.[0]?.[1]?.context?.targetDetails?.entryId === undefined) {
if (selection?.[0]?.[0]?.context?.targetDetails?.entryId === undefined) {
this.selectedEntryId = '';
}
},

View File

@ -472,16 +472,11 @@ export default {
targetDomainObjects[keyString] = this.domainObject;
this.openmct.selection.select(
[
{
element: this.openmct.layout.$refs.browseObject.$el,
context: {
item: this.domainObject
}
},
{
element: event.currentTarget,
context: {
type: 'notebook-entry-selection',
item: this.domainObject,
targetDetails,
targetDomainObjects,
annotations: this.notebookAnnotations,

View File

@ -23,16 +23,8 @@
<div
v-if="loaded"
class="gl-plot"
:class="[plotLegendExpandedStateClass, plotLegendPositionClass]"
>
<plot-legend
v-if="!isNestedWithinAStackedPlot"
:cursor-locked="!!lockHighlightPoint"
:series="seriesModels"
:highlights="highlights"
:legend="legend"
@legendHoverChanged="legendHoverChanged"
/>
<slot></slot>
<div class="plot-wrapper-axis-and-display-area flex-elem grows">
<div
v-if="seriesModels.length"
@ -94,7 +86,6 @@
:highlights="highlights"
:annotated-points="annotatedPoints"
:annotation-selections="annotationSelections"
:show-limit-line-labels="showLimitLineLabels"
:hidden-y-axis-ids="hiddenYAxisIds"
:annotation-viewing-and-editing-allowed="annotationViewingAndEditingAllowed"
@plotReinitializeCanvas="initCanvas"
@ -217,7 +208,6 @@ import LinearScale from "./LinearScale";
import PlotConfigurationModel from './configuration/PlotConfigurationModel';
import configStore from './configuration/ConfigStore';
import PlotLegend from "./legend/PlotLegend.vue";
import MctTicks from "./MctTicks.vue";
import MctChart from "./chart/MctChart.vue";
import XAxis from "./axis/XAxis.vue";
@ -232,7 +222,6 @@ export default {
components: {
XAxis,
YAxis,
PlotLegend,
MctTicks,
MctChart
},
@ -296,7 +285,6 @@ export default {
isRealTime: this.openmct.time.clock() !== undefined,
loaded: false,
isTimeOutOfSync: false,
showLimitLineLabels: this.limitLineLabels,
isFrozenOnMouseDown: false,
cursorGuide: this.initCursorGuide,
gridLines: this.initGridLines,
@ -334,23 +322,9 @@ export default {
return this.config.xAxis.get('frozen') === true && this.config.yAxis.get('frozen') === true;
},
annotationViewingAndEditingAllowed() {
// only allow annotations viewing/editing if plot is paused or in fixed time mode
// only allow annotations viewing/editing if plot is paused or in fixed time mode
return this.isFrozen || !this.isRealTime;
},
plotLegendPositionClass() {
return !this.isNestedWithinAStackedPlot ? `plot-legend-${this.config.legend.get('position')}` : '';
},
plotLegendExpandedStateClass() {
if (this.isNestedWithinAStackedPlot) {
return '';
}
if (this.config.legend.get('expanded')) {
return 'plot-legend-expanded';
} else {
return 'plot-legend-collapsed';
}
},
plotLeftTickWidth() {
let leftTickWidth = 0;
this.yAxes.forEach((yAxis) => {
@ -365,12 +339,6 @@ export default {
}
},
watch: {
limitLineLabels: {
handler(limitLineLabels) {
this.legendHoverChanged(limitLineLabels);
},
deep: true
},
initGridLines(newGridLines) {
this.gridLines = newGridLines;
},
@ -406,8 +374,7 @@ export default {
}));
}
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.$emit('configLoaded', configId);
this.$emit('configLoaded', true);
this.listenTo(this.config.series, 'add', this.addSeries, this);
this.listenTo(this.config.series, 'remove', this.removeSeries, this);
@ -439,15 +406,20 @@ export default {
methods: {
updateSelection(selection) {
const selectionContext = selection?.[0]?.[0]?.context?.item;
if (!selectionContext
|| this.openmct.objects.areIdsEqual(selectionContext.identifier, this.domainObject.identifier)) {
// Selection changed, but it's us, so ignoring it
// on clicking on a search result we highlight the annotation and zoom - we know it's an annotation result when isAnnotationSearchResult === true
// We shouldn't zoom when we're selecting existing annotations to view them or creating new annotations.
const selectionType = selection?.[0]?.[0]?.context?.type;
const validSelectionTypes = ['clicked-on-plot-selection', 'plot-annotation-search-result'];
const isAnnotationSearchResult = selectionType === 'plot-annotation-search-result';
if (!validSelectionTypes.includes(selectionType)) {
// wrong type of selection
return;
}
const selectionType = selection?.[0]?.[1]?.context?.type;
if (selectionType !== 'plot-points-selection') {
// wrong type of selection
if (selectionContext
&& (!isAnnotationSearchResult)
&& this.openmct.objects.areIdsEqual(selectionContext.identifier, this.domainObject.identifier)) {
return;
}
@ -460,7 +432,18 @@ export default {
return;
}
const selectedAnnotations = selection?.[0]?.[1]?.context?.annotations;
const selectedAnnotations = selection?.[0]?.[0]?.context?.annotations;
//This section is only for the annotations search results entry to displaying annotations
if (isAnnotationSearchResult) {
this.showAnnotationsFromSearchResults(selectedAnnotations);
}
//This section is common to all entry points for annotation display
this.prepareExistingAnnotationSelection(selectedAnnotations);
},
showAnnotationsFromSearchResults(selectedAnnotations) {
//Start section
if (selectedAnnotations?.length) {
// just use first annotation
const boundingBoxes = Object.values(selectedAnnotations[0].targets);
@ -494,10 +477,9 @@ export default {
min: minY,
max: maxY
});
//Zoom out just a touch so that the highlighted section for annotations doesn't take over the whole view - which is not a nice look.
this.zoom('out', 0.2);
}
this.prepareExistingAnnotationSelection(selectedAnnotations);
},
handleKeyDown(event) {
if (event.key === 'Alt') {
@ -688,9 +670,15 @@ export default {
series.reset();
});
},
shareCommonParent(domainObjectToFind) {
return false;
},
compositionPathContainsId(domainObjectToFind) {
if (!domainObjectToFind.composition) {
return false;
}
compositionPathContainsId(domainObjectToClear) {
return domainObjectToClear.composition.some((compositionIdentifier) => {
return domainObjectToFind.composition.some((compositionIdentifier) => {
return this.openmct.objects.areIdsEqual(compositionIdentifier, this.domainObject.identifier);
});
},
@ -1044,8 +1032,6 @@ export default {
highlightValues(point) {
this.highlightPoint = point;
// TODO: used in StackedPlotController
this.$emit('plotHighlightUpdate', point);
if (this.lockHighlightPoint) {
return;
}
@ -1157,7 +1143,7 @@ export default {
endPixels: this.positionOverElement,
start: this.positionOverPlot,
end: this.positionOverPlot,
color: [1, 1, 1, 0.5]
color: [1, 1, 1, 0.25]
};
if (annotationEvent) {
this.marquee.annotationEvent = true;
@ -1168,13 +1154,21 @@ export default {
}
},
selectNearbyAnnotations(event) {
// need to stop propagation right away to prevent selecting the plot itself
event.stopPropagation();
if (!this.annotationViewingAndEditingAllowed || this.annotationSelections.length) {
return;
}
const nearbyAnnotations = this.gatherNearbyAnnotations();
if (!nearbyAnnotations.length) {
const emptySelection = this.createPathSelection();
this.openmct.selection.select(emptySelection, true);
// should show plot itself if we didn't find any annotations
return;
}
const { targetDomainObjects, targetDetails } = this.prepareExistingAnnotationSelection(nearbyAnnotations);
this.selectPlotAnnotations({
targetDetails,
@ -1182,33 +1176,50 @@ export default {
annotations: nearbyAnnotations
});
},
createPathSelection() {
let selection = [];
this.path.forEach((pathObject, index) => {
selection.push({
element: this.openmct.layout.$refs.browseObject.$el,
context: {
item: pathObject
}
});
});
return selection;
},
selectPlotAnnotations({targetDetails, targetDomainObjects, annotations}) {
const selection =
[
{
element: this.openmct.layout.$refs.browseObject.$el,
context: {
item: this.domainObject
}
},
{
element: this.$el,
context: {
type: 'plot-points-selection',
targetDetails,
targetDomainObjects,
annotations,
annotationType: this.openmct.annotation.ANNOTATION_TYPES.PLOT_SPATIAL,
onAnnotationChange: this.onAnnotationChange
}
}
];
const annotationContext = {
type: 'clicked-on-plot-selection',
targetDetails,
targetDomainObjects,
annotations,
annotationType: this.openmct.annotation.ANNOTATION_TYPES.PLOT_SPATIAL,
onAnnotationChange: this.onAnnotationChange
};
const selection = this.createPathSelection();
if (selection.length && this.openmct.objects.areIdsEqual(selection[0].context.item.identifier, this.domainObject.identifier)) {
selection[0].context = {
...selection[0].context,
...annotationContext
};
} else {
selection.unshift({
element: this.$el,
context: {
item: this.domainObject,
...annotationContext
}
});
}
this.openmct.selection.select(selection, true);
},
selectNewPlotAnnotations(boundingBoxPerYAxis, pointsInBox, event) {
let targetDomainObjects = {};
let targetDetails = {};
let annotations = {};
let annotations = [];
pointsInBox.forEach(pointInBox => {
if (pointInBox.length) {
const seriesID = pointInBox[0].series.keyString;
@ -1752,9 +1763,6 @@ export default {
this.config.series.models.forEach(this.loadSeriesData, this);
}
},
legendHoverChanged(data) {
this.showLimitLineLabels = data;
},
toggleCursorGuide() {
this.cursorGuide = !this.cursorGuide;
this.$emit('cursorGuide', this.cursorGuide);

View File

@ -36,12 +36,26 @@
:model="{progressPerc: undefined}"
/>
<mct-plot
:class="[plotLegendExpandedStateClass, plotLegendPositionClass]"
:init-grid-lines="gridLines"
:init-cursor-guide="cursorGuide"
:options="options"
:limit-line-labels="limitLineLabels"
@loadingUpdated="loadingUpdated"
@statusUpdated="setStatus"
/>
@configLoaded="updateReady"
@lockHighlightPoint="lockHighlightPointUpdated"
@highlights="highlightsUpdated"
>
<plot-legend
v-if="configReady"
:cursor-locked="lockHighlightPoint"
:highlights="highlights"
@legendHoverChanged="legendHoverChanged"
@expanded="updateExpanded"
@position="updatePosition"
/>
</mct-plot>
</div>
</div>
</template>
@ -50,13 +64,15 @@
import eventHelpers from './lib/eventHelpers';
import ImageExporter from '../../exporters/ImageExporter';
import MctPlot from './MctPlot.vue';
import PlotLegend from "./legend/PlotLegend.vue";
import ProgressBar from "../../ui/components/ProgressBar.vue";
import StalenessUtils from '@/utils/staleness';
export default {
components: {
MctPlot,
ProgressBar
ProgressBar,
PlotLegend
},
inject: ['openmct', 'domainObject', 'path'],
props: {
@ -77,7 +93,13 @@ export default {
gridLines: !this.options.compact,
loading: false,
status: '',
staleObjects: []
staleObjects: [],
limitLineLabels: undefined,
lockHighlightPoint: false,
highlights: [],
expanded: false,
position: undefined,
configReady: false
};
},
computed: {
@ -87,6 +109,16 @@ export default {
}
return '';
},
plotLegendPositionClass() {
return this.position ? `plot-legend-${this.position}` : '';
},
plotLegendExpandedStateClass() {
if (this.expanded) {
return 'plot-legend-expanded';
} else {
return 'plot-legend-collapsed';
}
}
},
mounted() {
@ -183,6 +215,24 @@ export default {
exportPNG: this.exportPNG,
exportJPG: this.exportJPG
};
},
lockHighlightPointUpdated(data) {
this.lockHighlightPoint = data;
},
highlightsUpdated(data) {
this.highlights = data;
},
legendHoverChanged(data) {
this.limitLineLabels = data;
},
updateExpanded(expanded) {
this.expanded = expanded;
},
updatePosition(position) {
this.position = position;
},
updateReady(ready) {
this.configReady = ready;
}
}
};

View File

@ -202,6 +202,7 @@ export default {
}
this.listenTo(series, 'change:yAxisId', this.addOrRemoveSeries.bind(this, series), this);
this.listenTo(series, 'change:color', this.updateSeriesColors.bind(this, series), this);
},
removeSeries(plotSeries) {
const seriesIndex = this.seriesModels.findIndex(model => this.openmct.objects.areIdsEqual(model.get('identifier'), plotSeries.get('identifier')));
@ -216,6 +217,9 @@ export default {
return model.get('yKey') === this.seriesModels[0].get('yKey');
});
this.singleSeries = this.seriesModels.length === 1;
this.updateSeriesColors();
},
updateSeriesColors() {
this.seriesColors = this.seriesModels.map(model => {
return model.get('color').asHexString();
});

View File

@ -533,7 +533,6 @@ export default {
},
updateLimitsAndDraw() {
this.drawLimitLines();
this.scheduleDraw();
},
scheduleDraw() {
if (!this.drawScheduled) {

View File

@ -67,6 +67,10 @@ export default class SeriesCollection extends Collection {
}, this);
}
watchTelemetryContainer(domainObject) {
if (domainObject.type === 'telemetry.plot.stacked') {
return;
}
const composition = this.openmct.composition.get(domainObject);
this.listenTo(composition, 'add', this.addTelemetryObject, this);
this.listenTo(composition, 'remove', this.removeTelemetryObject, this);

View File

@ -93,7 +93,7 @@
</ul>
</div>
<div
v-if="plotSeries.length && (isStackedPlotObject || !isNestedWithinAStackedPlot)"
v-if="isStackedPlotObject || !isNestedWithinAStackedPlot"
class="grid-properties"
>
<ul
@ -190,10 +190,13 @@ export default {
mounted() {
eventHelpers.extend(this);
this.config = this.getConfig();
this.initYAxesConfiguration();
if (!this.isStackedPlotObject) {
this.initYAxesConfiguration();
this.registerListeners();
} else {
this.initLegendConfiguration();
}
this.registerListeners();
this.initLegendConfiguration();
this.loaded = true;
},
@ -245,9 +248,9 @@ export default {
}
},
getConfig() {
this.configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
return configStore.get(this.configId);
return configStore.get(configId);
},
registerListeners() {
if (this.config) {

View File

@ -53,7 +53,6 @@
>
<h2 title="Legend options">Legend</h2>
<legend-form
v-if="plotSeries.length"
class="grid-properties"
:legend="config.legend"
/>
@ -97,20 +96,23 @@ export default {
mounted() {
eventHelpers.extend(this);
this.config = this.getConfig();
this.yAxes = [{
id: this.config.yAxis.id,
seriesCount: 0
}];
if (this.config.additionalYAxes) {
this.yAxes = this.yAxes.concat(this.config.additionalYAxes.map(yAxis => {
return {
id: yAxis.id,
seriesCount: 0
};
}));
if (!this.isStackedPlotObject) {
this.yAxes = [{
id: this.config.yAxis.id,
seriesCount: 0
}];
if (this.config.additionalYAxes) {
this.yAxes = this.yAxes.concat(this.config.additionalYAxes.map(yAxis => {
return {
id: yAxis.id,
seriesCount: 0
};
}));
}
this.registerListeners();
}
this.registerListeners();
this.loaded = true;
},
beforeDestroy() {

View File

@ -12,11 +12,12 @@ export default function PlotsInspectorViewProvider(openmct) {
}
let object = selection[0][0].context.item;
let parent = selection[0].length > 1 && selection[0][1].context.item;
const isOverlayPlotObject = object && object.type === 'telemetry.plot.overlay';
const isStackedPlotObject = object && object.type === 'telemetry.plot.stacked';
const isParentStackedPlotObject = parent && parent.type === 'telemetry.plot.stacked';
return isStackedPlotObject || isOverlayPlotObject;
return isOverlayPlotObject || isParentStackedPlotObject;
},
view: function (selection) {
let component;

View File

@ -12,12 +12,10 @@ export default function StackedPlotsInspectorViewProvider(openmct) {
}
const object = selection[0][0].context.item;
const parent = selection[0].length > 1 && selection[0][1].context.item;
const isOverlayPlotObject = object && object.type === 'telemetry.plot.overlay';
const isParentStackedPlotObject = parent && parent.type === 'telemetry.plot.stacked';
const isStackedPlotObject = object && object.type === 'telemetry.plot.stacked';
return !isOverlayPlotObject && isParentStackedPlotObject;
return isStackedPlotObject;
},
view: function (selection) {
let component;

View File

@ -49,10 +49,10 @@
title="Cursor is point locked. Click anywhere in the plot to unlock."
></div>
<plot-legend-item-collapsed
v-for="(seriesObject, seriesIndex) in series"
:key="`${seriesObject.keyString}-${seriesIndex}`"
v-for="(seriesObject, seriesIndex) in seriesModels"
:key="`${seriesObject.keyString}-${seriesIndex}-collapsed`"
:highlights="highlights"
:value-to-show-when-collapsed="legend.get('valueToShowWhenCollapsed')"
:value-to-show-when-collapsed="valueToShowWhenCollapsed"
:series-object="seriesObject"
@legendHoverChanged="legendHoverChanged"
/>
@ -95,11 +95,10 @@
</thead>
<tbody>
<plot-legend-item-expanded
v-for="(seriesObject, seriesIndex) in series"
v-for="(seriesObject, seriesIndex) in seriesModels"
:key="`${seriesObject.keyString}-${seriesIndex}-expanded`"
:series-object="seriesObject"
:highlights="highlights"
:legend="legend"
@legendHoverChanged="legendHoverChanged"
/>
</tbody>
@ -111,6 +110,9 @@
<script>
import PlotLegendItemCollapsed from "./PlotLegendItemCollapsed.vue";
import PlotLegendItemExpanded from "./PlotLegendItemExpanded.vue";
import configStore from "../configuration/ConfigStore";
import eventHelpers from "../lib/eventHelpers";
export default {
components: {
PlotLegendItemExpanded,
@ -124,57 +126,113 @@ export default {
return false;
}
},
series: {
type: Array,
default() {
return [];
}
},
highlights: {
type: Array,
default() {
return [];
}
},
legend: {
type: Object,
default() {
return {};
}
}
},
data() {
return {
isLegendExpanded: this.legend.get('expanded') === true
isLegendExpanded: false,
seriesModels: [],
loaded: false
};
},
computed: {
showUnitsWhenExpanded() {
return this.legend.get('showUnitsWhenExpanded') === true;
return this.loaded && this.legend.get('showUnitsWhenExpanded') === true;
},
showMinimumWhenExpanded() {
return this.legend.get('showMinimumWhenExpanded') === true;
return this.loaded && this.legend.get('showMinimumWhenExpanded') === true;
},
showMaximumWhenExpanded() {
return this.legend.get('showMaximumWhenExpanded') === true;
return this.loaded && this.legend.get('showMaximumWhenExpanded') === true;
},
showValueWhenExpanded() {
return this.legend.get('showValueWhenExpanded') === true;
return this.loaded && this.legend.get('showValueWhenExpanded') === true;
},
showTimestampWhenExpanded() {
return this.legend.get('showTimestampWhenExpanded') === true;
return this.loaded && this.legend.get('showTimestampWhenExpanded') === true;
},
isLegendHidden() {
return this.legend.get('hideLegendWhenSmall') === true;
return this.loaded && this.legend.get('hideLegendWhenSmall') === true;
},
valueToShowWhenCollapsed() {
return this.loaded && this.legend.get('valueToShowWhenCollapsed');
}
},
mounted() {
this.seriesModels = [];
eventHelpers.extend(this);
this.config = this.getConfig();
this.legend = this.config.legend;
this.loaded = true;
this.isLegendExpanded = this.legend.get('expanded') === true;
this.listenTo(this.config.legend, 'change:position', this.updatePosition, this);
this.updatePosition();
this.initialize();
},
beforeDestroy() {
if (this.objectComposition) {
this.objectComposition.off('add', this.addTelemetryObject);
this.objectComposition.off('remove', this.removeTelemetryObject);
}
this.stopListening();
},
methods: {
initialize() {
if (this.domainObject.type === 'telemetry.plot.stacked') {
this.objectComposition = this.openmct.composition.get(this.domainObject);
this.objectComposition.on('add', this.addTelemetryObject);
this.objectComposition.on('remove', this.removeTelemetryObject);
this.objectComposition.load();
} else {
this.registerListeners(this.config);
}
},
getConfig() {
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
return configStore.get(configId);
},
addTelemetryObject(object) {
//get the config for each child
const configId = this.openmct.objects.makeKeyString(object.identifier);
const config = configStore.get(configId);
if (config) {
this.registerListeners(config);
}
},
registerListeners(config) {
//listen to any changes to the telemetry endpoints that are associated with the child
this.listenTo(config.series, 'add', this.addSeries, this);
this.listenTo(config.series, 'remove', this.removeSeries, this);
config.series.forEach(this.addSeries, this);
},
addSeries(series) {
this.$set(this.seriesModels, this.seriesModels.length, series);
},
removeSeries(plotSeries) {
this.stopListening(plotSeries);
const seriesIndex = this.seriesModels.findIndex(series => series.keyString === plotSeries.keyString);
this.seriesModels.splice(seriesIndex, 1);
},
expandLegend() {
this.isLegendExpanded = !this.isLegendExpanded;
this.legend.set('expanded', this.isLegendExpanded);
this.$emit('expanded', this.isLegendExpanded);
},
legendHoverChanged(data) {
this.$emit('legendHoverChanged', data);
},
updatePosition() {
this.$emit('position', this.legend.get('position'));
}
}
};

View File

@ -57,15 +57,12 @@
import {getLimitClass} from "@/plugins/plot/chart/limitUtil";
import eventHelpers from "../lib/eventHelpers";
import stalenessMixin from '@/ui/mixins/staleness-mixin';
import configStore from "../configuration/ConfigStore";
export default {
mixins: [stalenessMixin],
inject: ['openmct', 'domainObject'],
props: {
valueToShowWhenCollapsed: {
type: String,
required: true
},
seriesObject: {
type: Object,
required: true,
@ -88,10 +85,14 @@ export default {
formattedYValue: '',
formattedXValue: '',
mctLimitStateClass: '',
formattedYValueFromStats: ''
formattedYValueFromStats: '',
loaded: false
};
},
computed: {
valueToShowWhenCollapsed() {
return this.loaded ? this.legend.get('valueToShowWhenCollapsed') : [];
},
valueToDisplayWhenCollapsedClass() {
return `value-to-display-${ this.valueToShowWhenCollapsed }`;
},
@ -109,6 +110,9 @@ export default {
},
mounted() {
eventHelpers.extend(this);
this.config = this.getConfig();
this.legend = this.config.legend;
this.loaded = true;
this.listenTo(this.seriesObject, 'change:color', (newColor) => {
this.updateColor(newColor);
}, this);
@ -122,8 +126,13 @@ export default {
this.stopListening();
},
methods: {
getConfig() {
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
return configStore.get(configId);
},
initialize(highlightedObject) {
const seriesObject = highlightedObject ? highlightedObject.series : this.seriesObject;
const seriesObject = highlightedObject?.series || this.seriesObject;
this.isMissing = seriesObject.domainObject.status === 'missing';
this.colorAsHexString = seriesObject.get('color').asHexString();

View File

@ -83,6 +83,7 @@
import {getLimitClass} from "@/plugins/plot/chart/limitUtil";
import eventHelpers from "@/plugins/plot/lib/eventHelpers";
import stalenessMixin from '@/ui/mixins/staleness-mixin';
import configStore from "../configuration/ConfigStore";
export default {
mixins: [stalenessMixin],
@ -100,10 +101,6 @@ export default {
default() {
return [];
}
},
legend: {
type: Object,
required: true
}
},
data() {
@ -116,24 +113,25 @@ export default {
formattedXValue: '',
formattedMinY: '',
formattedMaxY: '',
mctLimitStateClass: ''
mctLimitStateClass: '',
loaded: false
};
},
computed: {
showUnitsWhenExpanded() {
return this.legend.get('showUnitsWhenExpanded') === true;
return this.loaded && this.legend.get('showUnitsWhenExpanded') === true;
},
showMinimumWhenExpanded() {
return this.legend.get('showMinimumWhenExpanded') === true;
return this.loaded && this.legend.get('showMinimumWhenExpanded') === true;
},
showMaximumWhenExpanded() {
return this.legend.get('showMaximumWhenExpanded') === true;
return this.loaded && this.legend.get('showMaximumWhenExpanded') === true;
},
showValueWhenExpanded() {
return this.legend.get('showValueWhenExpanded') === true;
return this.loaded && this.legend.get('showValueWhenExpanded') === true;
},
showTimestampWhenExpanded() {
return this.legend.get('showTimestampWhenExpanded') === true;
return this.loaded && this.legend.get('showTimestampWhenExpanded') === true;
}
},
watch: {
@ -146,6 +144,9 @@ export default {
},
mounted() {
eventHelpers.extend(this);
this.config = this.getConfig();
this.legend = this.config.legend;
this.loaded = true;
this.listenTo(this.seriesObject, 'change:color', (newColor) => {
this.updateColor(newColor);
}, this);
@ -159,8 +160,13 @@ export default {
this.stopListening();
},
methods: {
getConfig() {
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
return configStore.get(configId);
},
initialize(highlightedObject) {
const seriesObject = highlightedObject ? highlightedObject.series : this.seriesObject;
const seriesObject = highlightedObject?.series || this.seriesObject;
this.isMissing = seriesObject.domainObject.status === 'missing';
this.colorAsHexString = seriesObject.get('color').asHexString();

View File

@ -27,13 +27,16 @@
:class="[plotLegendExpandedStateClass, plotLegendPositionClass]"
>
<plot-legend
v-if="compositionObjectsConfigLoaded"
:cursor-locked="!!lockHighlightPoint"
:series="seriesModels"
:highlights="highlights"
:legend="legend"
@legendHoverChanged="legendHoverChanged"
@expanded="updateExpanded"
@position="updatePosition"
/>
<div class="l-view-section">
<div
class="l-view-section"
>
<stacked-plot-item
v-for="objectWrapper in compositionObjects"
:key="objectWrapper.keyString"
@ -51,7 +54,7 @@
@gridLines="onGridLinesChange"
@lockHighlightPoint="lockHighlightPointUpdated"
@highlights="highlightsUpdated"
@configLoaded="registerSeriesListeners"
@configLoaded="configLoadedForObject(objectWrapper.keyString)"
/>
</div>
</div>
@ -66,14 +69,13 @@ import ColorPalette from "@/ui/color/ColorPalette";
import PlotLegend from "../legend/PlotLegend.vue";
import StackedPlotItem from './StackedPlotItem.vue';
import ImageExporter from '../../../exporters/ImageExporter';
import eventHelpers from "@/plugins/plot/lib/eventHelpers";
export default {
components: {
StackedPlotItem,
PlotLegend
},
inject: ['openmct', 'domainObject', 'composition', 'path'],
inject: ['openmct', 'domainObject', 'path'],
props: {
options: {
type: Object,
@ -87,24 +89,25 @@ export default {
hideExportButtons: false,
cursorGuide: false,
gridLines: true,
loading: false,
configLoaded: {},
compositionObjects: [],
tickWidthMap: {},
legend: {},
loaded: false,
lockHighlightPoint: false,
highlights: [],
seriesModels: [],
showLimitLineLabels: undefined,
colorPalette: new ColorPalette()
colorPalette: new ColorPalette(),
compositionObjectsConfigLoaded: false,
position: 'top',
expanded: false
};
},
computed: {
plotLegendPositionClass() {
return `plot-legend-${this.config.legend.get('position')}`;
return `plot-legend-${this.position}`;
},
plotLegendExpandedStateClass() {
if (this.config.legend.get('expanded')) {
if (this.expanded) {
return 'plot-legend-expanded';
} else {
return 'plot-legend-collapsed';
@ -118,17 +121,14 @@ export default {
this.destroy();
},
mounted() {
eventHelpers.extend(this);
this.seriesConfig = {};
//We only need to initialize the stacked plot config for legend properties
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.config = this.getConfig(configId);
this.legend = this.config.legend;
this.loaded = true;
this.imageExporter = new ImageExporter(this.openmct);
this.composition = this.openmct.composition.get(this.domainObject);
this.composition.on('add', this.addChild);
this.composition.on('remove', this.removeChild);
this.composition.on('reorder', this.compositionReorder);
@ -142,7 +142,6 @@ export default {
id: configId,
domainObject: this.domainObject,
openmct: this.openmct,
palette: this.colorPalette,
callback: (data) => {
this.data = data;
}
@ -155,10 +154,19 @@ export default {
loadingUpdated(loaded) {
this.loading = loaded;
},
destroy() {
this.stopListening();
configStore.deleteStore(this.config.id);
configLoadedForObject(childObjIdentifier) {
const childObjId = this.openmct.objects.makeKeyString(childObjIdentifier);
this.configLoaded[childObjId] = true;
this.setConfigLoadedForComposition();
},
setConfigLoadedForComposition() {
this.compositionObjectsConfigLoaded = this.compositionObjects.length && this.compositionObjects.every(childObject => {
const id = childObject.keyString;
return this.configLoaded[id] === true;
});
},
destroy() {
this.composition.off('add', this.addChild);
this.composition.off('remove', this.removeChild);
this.composition.off('reorder', this.compositionReorder);
@ -173,6 +181,7 @@ export default {
object: child,
keyString: id
});
this.setConfigLoadedForComposition();
},
removeChild(childIdentifier) {
@ -180,23 +189,36 @@ export default {
this.$delete(this.tickWidthMap, id);
const configIndex = this.domainObject.configuration.series.findIndex((seriesConfig) => {
return this.openmct.objects.areIdsEqual(seriesConfig.identifier, childIdentifier);
});
const childObj = this.compositionObjects.filter((c) => {
const identifier = c.keyString;
if (configIndex > -1) {
this.domainObject.configuration.series.splice(configIndex, 1);
return identifier === id;
})[0];
if (childObj) {
if (childObj.object.type !== 'telemetry.plot.overlay') {
const config = this.getConfig(childObj.keyString);
if (config) {
config.series.remove(config.series.at(0));
}
}
}
this.removeSeries({
keyString: id
});
this.compositionObjects = this.compositionObjects.filter((c) => {
const identifier = c.keyString;
return identifier !== id;
});
const configIndex = this.domainObject.configuration.series.findIndex((seriesConfig) => {
return this.openmct.objects.areIdsEqual(seriesConfig.identifier, childIdentifier);
});
if (configIndex > -1) {
const cSeries = this.domainObject.configuration.series.slice();
this.openmct.objects.mutate(this.domainObject, 'configuration.series', cSeries);
}
this.setConfigLoadedForComposition();
},
compositionReorder(reorderPlan) {
@ -245,39 +267,18 @@ export default {
lockHighlightPointUpdated(data) {
this.lockHighlightPoint = data;
},
updateExpanded(expanded) {
this.expanded = expanded;
},
updatePosition(position) {
this.position = position;
},
updateReady(ready) {
this.configReady = ready;
},
highlightsUpdated(data) {
this.highlights = data;
},
registerSeriesListeners(configId) {
const config = this.getConfig(configId);
this.seriesConfig[configId] = config;
const childObject = config.get('domainObject');
//TODO differentiate between objects with composition and those without
if (childObject.type === 'telemetry.plot.overlay') {
this.listenTo(config.series, 'add', this.addSeries, this);
this.listenTo(config.series, 'remove', this.removeSeries, this);
}
config.series.models.forEach(this.addSeries, this);
},
addSeries(series) {
const childObject = series.domainObject;
//don't add the series if it can have child series this will happen in registerSeriesListeners
if (childObject.type !== 'telemetry.plot.overlay') {
const index = this.seriesModels.length;
this.$set(this.seriesModels, index, series);
}
},
removeSeries(plotSeries) {
const index = this.seriesModels.findIndex(seriesModel => seriesModel.keyString === plotSeries.keyString);
if (index > -1) {
this.$delete(this.seriesModels, index);
}
this.stopListening(plotSeries);
},
onCursorGuideChange(cursorGuide) {
this.cursorGuide = cursorGuide === true;
},

View File

@ -100,10 +100,6 @@ export default {
this.updateView();
},
beforeDestroy() {
if (this.removeSelectable) {
this.removeSelectable();
}
if (this.component) {
this.component.$destroy();
}
@ -180,8 +176,6 @@ export default {
},
template: '<div v-if="!isMissing" ref="plotWrapper" class="l-view-section u-style-receiver js-style-receiver" :class="{\'s-status-timeconductor-unsynced\': status && status === \'timeconductor-unsynced\', \'is-stale\': isStale}"><progress-bar v-show="loading !== false" class="c-telemetry-table__progress-bar" :model="{progressPerc: undefined}" /><mct-plot :init-grid-lines="gridLines" :init-cursor-guide="cursorGuide" :plot-tick-width="plotTickWidth" :limit-line-labels="limitLineLabels" :color-palette="colorPalette" :options="options" @plotTickWidth="onTickWidthChange" @lockHighlightPoint="onLockHighlightPointUpdated" @highlights="onHighlightsUpdated" @configLoaded="onConfigLoaded" @cursorGuide="onCursorGuideChange" @gridLines="onGridLinesChange" @statusUpdated="setStatus" @loadingUpdated="loadingUpdated"/></div>'
});
this.setSelection();
},
onLockHighlightPointUpdated() {
this.$emit('lockHighlightPoint', ...arguments);
@ -205,17 +199,6 @@ export default {
this.status = status;
this.updateComponentProp('status', status);
},
setSelection() {
let childContext = {};
childContext.item = this.childObject;
this.context = childContext;
if (this.removeSelectable) {
this.removeSelectable();
}
this.removeSelectable = this.openmct.selection.selectable(
this.$el, this.context);
},
getProps() {
return {
limitLineLabels: this.showLimitLineLabels,
@ -230,7 +213,7 @@ export default {
},
getPlotObject() {
if (this.childObject.configuration && this.childObject.configuration.series) {
//If the object has a configuration, allow initialization of the config from it's persisted config
//If the object has a configuration (like an overlay plot), allow initialization of the config from it's persisted config
return this.childObject;
} else {
//If object is missing, warn and return object

View File

@ -57,7 +57,6 @@ export default function StackedPlotViewProvider(openmct) {
provide: {
openmct,
domainObject,
composition: openmct.composition.get(domainObject),
path: objectPath
},
data() {

View File

@ -173,7 +173,7 @@ describe("the plugin", function () {
let testTelemetryObject2;
let config;
let component;
let mockComposition;
let mockCompositionList = [];
let plotViewComponentObject;
afterAll(() => {
@ -271,14 +271,34 @@ describe("the plugin", function () {
}
};
mockComposition = new EventEmitter();
mockComposition.load = () => {
mockComposition.emit('add', testTelemetryObject);
stackedPlotObject.composition = [{
identifier: testTelemetryObject.identifier
}];
return [testTelemetryObject];
};
mockCompositionList = [];
spyOn(openmct.composition, 'get').and.callFake((domainObject) => {
//We need unique compositions here - one for the StackedPlot view and one for the PlotLegend view
const numObjects = domainObject.composition.length;
const mockComposition = new EventEmitter();
mockComposition.load = () => {
if (numObjects === 1) {
mockComposition.emit('add', testTelemetryObject);
spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
return [testTelemetryObject];
} else if (numObjects === 2) {
mockComposition.emit('add', testTelemetryObject);
mockComposition.emit('add', testTelemetryObject2);
return [testTelemetryObject, testTelemetryObject2];
} else {
return [];
}
};
mockCompositionList.push(mockComposition);
return mockComposition;
});
let viewContainer = document.createElement("div");
child.append(viewContainer);
@ -290,7 +310,6 @@ describe("the plugin", function () {
provide: {
openmct: openmct,
domainObject: stackedPlotObject,
composition: openmct.composition.get(stackedPlotObject),
path: [stackedPlotObject]
},
template: "<stacked-plot></stacked-plot>"
@ -321,7 +340,7 @@ describe("the plugin", function () {
expect(legend.length).toBe(6);
});
it("Renders X-axis ticks for the telemetry object", (done) => {
it("Renders X-axis ticks for the telemetry object", () => {
let xAxisElement = element.querySelectorAll(".gl-plot-axis-area.gl-plot-x .gl-plot-tick-wrapper");
expect(xAxisElement.length).toBe(1);
@ -329,13 +348,8 @@ describe("the plugin", function () {
min: 0,
max: 4
});
Vue.nextTick(() => {
let ticks = xAxisElement[0].querySelectorAll(".gl-plot-tick");
expect(ticks.length).toBe(9);
done();
});
let ticks = xAxisElement[0].querySelectorAll(".gl-plot-tick");
expect(ticks.length).toBe(9);
});
it("Renders Y-axis ticks for the telemetry object", (done) => {
@ -401,17 +415,22 @@ describe("the plugin", function () {
});
it('plots a new series when a new telemetry object is added', (done) => {
mockComposition.emit('add', testTelemetryObject2);
//setting composition here so that any new triggers to composition.load with correctly load the mockComposition in the beforeEach
stackedPlotObject.composition = [testTelemetryObject, testTelemetryObject2];
mockCompositionList[0].emit('add', testTelemetryObject2);
Vue.nextTick(() => {
let legend = element.querySelectorAll(".plot-wrapper-collapsed-legend .plot-series-name");
expect(legend.length).toBe(2);
expect(legend[1].innerHTML).toEqual("Test Object2");
done();
});
});
it('removes plots from series when a telemetry object is removed', (done) => {
mockComposition.emit('remove', testTelemetryObject.identifier);
stackedPlotObject.composition = [];
mockCompositionList[0].emit('remove', testTelemetryObject.identifier);
Vue.nextTick(() => {
expect(plotViewComponentObject.compositionObjects.length).toBe(0);
done();
@ -429,16 +448,6 @@ describe("the plugin", function () {
});
});
it("Renders a new series when added to one of the plots", (done) => {
mockComposition.emit('add', testTelemetryObject2);
Vue.nextTick(() => {
let legend = element.querySelectorAll(".plot-wrapper-collapsed-legend .plot-series-name");
expect(legend.length).toBe(2);
expect(legend[1].innerHTML).toEqual("Test Object2");
done();
});
});
it("Adds a new point to the plot", (done) => {
let originalLength = config.series.models[0].getSeriesData().length;
config.series.models[0].add({
@ -459,7 +468,7 @@ describe("the plugin", function () {
max: 10
});
Vue.nextTick(() => {
expect(plotViewComponentObject.$children[1].component.$children[1].xScale.domain()).toEqual({
expect(plotViewComponentObject.$children[0].component.$children[1].xScale.domain()).toEqual({
min: 0,
max: 10
});
@ -476,7 +485,7 @@ describe("the plugin", function () {
});
});
Vue.nextTick(() => {
const yAxesScales = plotViewComponentObject.$children[1].component.$children[1].yScale;
const yAxesScales = plotViewComponentObject.$children[0].component.$children[1].yScale;
yAxesScales.forEach((yAxisScale) => {
expect(yAxisScale.scale.domain()).toEqual({
min: 10,

View File

@ -42,7 +42,6 @@
@import "../ui/inspector/elements.scss";
@import "../ui/inspector/inspector.scss";
@import "../ui/inspector/location.scss";
@import "../ui/inspector/annotations/annotation-inspector.scss";
@import "../ui/layout/app-logo.scss";
@import "../ui/layout/create-button.scss";
@import "../ui/layout/layout.scss";

View File

@ -145,7 +145,7 @@ export default {
this.unlistenComposition();
if (this.parentObject) {
if (this.parentObject && this.parentObject.type === 'telemetry.plot.overlay') {
this.setYAxisIds();
this.composition = this.openmct.composition.get(this.parentObject);
@ -175,6 +175,7 @@ export default {
setYAxisIds() {
const configId = this.openmct.objects.makeKeyString(this.parentObject.identifier);
this.config = configStore.get(configId);
this.yAxes = [];
this.yAxes.push({
id: this.config.yAxis.id,
elements: this.parentObject.configuration.series.filter(

View File

@ -1,83 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
<template>
<div class="c-annotation__row">
<textarea
v-model="contentModel"
class="c-annotation__text_area"
type="text"
></textarea>
<div>
<span>{{ modifiedOnDate }}</span>
<span>{{ modifiedOnTime }}</span>
</div>
</div>
</template>
<script>
import Moment from 'moment';
export default {
inject: ['openmct'],
props: {
annotation: {
type: Object,
default() {
return {};
}
}
},
data() {
return {
};
},
computed: {
contentModel: {
get() {
return this.annotation.contentText;
},
set(contentText) {
console.debug(`Set tag called with ${contentText}`);
}
},
modifiedOnDate() {
return this.formatTime(this.annotation.modified, 'YYYY-MM-DD');
},
modifiedOnTime() {
return this.formatTime(this.annotation.modified, 'HH:mm:ss');
}
},
mounted() {
},
methods: {
getAvailableTagByID(tagID) {
return this.openmct.annotation.getAvailableTags().find(tag => {
return tag.id === tagID;
});
},
formatTime(unixTime, timeFormat) {
return Moment.utc(unixTime).format(timeFormat);
}
}
};
</script>

View File

@ -111,25 +111,31 @@ export default {
return this?.selection?.[0]?.[0]?.context?.item;
},
targetDetails() {
return this?.selection?.[0]?.[1]?.context?.targetDetails ?? {};
return this?.selection?.[0]?.[0]?.context?.targetDetails ?? {};
},
shouldShowTagsEditor() {
return Object.keys(this.targetDetails).length > 0;
const showingTagsEditor = Object.keys(this.targetDetails).length > 0;
if (showingTagsEditor) {
return true;
}
return false;
},
targetDomainObjects() {
return this?.selection?.[0]?.[1]?.context?.targetDomainObjects ?? {};
return this?.selection?.[0]?.[0]?.context?.targetDomainObjects ?? {};
},
selectedAnnotations() {
return this?.selection?.[0]?.[1]?.context?.annotations;
return this?.selection?.[0]?.[0]?.context?.annotations;
},
annotationType() {
return this?.selection?.[0]?.[1]?.context?.annotationType;
return this?.selection?.[0]?.[0]?.context?.annotationType;
},
annotationFilter() {
return this?.selection?.[0]?.[1]?.context?.annotationFilter;
return this?.selection?.[0]?.[0]?.context?.annotationFilter;
},
onAnnotationChange() {
return this?.selection?.[0]?.[1]?.context?.onAnnotationChange;
return this?.selection?.[0]?.[0]?.context?.onAnnotationChange;
}
},
async mounted() {
@ -195,6 +201,7 @@ export default {
}
},
async loadAnnotationForTargetObject(target) {
console.debug(`📝 Loading annotations for target`, target);
const targetID = this.openmct.objects.makeKeyString(target.identifier);
const allAnnotationsForTarget = await this.openmct.annotation.getAnnotations(target.identifier);
const filteredAnnotationsForSelection = allAnnotationsForTarget.filter(annotation => {

View File

@ -1,18 +0,0 @@
.c-inspect-annotations {
> * + * {
margin-top: $interiorMargin;
}
&__content{
> * + * {
margin-top: $interiorMargin;
}
}
&__content {
display: flex;
flex-direction: column;
}
}

View File

@ -150,16 +150,11 @@ export default {
});
const selection =
[
{
element: this.openmct.layout.$refs.browseObject.$el,
context: {
item: this.result
}
},
{
element: this.$el,
context: {
type: 'plot-points-selection',
item: this.result.targetModels[0],
type: 'plot-annotation-search-result',
targetDetails,
targetDomainObjects,
annotations: [this.result],