From e0ed0bb6e22d94a3ac894d03c7cd7c0fd0f4aefe Mon Sep 17 00:00:00 2001 From: Khalid Adil Date: Mon, 12 Dec 2022 13:51:57 -0600 Subject: [PATCH] [Plots] Ignore Infinity when autoscaling y-axis (#5907) * Change approach to filter positive and negative infinity values when updating stats * Change filter when there are no plot stats and a positive/negative infinity value occurs * Add check for negative infinity * Name the unplottable values array and move it to the constructor * Add option to render infinity values * Add e2e test to render plot with infinity values * Add accessibility labels to help locate items in tests Refactor tests * refactor(e2e): stabilize plotRendering test Co-authored-by: Shefali Co-authored-by: Jesse Mazzella --- .../plugins/plot/plotRendering.e2e.spec.js | 89 ++++++++++++++++++- example/generator/GeneratorProvider.js | 6 +- example/generator/generatorWorker.js | 21 +++-- example/generator/plugin.js | 13 ++- .../components/controls/ToggleSwitchField.vue | 1 + src/plugins/plot/configuration/PlotSeries.js | 12 ++- src/ui/components/ToggleSwitch.vue | 11 ++- 7 files changed, 138 insertions(+), 15 deletions(-) diff --git a/e2e/tests/functional/plugins/plot/plotRendering.e2e.spec.js b/e2e/tests/functional/plugins/plot/plotRendering.e2e.spec.js index 2979a9a3bc..5ad938eb14 100644 --- a/e2e/tests/functional/plugins/plot/plotRendering.e2e.spec.js +++ b/e2e/tests/functional/plugins/plot/plotRendering.e2e.spec.js @@ -26,7 +26,7 @@ */ const { test, expect } = require('../../../../pluginFixtures'); -const { createDomainObjectWithDefaults } = require('../../../../appActions'); +const { createDomainObjectWithDefaults} = require('../../../../appActions'); test.describe('Plot Integrity Testing @unstable', () => { let sineWaveGeneratorObject; @@ -40,7 +40,6 @@ test.describe('Plot Integrity Testing @unstable', () => { test('Plots do not re-request data when a plot is clicked', async ({ page }) => { //Navigate to Sine Wave Generator await page.goto(sineWaveGeneratorObject.url); - //Capture the number of plots points and store as const name numberOfPlotPoints //Click on the plot canvas await page.locator('canvas').nth(1).click(); //No request was made to get historical data @@ -51,4 +50,90 @@ test.describe('Plot Integrity Testing @unstable', () => { }); expect(createMineFolderRequests.length).toEqual(0); }); + + test('Plot is rendered when infinity values exist', async ({ page }) => { + // Edit Plot + await editSineWaveToUseInfinityOption(page, sineWaveGeneratorObject); + + //Get pixel data from Canvas + const plotPixelSize = await getCanvasPixelsWithData(page); + expect(plotPixelSize).toBeGreaterThan(0); + }); }); + +/** + * This function edits a sine wave generator with the default options and enables the infinity values option. + * + * @param {import('@playwright/test').Page} page + * @param {import('../../../../appActions').CreateObjectInfo} sineWaveGeneratorObject + * @returns {Promise} An object containing information about the edited domain object. + */ +async function editSineWaveToUseInfinityOption(page, sineWaveGeneratorObject) { + await page.goto(sineWaveGeneratorObject.url); + // Edit LAD table + await page.locator('[title="More options"]').click(); + await page.locator('[title="Edit properties of this object."]').click(); + // Modify the infinity option to true + const infinityInput = page.locator('[aria-label="Include Infinity Values"]'); + await infinityInput.click(); + + // Click OK button and wait for Navigate event + await Promise.all([ + page.waitForLoadState(), + page.click('[aria-label="Save"]'), + // Wait for Save Banner to appear + page.waitForSelector('.c-message-banner__message') + ]); + + // FIXME: Changes to SWG properties should be reflected on save, but they're not? + // Thus, navigate away and back to the object. + await page.goto('./#/browse/mine'); + await page.goto(sineWaveGeneratorObject.url); + + await page.locator('c-progress-bar c-telemetry-table__progress-bar').waitFor({ + state: 'hidden' + }); + + // FIXME: The progress bar disappears on series data load, not on plot render, + // so wait for a half a second before evaluating the canvas. + // eslint-disable-next-line playwright/no-wait-for-timeout + await page.waitForTimeout(500); +} + +/** + * @param {import('@playwright/test').Page} page + */ +async function getCanvasPixelsWithData(page) { + const getTelemValuePromise = new Promise(resolve => page.exposeFunction('getCanvasValue', resolve)); + + await page.evaluate(() => { + // The document canvas is where the plot points and lines are drawn. + // The only way to access the canvas is using document (using page.evaluate) + let data; + let canvas; + let ctx; + canvas = document.querySelector('canvas'); + ctx = canvas.getContext('2d'); + data = ctx.getImageData(0, 0, canvas.width, canvas.height).data; + const imageDataValues = Object.values(data); + let plotPixels = []; + // Each pixel consists of four values within the ImageData.data array. The for loop iterates by multiples of four. + // The values associated with each pixel are R (red), G (green), B (blue), and A (alpha), in that order. + for (let i = 0; i < imageDataValues.length;) { + if (imageDataValues[i] > 0) { + plotPixels.push({ + startIndex: i, + endIndex: i + 3, + value: `rgb(${imageDataValues[i]}, ${imageDataValues[i + 1]}, ${imageDataValues[i + 2]}, ${imageDataValues[i + 3]})` + }); + } + + i = i + 4; + + } + + window.getCanvasValue(plotPixels.length); + }); + + return getTelemValuePromise; +} diff --git a/example/generator/GeneratorProvider.js b/example/generator/GeneratorProvider.js index ee0bf98f91..3c28f5b795 100644 --- a/example/generator/GeneratorProvider.js +++ b/example/generator/GeneratorProvider.js @@ -33,7 +33,8 @@ define([ dataRateInHz: 1, randomness: 0, phase: 0, - loadDelay: 0 + loadDelay: 0, + infinityValues: false }; function GeneratorProvider(openmct) { @@ -56,7 +57,8 @@ define([ 'dataRateInHz', 'randomness', 'phase', - 'loadDelay' + 'loadDelay', + 'infinityValues' ]; request = request || {}; diff --git a/example/generator/generatorWorker.js b/example/generator/generatorWorker.js index bc9083da3a..f227eedace 100644 --- a/example/generator/generatorWorker.js +++ b/example/generator/generatorWorker.js @@ -76,10 +76,10 @@ name: data.name, utc: nextStep, yesterday: nextStep - 60 * 60 * 24 * 1000, - sin: sin(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness), + sin: sin(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness, data.infinityValues), wavelengths: wavelengths(), intensities: intensities(), - cos: cos(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness) + cos: cos(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness, data.infinityValues) } }); nextStep += step; @@ -117,6 +117,7 @@ var phase = request.phase; var randomness = request.randomness; var loadDelay = Math.max(request.loadDelay, 0); + var infinityValues = request.infinityValues; var step = 1000 / dataRateInHz; var nextStep = start - (start % step) + step; @@ -127,10 +128,10 @@ data.push({ utc: nextStep, yesterday: nextStep - 60 * 60 * 24 * 1000, - sin: sin(nextStep, period, amplitude, offset, phase, randomness), + sin: sin(nextStep, period, amplitude, offset, phase, randomness, infinityValues), wavelengths: wavelengths(), intensities: intensities(), - cos: cos(nextStep, period, amplitude, offset, phase, randomness) + cos: cos(nextStep, period, amplitude, offset, phase, randomness, infinityValues) }); } @@ -155,12 +156,20 @@ }); } - function cos(timestamp, period, amplitude, offset, phase, randomness) { + function cos(timestamp, period, amplitude, offset, phase, randomness, infinityValues) { + if (infinityValues && Math.random() > 0.5) { + return Number.POSITIVE_INFINITY; + } + return amplitude * Math.cos(phase + (timestamp / period / 1000 * Math.PI * 2)) + (amplitude * Math.random() * randomness) + offset; } - function sin(timestamp, period, amplitude, offset, phase, randomness) { + function sin(timestamp, period, amplitude, offset, phase, randomness, infinityValues) { + if (infinityValues && Math.random() > 0.5) { + return Number.POSITIVE_INFINITY; + } + return amplitude * Math.sin(phase + (timestamp / period / 1000 * Math.PI * 2)) + (amplitude * Math.random() * randomness) + offset; } diff --git a/example/generator/plugin.js b/example/generator/plugin.js index 7444e1b9cd..1060020a6d 100644 --- a/example/generator/plugin.js +++ b/example/generator/plugin.js @@ -143,6 +143,16 @@ define([ "telemetry", "loadDelay" ] + }, + { + name: "Include Infinity Values", + control: "toggleSwitch", + cssClass: "l-input", + key: "infinityValues", + property: [ + "telemetry", + "infinityValues" + ] } ], initialize: function (object) { @@ -153,7 +163,8 @@ define([ dataRateInHz: 1, phase: 0, randomness: 0, - loadDelay: 0 + loadDelay: 0, + infinityValues: false }; } }); diff --git a/src/api/forms/components/controls/ToggleSwitchField.vue b/src/api/forms/components/controls/ToggleSwitchField.vue index bea9224e30..3699331217 100644 --- a/src/api/forms/components/controls/ToggleSwitchField.vue +++ b/src/api/forms/components/controls/ToggleSwitchField.vue @@ -29,6 +29,7 @@ diff --git a/src/plugins/plot/configuration/PlotSeries.js b/src/plugins/plot/configuration/PlotSeries.js index 5923c2c154..c844bcc8b1 100644 --- a/src/plugins/plot/configuration/PlotSeries.js +++ b/src/plugins/plot/configuration/PlotSeries.js @@ -83,6 +83,8 @@ export default class PlotSeries extends Model { // Model.apply(this, arguments); this.onXKeyChange(this.get('xKey')); this.onYKeyChange(this.get('yKey')); + + this.unPlottableValues = [undefined, Infinity, -Infinity]; } /** @@ -342,6 +344,10 @@ export default class PlotSeries extends Model { let stats = this.get('stats'); let changed = false; if (!stats) { + if ([Infinity, -Infinity].includes(value)) { + return; + } + stats = { minValue: value, minPoint: point, @@ -350,13 +356,13 @@ export default class PlotSeries extends Model { }; changed = true; } else { - if (stats.maxValue < value) { + if (stats.maxValue < value && value !== Infinity) { stats.maxValue = value; stats.maxPoint = point; changed = true; } - if (stats.minValue > value) { + if (stats.minValue > value && value !== -Infinity) { stats.minValue = value; stats.minPoint = point; changed = true; @@ -419,7 +425,7 @@ export default class PlotSeries extends Model { * @private */ isValueInvalid(val) { - return Number.isNaN(val) || val === undefined; + return Number.isNaN(val) || this.unPlottableValues.includes(val); } /** diff --git a/src/ui/components/ToggleSwitch.vue b/src/ui/components/ToggleSwitch.vue index 65e476abc8..fe268a02d8 100644 --- a/src/ui/components/ToggleSwitch.vue +++ b/src/ui/components/ToggleSwitch.vue @@ -7,7 +7,11 @@ :checked="checked" @change="onUserSelect($event)" > - +