Plot legends expand by default when enabled (#7453)

* expanded legend showing, but malformed

* fix legends

* add e2e test and aria labels for controls

* fix tests

* remove focused test

* make plot legend items dynamic

* expand legend immediately when changing default

* Ensure stacked plots show cumulative legend (#7481)

* simplify config loading logic

* wip

* fixed stacked plot legend issue

* fix legend

* remove console.debugs

* remove extraneous prop

* add test

* fix legend

* use props
This commit is contained in:
Scott Bell 2024-02-07 19:35:19 +01:00 committed by GitHub
parent cd6adbadde
commit 5f8d6899d2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 380 additions and 141 deletions

View File

@ -493,7 +493,7 @@
"WCAG",
"stackedplot",
"Andale",
"unnnormalized",
"unnormalized",
"checksnapshots",
"specced",
"composables",

View File

@ -63,6 +63,66 @@ test.describe('Overlay Plot', () => {
await expect(seriesColorSwatch).toHaveCSS('background-color', 'rgb(255, 166, 61)');
});
test('Plot legend expands by default', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7403'
});
const overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot'
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: overlayPlot.uuid
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: overlayPlot.uuid
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: overlayPlot.uuid
});
await page.goto(overlayPlot.url);
await page.getByRole('tab', { name: 'Config' }).click();
// Assert that the legend is collapsed by default
await expect(page.getByLabel('Plot Legend Collapsed')).toBeVisible();
await expect(page.getByLabel('Plot Legend Expanded')).toBeHidden();
await expect(page.getByLabel('Expand by Default')).toHaveText('No');
expect(await page.getByLabel('Plot Legend Item').count()).toBe(3);
// Change the legend to expand by default
await page.getByLabel('Edit Object').click();
await page.getByLabel('Expand By Default').check();
await page.getByLabel('Save').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Assert that the legend is now open
await expect(page.getByLabel('Plot Legend Collapsed')).toBeHidden();
await expect(page.getByLabel('Plot Legend Expanded')).toBeVisible();
await expect(page.getByRole('cell', { name: 'Name' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'Value' })).toBeVisible();
await expect(page.getByLabel('Expand by Default')).toHaveText('Yes');
await expect(page.getByLabel('Plot Legend Item')).toHaveCount(3);
// Assert that the legend is expanded on page load
await page.reload();
await expect(page.getByLabel('Plot Legend Collapsed')).toBeHidden();
await expect(page.getByLabel('Plot Legend Expanded')).toBeVisible();
await expect(page.getByRole('cell', { name: 'Name' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'Value' })).toBeVisible();
await expect(page.getByLabel('Expand by Default')).toHaveText('Yes');
await expect(page.getByLabel('Plot Legend Item')).toHaveCount(3);
});
test('Limit lines persist when series is moved to another Y Axis and on refresh', async ({
page
}) => {

View File

@ -257,6 +257,56 @@ test.describe('Stacked Plot', () => {
await assertAggregateLegendIsVisible(page);
});
test('can toggle between aggregate and per child legends', async ({ page }) => {
// make some an overlay plot
const overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot',
parent: stackedPlot.uuid
});
// make some SWGs for the overlay plot
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: overlayPlot.uuid
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: overlayPlot.uuid
});
await page.goto(stackedPlot.url);
await page.getByLabel('Edit Object').click();
await page.getByRole('tab', { name: 'Config' }).click();
await page.getByLabel('Inspector Views').getByRole('checkbox').uncheck();
await page.getByLabel('Expand By Default').check();
await page.getByLabel('Save').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await expect(page.getByLabel('Plot Legend Expanded')).toHaveCount(1);
await expect(page.getByLabel('Plot Legend Item')).toHaveCount(5);
// reload and ensure the legend is still expanded
await page.reload();
await expect(page.getByLabel('Plot Legend Expanded')).toHaveCount(1);
await expect(page.getByLabel('Plot Legend Item')).toHaveCount(5);
// change to collapsed by default
await page.getByLabel('Edit Object').click();
await page.getByLabel('Expand By Default').uncheck();
await page.getByLabel('Save').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await expect(page.getByLabel('Plot Legend Collapsed')).toHaveCount(1);
await expect(page.getByLabel('Plot Legend Item')).toHaveCount(5);
// change it to individual legends
await page.getByLabel('Edit Object').click();
await page.getByRole('tab', { name: 'Config' }).click();
await page.getByLabel('Show Legends For Children').check();
await page.getByLabel('Save').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await expect(page.getByLabel('Plot Legend Collapsed')).toHaveCount(4);
await expect(page.getByLabel('Plot Legend Item')).toHaveCount(5);
});
});
/**

View File

@ -28,9 +28,9 @@ const DEFAULT_NAMESPACE = '';
const LEGACY_SPACE = 'mct';
export default function CouchPlugin(options) {
function normalizeOptions(unnnormalizedOptions) {
function normalizeOptions(unnormalizedOptions) {
const normalizedOptions = {};
if (typeof unnnormalizedOptions === 'string') {
if (typeof unnormalizedOptions === 'string') {
normalizedOptions.databases = [
{
url: options,
@ -41,19 +41,19 @@ export default function CouchPlugin(options) {
indicator: true
}
];
} else if (!unnnormalizedOptions.databases) {
} else if (!unnormalizedOptions.databases) {
normalizedOptions.databases = [
{
url: unnnormalizedOptions.url,
url: unnormalizedOptions.url,
namespace: DEFAULT_NAMESPACE,
additionalNamespaces: [LEGACY_SPACE],
readOnly: false,
useDesignDocuments: unnnormalizedOptions.useDesignDocuments,
useDesignDocuments: unnormalizedOptions.useDesignDocuments,
indicator: true
}
];
} else {
normalizedOptions.databases = unnnormalizedOptions.databases;
normalizedOptions.databases = unnormalizedOptions.databases;
}
// final sanity check, ensure we have all options

View File

@ -94,7 +94,7 @@ export default class XAxisModel extends Model {
*/
defaultModel(options) {
const bounds = options.openmct.time.bounds();
const timeSystem = options.openmct.time.timeSystem();
const timeSystem = options.openmct.time.getTimeSystem();
const format = options.openmct.telemetry.getFormatter(timeSystem.timeFormat);
/** @type {XAxisModelType} */

View File

@ -79,7 +79,7 @@
</div>
<div class="grid-cell value">{{ showLegendsForChildren ? 'Yes' : 'No' }}</div>
</li>
<li class="grid-row">
<li v-if="showLegendDetails" class="grid-row">
<div
class="grid-cell label"
title="The position of the legend relative to the plot display area."
@ -88,25 +88,27 @@
</div>
<div class="grid-cell value capitalize">{{ position }}</div>
</li>
<li class="grid-row">
<li v-if="showLegendDetails" class="grid-row">
<div class="grid-cell label" title="Hide the legend when the plot is small">
Hide when plot small
</div>
<div class="grid-cell value">{{ hideLegendWhenSmall ? 'Yes' : 'No' }}</div>
</li>
<li class="grid-row">
<li v-if="showLegendDetails" class="grid-row">
<div class="grid-cell label" title="Show the legend expanded by default">
Expand by Default
</div>
<div class="grid-cell value">{{ expandByDefault ? 'Yes' : 'No' }}</div>
<div aria-label="Expand by Default" class="grid-cell value">
{{ expandByDefault ? 'Yes' : 'No' }}
</div>
</li>
<li class="grid-row">
<li v-if="showLegendDetails" class="grid-row">
<div class="grid-cell label" title="What to display in the legend when it's collapsed.">
Show when collapsed:
</div>
<div class="grid-cell value">{{ valueToShowWhenCollapsed.replace('nearest', '') }}</div>
</li>
<li class="grid-row">
<li v-if="showLegendDetails" class="grid-row">
<div class="grid-cell label" title="What to display in the legend when it's expanded.">
Show when expanded:
</div>
@ -164,6 +166,11 @@ export default {
pathObjIndex === 0 && pathObject.type === 'telemetry.plot.stacked'
);
},
showLegendDetails() {
return (
!this.isStackedPlotObject || (this.isStackedPlotObject && !this.showLegendsForChildren)
);
},
yAxesWithSeries() {
return this.yAxes.filter((yAxis) => yAxis.seriesCount > 0);
}
@ -174,9 +181,8 @@ export default {
if (!this.isStackedPlotObject) {
this.initYAxesConfiguration();
this.registerListeners();
} else {
this.initLegendConfiguration();
}
this.initLegendConfiguration();
this.loaded = true;
},

