| import './index'; |
| |
| import { load } from '@google-web-components/google-chart/loader'; |
| import { assert } from 'chai'; |
| import { LitElement } from 'lit'; |
| import sinon from 'sinon'; |
| |
| import { PlotSummarySk } from './plot-summary-sk'; |
| import { PlotGoogleChartSk } from '../plot-google-chart-sk/plot-google-chart-sk'; |
| import { generateFullDataFrame } from '../dataframe/test_utils'; |
| import { convertFromDataframe, getTraceColor } from '../common/plot-builder'; |
| import { setUpElementUnderTest } from '../../../infra-sk/modules/test_util'; |
| |
| describe('plot-summary-sk', () => { |
| const now = new Date('2024/9/20').getTime(); |
| const timeSpans = [6 * 60 * 60]; // 6 hours |
| const commitRange = { begin: 100, end: 110 }; |
| const df = generateFullDataFrame(commitRange, now, 1, timeSpans); |
| const newEl = setUpElementUnderTest<PlotSummarySk>('plot-summary-sk'); |
| |
| // `describe` doesn't support async setup, so we need to move this into `before` block. |
| // The Google Chart API is being loaded async'ly. |
| let dt: google.visualization.DataTable | null = null; |
| before(async () => { |
| // Load Google Chart API for DataTable. |
| await load(); |
| dt = google.visualization.arrayToDataTable(convertFromDataframe(df, 'both')!); |
| }); |
| |
| const chartReady = async (cb: () => LitElement) => |
| await new Promise<LitElement>((resolve) => { |
| const el = cb(); |
| el.addEventListener('google-chart-ready', () => { |
| resolve(el); |
| }); |
| }).then(async (el) => { |
| return await el.updateComplete; |
| }); |
| |
| describe('trace colors', () => { |
| it('assigns deterministic colors based on trace name', async () => { |
| const element = newEl((el) => { |
| el.style.width = '100px'; |
| }); |
| await element.updateComplete; |
| await chartReady(() => { |
| element.data = dt; |
| return element; |
| }); |
| |
| const traceKey = Object.keys(df.traceset)[0]; |
| const expectedColor = getTraceColor(traceKey); |
| const actualColor = (element as any).traceColorMap.get(traceKey); |
| |
| assert.equal(actualColor, expectedColor); |
| }); |
| }); |
| |
| describe('trace colors consistency', () => { |
| let fetchStub: sinon.SinonStub; |
| |
| beforeEach(() => { |
| fetchStub = sinon.stub(window, 'fetch').resolves({ |
| ok: true, |
| text: async () => await Promise.resolve(''), |
| json: async () => await Promise.resolve({}), |
| } as Response); |
| }); |
| |
| afterEach(() => { |
| fetchStub.restore(); |
| }); |
| |
| it('matches PlotGoogleChartSk color for the same trace', async () => { |
| // Setup PlotSummarySk |
| const summaryElement = newEl((el) => { |
| el.style.width = '100px'; |
| }); |
| await summaryElement.updateComplete; |
| await chartReady(() => { |
| summaryElement.data = dt; |
| return summaryElement; |
| }); |
| |
| // Setup PlotGoogleChartSk |
| const googleChartElement = new PlotGoogleChartSk(); |
| document.body.appendChild(googleChartElement); |
| googleChartElement.data = dt; |
| await googleChartElement.updateComplete; |
| // Allow async updateDataView to finish |
| await new Promise((resolve) => setTimeout(resolve, 0)); |
| await googleChartElement.updateComplete; |
| |
| const traceKey = Object.keys(df.traceset)[0]; |
| const summaryColor = (summaryElement as any).traceColorMap.get(traceKey); |
| const googleChartColor = googleChartElement.traceColorMap.get(traceKey); |
| |
| assert.equal(summaryColor, googleChartColor); |
| |
| document.body.removeChild(googleChartElement); |
| }); |
| }); |
| |
| ['material'].forEach((mode) => |
| describe(`selection in mode ${mode}`, () => { |
| it('select an area', async () => { |
| const element = newEl((el) => { |
| el.style.width = '100px'; |
| }); |
| await element.updateComplete; |
| await chartReady(() => { |
| element.data = dt; |
| return element; |
| }); |
| |
| const header = df.header!, |
| start = 2, |
| end = 6; |
| element.Select(header![start]!, header![end]!); |
| |
| assert.approximately(element.selectedValueRange!.begin, header[start]!.offset, 1e-3); |
| assert.approximately(element.selectedValueRange!.end, header[end]!.offset, 1e-3); |
| }); |
| |
| it('select an area before the chart is ready', async () => { |
| const element = newEl((el) => { |
| el.style.width = '100px'; |
| }); |
| await element.updateComplete; |
| |
| const header = df.header!, |
| start = 2, |
| end = 6; |
| element.data = dt; |
| element.Select(header![start]!, header![end]!); |
| |
| assert.isNull(element['chartLayout']); |
| assert.approximately(element.selectedValueRange!.begin!, header[start]!.offset, 1e-3); |
| assert.approximately(element.selectedValueRange!.end!, header[end]!.offset, 1e-3); |
| |
| await chartReady(() => { |
| return element; |
| }); |
| |
| assert.approximately(element.selectedValueRange!.begin, header[start]!.offset, 1e-3); |
| assert.approximately(element.selectedValueRange!.end, header[end]!.offset, 1e-3); |
| }); |
| |
| it('select an area in date mode', async () => { |
| const element = newEl((el) => { |
| el.style.width = '100px'; |
| // Enable date mode, the underlying data should stay the same. |
| el.domain = 'date'; |
| }); |
| await element.updateComplete; |
| await chartReady(() => { |
| element.data = dt; |
| return element; |
| }); |
| |
| const header = df.header!, |
| start = 3, |
| end = 9; |
| element.Select(header![start]!, header![end]!); |
| assert.approximately(element.selectedValueRange!.begin, header[start]!.timestamp, 1e-3); |
| assert.approximately(element.selectedValueRange!.end, header[end]!.timestamp, 1e-3); |
| }); |
| }) |
| ); |
| |
| describe('performance with downsampling', () => { |
| it('efficiently handles large datasets using min-max bucketing', async () => { |
| const element = newEl((el) => { |
| el.style.width = '100px'; |
| }); |
| await element.updateComplete; |
| |
| // Create a large dataset (e.g., 20,000 rows) |
| const numRows = 20000; |
| const data = new google.visualization.DataTable(); |
| data.addColumn('number', 'offset'); |
| data.addColumn('datetime', 'timestamp'); |
| data.addColumn('number', 'trace1'); |
| |
| const dataRows = []; |
| for (let i = 0; i < numRows; i++) { |
| dataRows.push([i, new Date(now + i * 1000), Math.sin(i / 100) * 100]); |
| } |
| data.addRows(dataRows); |
| |
| const start = performance.now(); |
| |
| // Trigger the update (which includes downsampling logic) |
| element.data = data; |
| // We access the private method directly or trigger it via property change. |
| // Property 'data' change calls 'updateDataView'. |
| |
| // Wait for it to settle? updateDataView is synchronous in logic but might trigger async rendering. |
| // The downsampling calculation happens synchronously in updateDataView. |
| await element.updateComplete; |
| |
| const end = performance.now(); |
| const duration = end - start; |
| assert.isBelow(duration, 200, 'Downsampling should be fast (< 200ms)'); |
| |
| // Verify downsampling occurred by checking the view in the chart |
| const view = (element as any)._viewForTesting; |
| assert.isNotNull(view); |
| const numDownsampledRows = view.getNumberOfRows(); |
| assert.isBelow(numDownsampledRows, numRows, 'Data should be downsampled'); |
| // We expect ~1000 rows (target resolution) |
| assert.isBelow(numDownsampledRows, 1100, 'Downsampled size should be close to target (1000)'); |
| assert.isAbove(numDownsampledRows, 900, 'Downsampled size should be close to target (1000)'); |
| }); |
| }); |
| }); |