Toggle between showing aggregate stacked plot legend or per-plot legend (#6758)

* New option to show/hide stacked plot aggregate legend - defaulted to not show.
Use the Plot component in the StackedPlotItem component for simplicity and show/hide sub-legends as needed.

* Fix position and expanded classes when children are showing their legends

* Fix broken tests and ensure gridlines and cursorguides work.

* Adds e2e test for new legend configuration for stacked plot

* Address review comments - Remove commented out code, optimize property lookup, fix bug with staleness

* Remove the isStale icon in the legend when a plot is inside a stacked plot.

---------

Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
This commit is contained in:
Shefali Joshi 2023-07-11 16:16:46 -07:00 committed by GitHub
parent 293f25df19
commit d08ea62932
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 243 additions and 68 deletions

View File

@ -26,7 +26,11 @@ necessarily be used for reference when writing new tests in this area.
*/
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults, selectInspectorTab } = require('../../../../appActions');
const {
createDomainObjectWithDefaults,
selectInspectorTab,
waitForPlotsToRender
} = require('../../../../appActions');
test.describe('Stacked Plot', () => {
let stackedPlot;
@ -227,4 +231,45 @@ test.describe('Stacked Plot', () => {
page.locator('[aria-label="Plot Series Properties"] .c-object-label')
).toContainText(swgC.name);
});
test('the legend toggles between aggregate and per child', async ({ page }) => {
await page.goto(stackedPlot.url);
// Go into edit mode
await page.click('button[title="Edit"]');
await selectInspectorTab(page, 'Config');
let legendProperties = await page.locator('[aria-label="Legend Properties"]');
await legendProperties.locator('[title="Display legends per sub plot."]~div input').uncheck();
await assertAggregateLegendIsVisible(page);
// Save (exit edit mode)
await page.locator('button[title="Save"]').click();
await page.locator('li[title="Save and Finish Editing"]').click();
await assertAggregateLegendIsVisible(page);
await page.reload();
await assertAggregateLegendIsVisible(page);
});
});
/**
* Asserts that aggregate stacked plot legend is visible
* @param {import('@playwright/test').Page} page
*/
async function assertAggregateLegendIsVisible(page) {
// Wait for plot series data to load
await waitForPlotsToRender(page);
// Wait for plot legend to be shown
await page.waitForSelector('.js-stacked-plot-legend', { state: 'attached' });
// There should be 3 legend items
expect(
await page
.locator('.js-stacked-plot-legend .c-plot-legend__wrapper div.plot-legend-item')
.count()
).toBe(3);
}

View File