View File

@ -132,7 +132,8 @@ export default {
},
data() {
return {
expanded: false
expanded: false,
status: null
};
},
computed: {

View File

@ -26,12 +26,13 @@
<div class="grid-cell value">
<input
v-model="showLegendsForChildren"
aria-label="Show Legends For Children"
type="checkbox"
@change="updateForm('showLegendsForChildren')"
/>
</div>
</li>
<li class="grid-row">
<li v-if="showLegendDetails" class="grid-row">
<div
class="grid-cell label"
title="The position of the legend relative to the plot display area."
@ -47,7 +48,7 @@
</select>
</div>
</li>
<li class="grid-row">
<li v-if="showLegendDetails" class="grid-row">
<div class="grid-cell label" title="Hide the legend when the plot is small">
Hide when plot small
</div>
@ -59,15 +60,20 @@
/>
</div>
</li>
<li class="grid-row">
<li v-if="showLegendDetails" class="grid-row">
<div class="grid-cell label" title="Show the legend expanded by default">
Expand by default
</div>
<div class="grid-cell value">
<input v-model="expandByDefault" type="checkbox" @change="updateForm('expandByDefault')" />
<input
v-model="expandByDefault"
aria-label="Expand By Default"
type="checkbox"
@change="updateForm('expandByDefault')"
/>
</div>
</li>
<li class="grid-row">
<li v-if="showLegendDetails" class="grid-row">
<div class="grid-cell label" title="What to display in the legend when it's collapsed.">
When collapsed show
</div>
@ -82,7 +88,7 @@
</select>
</div>
</li>
<li class="grid-row">
<li v-if="showLegendDetails" class="grid-row">
<div class="grid-cell label" title="What to display in the legend when it's expanded.">
When expanded show
</div>
@ -169,6 +175,11 @@ export default {
(pathObject, pathObjIndex) =>
pathObjIndex === 0 && pathObject?.type === 'telemetry.plot.stacked'
);
},
showLegendDetails() {
return (
!this.isStackedPlotObject || (this.isStackedPlotObject && !this.showLegendsForChildren)
);
}
},
mounted() {

View File

@ -157,7 +157,8 @@ export default {
limitLines: this.series.get('limitLines'),
markerSize: this.series.get('markerSize'),
validation: {},
swatchActive: false
swatchActive: false,
status: null
};
},
computed: {

View File

@ -30,12 +30,17 @@
<div
class="c-plot-legend__view-control gl-plot-legend__view-control c-disclosure-triangle is-enabled"
:class="{ 'c-disclosure-triangle--expanded': isLegendExpanded }"
@click="expandLegend"
@click="toggleLegend"
></div>
<div class="c-plot-legend__wrapper" :class="{ 'is-cursor-locked': cursorLocked }">
<!-- COLLAPSED PLOT LEGEND -->
<div class="plot-wrapper-collapsed-legend" :class="{ 'is-cursor-locked': cursorLocked }">
<div
v-if="!isLegendExpanded"
class="plot-wrapper-collapsed-legend"
aria-label="Plot Legend Collapsed"
:class="{ 'is-cursor-locked': cursorLocked }"
>
<div
class="c-state-indicator__alert-cursor-lock icon-cursor-lock"
title="Cursor is point locked. Click anywhere in the plot to unlock."
@ -50,7 +55,12 @@
/>
</div>
<!-- EXPANDED PLOT LEGEND -->
<div class="plot-wrapper-expanded-legend" :class="{ 'is-cursor-locked': cursorLocked }">
<div
v-else
class="plot-wrapper-expanded-legend"
aria-label="Plot Legend Expanded"
:class="{ 'is-cursor-locked': cursorLocked }"
>
<div
class="c-state-indicator__alert-cursor-lock--verbose icon-cursor-lock"
title="Click anywhere in the plot to unlock."
@ -145,11 +155,22 @@ export default {
this.legend = this.config.legend;
this.seriesModels = [];
this.listenTo(this.config.legend, 'change:position', this.updatePosition, this);
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);
}
this.listenTo(this.config.legend, 'change:expandByDefault', this.changeExpandDefault, this);
this.initialize();
},
mounted() {
this.loaded = true;
this.isLegendExpanded = this.legend.get('expanded') === true;
this.$emit('expanded', this.isLegendExpanded);
this.updatePosition();
},
beforeUnmount() {
@ -171,6 +192,11 @@ export default {
this.registerListeners(this.config);
}
},
changeExpandDefault() {
this.isLegendExpanded = this.config.legend.model.expandByDefault;
this.legend.set('expanded', this.isLegendExpanded);
this.$emit('expanded', this.isLegendExpanded);
},
getConfig() {
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
@ -198,9 +224,12 @@ export default {
config.series.forEach(this.addSeries, this);
},
addSeries(series) {
this.seriesModels[this.seriesModels.length] = series;
const existingSeries = this.getSeries(series.keyString);
if (existingSeries) {
return;
}
this.seriesModels.push(series);
},
removeSeries(plotSeries) {
this.stopListening(plotSeries);
@ -209,7 +238,13 @@ export default {
);
this.seriesModels.splice(seriesIndex, 1);
},
expandLegend() {
getSeries(keyStringToFind) {
const foundSeries = this.seriesModels.find((series) => {
return series.keyString === keyStringToFind;
});
return foundSeries;
},
toggleLegend() {
this.isLegendExpanded = !this.isLegendExpanded;
this.legend.set('expanded', this.isLegendExpanded);
this.$emit('expanded', this.isLegendExpanded);

View File

@ -22,6 +22,7 @@
<template>
<div
class="plot-legend-item"
:aria-label="`Plot Legend Item for ${domainObject?.name}`"
:class="{
'is-stale': isStale,
'is-status--missing': isMissing
@ -123,9 +124,14 @@ export default {
this.seriesModels = [];
eventHelpers.extend(this);
this.config = this.getConfig();
this.listenTo(this.config.series, 'add', this.onSeriesAdd, this);
this.listenTo(this.config.series, 'remove', this.onSeriesRemove, this);
this.config.series.forEach(this.onSeriesAdd, this);
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);
}
this.legend = this.config.legend;
this.loaded = true;
this.setupClockChangedEvent((domainObject) => {
@ -135,6 +141,11 @@ export default {
},
beforeUnmount() {
this.stopListening();
if (this.objectComposition) {
this.objectComposition.off('add', this.addTelemetryObject);
this.objectComposition.off('remove', this.removeTelemetryObject);
}
},
methods: {
getConfig() {
@ -142,9 +153,36 @@ export default {
return configStore.get(configId);
},
onSeriesAdd(series, index) {
this.seriesModels[index] = series;
if (series.keyString === this.seriesKeyString) {
registerListeners(config) {
//listen to any changes to the telemetry endpoints that are associated with the child
this.listenTo(config.series, 'add', this.onSeriesAdd, this);
this.listenTo(config.series, 'remove', this.onSeriesRemove, this);
config.series.forEach(this.onSeriesAdd, this);
},
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);
}
},
removeTelemetryObject(identifier) {
const configId = this.openmct.objects.makeKeyString(identifier);
const config = configStore.get(configId);
if (config) {
config.series.forEach(this.onSeriesRemove, this);
}
},
onSeriesAdd(series) {
if (series.keyString !== this.seriesKeyString) {
return;
}
const existingSeries = this.getSeries(series.keyString);
if (existingSeries) {
return;
}
this.seriesModels.push(series);
this.listenTo(
series,
'change:color',
@ -163,7 +201,6 @@ export default {
);
this.subscribeToStaleness(series.domainObject);
this.initialize();
}
},
onSeriesRemove(seriesToRemove) {
const seriesIndexToRemove = this.seriesModels.findIndex(

View File

@ -22,6 +22,7 @@
<template>
<tr
class="plot-legend-item"
:aria-label="`Plot Legend Item for ${domainObject?.name}`"
:class="{
'is-stale': isStale,
'is-status--missing': isMissing
@ -103,6 +104,7 @@ export default {
isMissing: false,
colorAsHexString: '',
name: '',
nameWithUnit: '',
unit: '',
formattedYValue: '',
formattedXValue: '',
@ -146,9 +148,16 @@ export default {
this.seriesModels = [];
eventHelpers.extend(this);
this.config = this.getConfig();
this.listenTo(this.config.series, 'add', this.onSeriesAdd, this);
this.listenTo(this.config.series, 'remove', this.onSeriesRemove, this);
this.config.series.forEach(this.onSeriesAdd, this);
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);
}
this.legend = this.config.legend;
this.loaded = true;
this.setupClockChangedEvent((domainObject) => {
@ -158,11 +167,43 @@ export default {
},
beforeUnmount() {
this.stopListening();
if (this.objectComposition) {
this.objectComposition.off('add', this.addTelemetryObject);
this.objectComposition.off('remove', this.removeTelemetryObject);
}
},
methods: {
onSeriesAdd(series, index) {
this.seriesModels[index] = series;
if (series.keyString === this.seriesKeyString) {
registerListeners(config) {
//listen to any changes to the telemetry endpoints that are associated with the child
this.listenTo(config.series, 'add', this.onSeriesAdd, this);
this.listenTo(config.series, 'remove', this.onSeriesRemove, this);
config.series.forEach(this.onSeriesAdd, this);
},
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);
}
},
removeTelemetryObject(identifier) {
const configId = this.openmct.objects.makeKeyString(identifier);
const config = configStore.get(configId);
if (config) {
config.series.forEach(this.onSeriesRemove, this);
}
},
onSeriesAdd(series) {
if (series.keyString !== this.seriesKeyString) {
return;
}
const existingSeries = this.getSeries(series.keyString);
if (existingSeries) {
return;
}
this.seriesModels.push(series);
this.listenTo(
series,
'change:color',
@ -181,7 +222,6 @@ export default {
);
this.subscribeToStaleness(series.domainObject);
this.initialize();
}
},
onSeriesRemove(seriesToRemove) {
const seriesIndexToRemove = this.seriesModels.findIndex(

View File

@ -401,6 +401,7 @@ describe('the plugin', function () {
const clickEvent = createMouseEvent('click');
legendControl.dispatchEvent(clickEvent);
await nextTick();
let legend = element.querySelectorAll('.plot-wrapper-expanded-legend .plot-legend-item td');
expect(legend.length).toBe(6);

View File

@ -163,13 +163,13 @@ export default {
},
updateView() {
//If this object is not persistable, then package it with it's parent
const object = this.getPlotObject();
const plotObject = this.getPlotObject();
if (this.openmct.telemetry.isTelemetryObject(object)) {
this.subscribeToStaleness(object);
if (this.openmct.telemetry.isTelemetryObject(plotObject)) {
this.subscribeToStaleness(plotObject);
} else {
// possibly overlay or other composition based plot
this.composition = this.openmct.composition.get(object);
this.composition = this.openmct.composition.get(plotObject);
this.composition.on('add', this.subscribeToStaleness);
this.composition.on('remove', this.triggerUnsubscribeFromStaleness);
@ -209,29 +209,34 @@ export default {
this.removeSelectable = this.openmct.selection.selectable(this.$el, this.context);
},
getPlotObject() {
if (this.childObject.configuration?.series) {
//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
this.checkPlotConfiguration();
// If object is missing, warn
if (this.openmct.objects.isMissing(this.childObject)) {
console.warn('Missing domain object');
return this.childObject;
console.warn('Missing domain object for stacked plot', this.childObject);
}
// If the object does not have configuration, initialize the series config with the persisted config from the stacked plot
return this.childObject;
},
checkPlotConfiguration() {
// If the object has its own configuration (like an overlay plot), don't initialize a stacked plot configuration
// and instead use its configuration directly.
// Otherwise ensure we've got a stacked plot item configuration ready for us.
if (
!this.openmct.objects.isMissing(this.childObject) &&
!this.childObject.configuration?.series
) {
this.ensureStackedSeriesConfigInitialization();
}
},
ensureStackedSeriesConfigInitialization() {
const configId = this.openmct.objects.makeKeyString(this.childObject.identifier);
let config = configStore.get(configId);
if (!config) {
let persistedSeriesConfig = this.domainObject.configuration.series.find(
(seriesConfig) => {
const existingConfig = configStore.get(configId);
if (!existingConfig) {
let persistedSeriesConfig = this.domainObject.configuration.series.find((seriesConfig) => {
return this.openmct.objects.areIdsEqual(
seriesConfig.identifier,
this.childObject.identifier
);
}
);
});
if (!persistedSeriesConfig) {
persistedSeriesConfig = {
@ -240,7 +245,7 @@ export default {
};
}
config = new PlotConfigurationModel({
const newConfig = new PlotConfigurationModel({
id: configId,
domainObject: {
...this.childObject,
@ -260,10 +265,7 @@ export default {
this.data = data;
}
});
configStore.add(configId, config);
}
return this.childObject;
configStore.add(configId, newConfig);
}
}
}

View File

@ -360,7 +360,7 @@ describe('the plugin', function () {
expect(legend[0].innerHTML).toEqual('Test Object');
});
it('Renders an expanded legend for every telemetry', () => {
it('Renders an expanded legend for every telemetry', async () => {
let legendControl = element.querySelector(
'.c-plot-legend__view-control.gl-plot-legend__view-control.c-disclosure-triangle'
);
@ -368,6 +368,8 @@ describe('the plugin', function () {
legendControl.dispatchEvent(clickEvent);
await nextTick();
let legend = element.querySelectorAll('.plot-wrapper-expanded-legend .plot-legend-item td');
expect(legend.length).toBe(6);
});

View File

@ -658,13 +658,6 @@ mct-plot {
.gl-plot,
.c-plot {
&.plot-legend-collapsed .plot-wrapper-expanded-legend {
display: none;
}
&.plot-legend-expanded .plot-wrapper-collapsed-legend {
display: none;
}
&.plot-legend-collapsed .icon-cursor-lock::before {
padding-right: 5px;
}