@ -77,6 +77,7 @@
<mct-chart
:rectangles="rectangles"
:highlights="highlights"
:show-limit-line-labels="limitLineLabels"
:annotated-points="annotatedPoints"
:annotation-selections="annotationSelections"
:hidden-y-axis-ids="hiddenYAxisIds"
@ -231,7 +232,7 @@ export default {
limitLineLabels: {
type: Object,
default() {
return {};
return undefined;
}
},
colorPalette: {

View File

@ -33,18 +33,23 @@
/>
<mct-plot
:class="[plotLegendExpandedStateClass, plotLegendPositionClass]"
:init-grid-lines="gridLines"
:init-grid-lines="gridLinesProp"
:init-cursor-guide="cursorGuide"
:options="options"
:limit-line-labels="limitLineLabels"
:limit-line-labels="limitLineLabelsProp"
:parent-y-tick-width="parentYTickWidth"
:color-palette="colorPalette"
@loadingUpdated="loadingUpdated"
@statusUpdated="setStatus"
@configLoaded="updateReady"
@lockHighlightPoint="lockHighlightPointUpdated"
@highlights="highlightsUpdated"
@plotYTickWidth="onYTickWidthChange"
@cursorGuide="onCursorGuideChange"
@gridLines="onGridLinesChange"
>
<plot-legend
v-if="configReady"
v-if="configReady && hideLegend === false"
:cursor-locked="lockHighlightPoint"
:highlights="highlights"
@legendHoverChanged="legendHoverChanged"
@ -79,14 +84,50 @@ export default {
compact: false
};
}
},
gridLines: {
type: Boolean,
default() {
return true;
}
},
cursorGuide: {
type: Boolean,
default() {
return false;
}
},
parentLimitLineLabels: {
type: Object,
default() {
return undefined;
}
},
colorPalette: {
type: Object,
default() {
return undefined;
}
},
parentYTickWidth: {
type: Object,
default() {
return {
leftTickWidth: 0,
rightTickWidth: 0,
hasMultipleLeftAxes: false
};
}
},
hideLegend: {
type: Boolean,
default() {
return false;
}
}
},
data() {
return {
//Don't think we need this as it appears to be stacked plot specific
// hideExportButtons: false
cursorGuide: false,
gridLines: !this.options.compact,
loading: false,
status: '',
staleObjects: [],
@ -99,6 +140,12 @@ export default {
};
},
computed: {
limitLineLabelsProp() {
return this.parentLimitLineLabels ?? this.limitLineLabels;
},
gridLinesProp() {
return this.gridLines ?? !this.options.compact;
},
staleClass() {
if (this.staleObjects.length !== 0) {
return 'is-stale';
@ -117,6 +164,14 @@ export default {
}
}
},
watch: {
gridLines(newGridLines) {
this.gridLines = newGridLines;
},
cursorGuide(newCursorGuide) {
this.cursorGuide = newCursorGuide;
}
},
mounted() {
eventHelpers.extend(this);
this.imageExporter = new ImageExporter(this.openmct);
@ -188,6 +243,7 @@ export default {
},
loadingUpdated(loading) {
this.loading = loading;
this.$emit('loadingUpdated', ...arguments);
},
destroy() {
if (this.stalenessSubscription) {
@ -223,9 +279,11 @@ export default {
},
lockHighlightPointUpdated(data) {
this.lockHighlightPoint = data;
this.$emit('lockHighlightPoint', ...arguments);
},
highlightsUpdated(data) {
this.highlights = data;
this.$emit('highlights', ...arguments);
},
legendHoverChanged(data) {
this.limitLineLabels = data;
@ -238,6 +296,16 @@ export default {
},
updateReady(ready) {
this.configReady = ready;
this.$emit('configLoaded', ...arguments);
},
onYTickWidthChange() {
this.$emit('plotYTickWidth', ...arguments);
},
onCursorGuideChange() {
this.$emit('cursorGuide', ...arguments);
},
onGridLinesChange() {
this.$emit('gridLines', ...arguments);
}
}
};

View File

@ -114,7 +114,7 @@ export default {
showLimitLineLabels: {
type: Object,
default() {
return {};
return undefined;
}
},
hiddenYAxisIds: {
@ -725,7 +725,7 @@ export default {
});
},
showLabels(seriesKey) {
return this.showLimitLineLabels.seriesKey && this.showLimitLineLabels.seriesKey === seriesKey;
return this.showLimitLineLabels?.seriesKey === seriesKey;
},
getLimitElement(limit) {
let point = {

View File

@ -55,7 +55,8 @@ export default class LegendModel extends Model {
showValueWhenExpanded: true,
showMaximumWhenExpanded: true,
showMinimumWhenExpanded: true,
showUnitsWhenExpanded: true
showUnitsWhenExpanded: true,
showLegendsForChildren: true
};
}
}

View File

@ -73,6 +73,12 @@
<div v-if="isStackedPlotObject || !isNestedWithinAStackedPlot" class="grid-properties">
<ul class="l-inspector-part js-legend-properties">
<h2 class="--first" title="Legend settings for this object">Legend</h2>
<li v-if="isStackedPlotObject" class="grid-row">
<div class="grid-cell label" title="Display legends per sub plot.">
Show legend per plot
</div>
<div class="grid-cell value">{{ showLegendsForChildren ? 'Yes' : 'No' }}</div>
</li>
<li class="grid-row">
<div
class="grid-cell label"
@ -139,6 +145,7 @@ export default {
showMinimumWhenExpanded: '',
showMaximumWhenExpanded: '',
showUnitsWhenExpanded: '',
showLegendsForChildren: '',
loaded: false,
plotSeries: [],
yAxes: []
@ -218,6 +225,7 @@ export default {
this.showMinimumWhenExpanded = this.config.legend.get('showMinimumWhenExpanded');
this.showMaximumWhenExpanded = this.config.legend.get('showMaximumWhenExpanded');
this.showUnitsWhenExpanded = this.config.legend.get('showUnitsWhenExpanded');
this.showLegendsForChildren = this.config.legend.get('showLegendsForChildren');
}
},
getConfig() {

View File

@ -35,7 +35,11 @@
:y-axis="config.yAxis"
@seriesUpdated="updateSeriesConfigForObject"
/>
<ul v-if="isStackedPlotObject || !isStackedPlotNestedObject" class="l-inspector-part">
<ul
v-if="isStackedPlotObject || !isStackedPlotNestedObject"
class="l-inspector-part"
aria-label="Legend Properties"
>
<h2 class="--first" title="Legend options">Legend</h2>
<legend-form class="grid-properties" :legend="config.legend" />
</ul>

View File

@ -21,6 +21,16 @@
-->
<template>
<div>
<li v-if="isStackedPlotObject" class="grid-row">
<div class="grid-cell label" title="Display legends per sub plot.">Show legend per plot</div>
<div class="grid-cell value">
<input
v-model="showLegendsForChildren"
type="checkbox"
@change="updateForm('showLegendsForChildren')"
/>
</div>
</li>
<li class="grid-row">
<div
class="grid-cell label"
@ -128,7 +138,7 @@ import { coerce, objectPath, validate } from './formUtil';
import _ from 'lodash';
export default {
inject: ['openmct', 'domainObject'],
inject: ['openmct', 'domainObject', 'path'],
props: {
legend: {
type: Object,
@ -148,9 +158,18 @@ export default {
showMinimumWhenExpanded: '',
showMaximumWhenExpanded: '',
showUnitsWhenExpanded: '',
showLegendsForChildren: '',
validation: {}
};
},
computed: {
isStackedPlotObject() {
return this.path.find(
(pathObject, pathObjIndex) =>
pathObjIndex === 0 && pathObject?.type === 'telemetry.plot.stacked'
);
}
},
mounted() {
this.initialize();
this.initFormValues();
@ -200,6 +219,11 @@ export default {
modelProp: 'showUnitsWhenExpanded',
coerce: Boolean,
objectPath: 'configuration.legend.showUnitsWhenExpanded'
},
{
modelProp: 'showLegendsForChildren',
coerce: Boolean,
objectPath: 'configuration.legend.showLegendsForChildren'
}
];
},
@ -213,6 +237,7 @@ export default {
this.showMinimumWhenExpanded = this.legend.get('showMinimumWhenExpanded');
this.showMaximumWhenExpanded = this.legend.get('showMaximumWhenExpanded');
this.showUnitsWhenExpanded = this.legend.get('showUnitsWhenExpanded');
this.showLegendsForChildren = this.legend.get('showLegendsForChildren');
},
updateForm(formKey) {
const newVal = this[formKey];

View File

@ -181,9 +181,14 @@ export default {
},
toggleHover(hover) {
this.hover = hover;
this.$emit('legendHoverChanged', {
seriesKey: this.hover ? this.seriesObject.keyString : ''
});
this.$emit(
'legendHoverChanged',
this.hover
? {
seriesKey: this.seriesObject.keyString
}
: undefined
);
}
}
};

View File

@ -27,9 +27,10 @@
:class="[plotLegendExpandedStateClass, plotLegendPositionClass]"
>
<plot-legend
v-if="compositionObjectsConfigLoaded"
v-if="compositionObjectsConfigLoaded && showLegendsForChildren === false"
:cursor-locked="!!lockHighlightPoint"
:highlights="highlights"
class="js-stacked-plot-legend"
@legendHoverChanged="legendHoverChanged"
@expanded="updateExpanded"
@position="updatePosition"
@ -46,6 +47,7 @@
:cursor-guide="cursorGuide"
:show-limit-line-labels="showLimitLineLabels"
:parent-y-tick-width="maxTickWidth"
:hide-legend="showLegendsForChildren === false"
@plotYTickWidth="onYTickWidthChange"
@loadingUpdated="loadingUpdated"
@cursorGuide="onCursorGuideChange"
@ -66,6 +68,7 @@ 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 '../lib/eventHelpers';
export default {
components: {
@ -96,19 +99,28 @@ export default {
colorPalette: new ColorPalette(),
compositionObjectsConfigLoaded: false,
position: 'top',
showLegendsForChildren: true,
expanded: false
};
},
computed: {
plotLegendPositionClass() {
if (this.showLegendsForChildren) {
return '';
}
return `plot-legend-${this.position}`;
},
plotLegendExpandedStateClass() {
if (this.expanded) {
return 'plot-legend-expanded';
} else {
return 'plot-legend-collapsed';
let legendExpandedStateClass = '';
if (this.showLegendsForChildren !== true && this.expanded) {
legendExpandedStateClass = 'plot-legend-expanded';
} else if (this.showLegendsForChildren !== true && !this.expanded) {
legendExpandedStateClass = 'plot-legend-collapsed';
}
return legendExpandedStateClass;
},
/**
* Returns the maximum width of the left and right y axes ticks of this stacked plots children
@ -137,9 +149,11 @@ export default {
this.destroy();
},
mounted() {
eventHelpers.extend(this);
//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.showLegendsForChildren = this.config.legend.get('showLegendsForChildren');
this.loaded = true;
this.imageExporter = new ImageExporter(this.openmct);
@ -183,11 +197,21 @@ export default {
return this.configLoaded[id] === true;
});
if (this.compositionObjectsConfigLoaded) {
this.listenTo(
this.config.legend,
'change:showLegendsForChildren',
this.updateShowLegendsForChildren,
this
);
}
},
destroy() {
this.composition.off('add', this.addChild);
this.composition.off('remove', this.removeChild);
this.composition.off('reorder', this.compositionReorder);
this.stopListening();
},
addChild(child) {
@ -305,6 +329,9 @@ export default {
updatePosition(position) {
this.position = position;
},
updateShowLegendsForChildren(showLegendsForChildren) {
this.showLegendsForChildren = showLegendsForChildren;
},
updateReady(ready) {
this.configReady = ready;
},

View File

@ -23,14 +23,13 @@
<div :aria-label="`Stacked Plot Item ${childObject.name}`"></div>
</template>
<script>
import MctPlot from '../MctPlot.vue';
import Vue from 'vue';
import conditionalStylesMixin from './mixins/objectStyles-mixin';
import stalenessMixin from '@/ui/mixins/staleness-mixin';
import StalenessUtils from '@/utils/staleness';
import configStore from '@/plugins/plot/configuration/ConfigStore';
import PlotConfigurationModel from '@/plugins/plot/configuration/PlotConfigurationModel';
import ProgressBar from '../../../ui/components/ProgressBar.vue';
import Plot from '../Plot.vue';
export default {
mixins: [conditionalStylesMixin, stalenessMixin],
@ -63,7 +62,7 @@ export default {
showLimitLineLabels: {
type: Object,
default() {
return {};
return undefined;
}
},
colorPalette: {
@ -81,6 +80,12 @@ export default {
hasMultipleLeftAxes: false
};
}
},
hideLegend: {
type: Boolean,
default() {
return false;
}
}
},
data() {
@ -104,6 +109,9 @@ export default {
},
deep: true
},
hideLegend(newHideLegend) {
this.updateComponentProp('hideLegend', newHideLegend);
},
staleObjects() {
this.isStale = this.staleObjects.length > 0;
this.updateComponentProp('isStale', this.isStale);
@ -163,7 +171,6 @@ export default {
const onConfigLoaded = this.onConfigLoaded;
const onCursorGuideChange = this.onCursorGuideChange;
const onGridLinesChange = this.onGridLinesChange;
const setStatus = this.setStatus;
const openmct = this.openmct;
const path = this.path;
@ -192,8 +199,7 @@ export default {
this.component = new Vue({
el: viewContainer,
components: {
MctPlot,
ProgressBar
Plot
},
provide: {
openmct,
@ -209,7 +215,6 @@ export default {
onConfigLoaded,
onCursorGuideChange,
onGridLinesChange,
setStatus,
isMissing,
loading: false
};
@ -220,29 +225,22 @@ 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"
:parent-y-tick-width="parentYTickWidth"
:limit-line-labels="limitLineLabels"
:color-palette="colorPalette"
:options="options"
@plotYTickWidth="onYTickWidthChange"
@lockHighlightPoint="onLockHighlightPointUpdated"
@highlights="onHighlightsUpdated"
@configLoaded="onConfigLoaded"
@cursorGuide="onCursorGuideChange"
@gridLines="onGridLinesChange"
@statusUpdated="setStatus"
@loadingUpdated="loadingUpdated"/>
</div>`
<Plot ref="plotComponent" v-if="!isMissing"
:class="{'is-stale': isStale}"
:grid-lines="gridLines"
:hide-legend="hideLegend"
:cursor-guide="cursorGuide"
:parent-limit-line-labels="limitLineLabels"
:options="options"
:parent-y-tick-width="parentYTickWidth"
:color-palette="colorPalette"
@loadingUpdated="loadingUpdated"
@configLoaded="onConfigLoaded"
@lockHighlightPoint="onLockHighlightPointUpdated"
@highlights="onHighlightsUpdated"
@plotYTickWidth="onYTickWidthChange"
@cursorGuide="onCursorGuideChange"
@gridLines="onGridLinesChange"/>`
});
if (this.isEditing) {
@ -315,10 +313,6 @@ export default {
onGridLinesChange() {
this.$emit('gridLines', ...arguments);
},
setStatus(status) {
this.status = status;
this.updateComponentProp('status', status);
},
setSelection() {
let childContext = {};
childContext.item = this.childObject;
@ -331,12 +325,12 @@ export default {
},
getProps() {
return {
hideLegend: this.hideLegend,
limitLineLabels: this.showLimitLineLabels,
gridLines: this.gridLines,
cursorGuide: this.cursorGuide,
parentYTickWidth: this.parentYTickWidth,
options: this.options,
status: this.status,
colorPalette: this.colorPalette,
isStale: this.isStale
};

View File

@ -490,12 +490,12 @@ describe('the plugin', function () {
max: 10
});
Vue.nextTick(() => {
expect(plotViewComponentObject.$children[0].component.$children[1].xScale.domain()).toEqual(
{
min: 0,
max: 10
}
);
expect(
plotViewComponentObject.$children[0].component.$children[0].$children[1].xScale.domain()
).toEqual({
min: 0,
max: 10
});
done();
});
});
@ -509,7 +509,8 @@ describe('the plugin', function () {
});
});
Vue.nextTick(() => {
const yAxesScales = plotViewComponentObject.$children[0].component.$children[1].yScale;
const yAxesScales =
plotViewComponentObject.$children[0].component.$children[0].$children[1].yScale;
yAxesScales.forEach((yAxisScale) => {
expect(yAxisScale.scale.domain()).toEqual({
min: 10,

View File

@ -93,10 +93,6 @@ mct-plot {
min-height: $plotMinH;
overflow: hidden;
.is-stale {
@include isStaleHolder();
}
&[s-selected] {
.is-editing & {
border: $editMarqueeBorder;