diff --git a/public/index.js b/public/index.js index b183c15..cf50bef 100644 --- a/public/index.js +++ b/public/index.js @@ -1,660 +1,665 @@ -const E_TEST_NAME = { - N_X_M: "N line series M points", - SCATTER: "Brownian Motion Scatter Series", - LINE: "Line series which is unsorted in x", - POINT_LINE: "Point series, sorted, updating y-values", - COLUMN: "Column chart with data ascending in X", - CANDLESTICK: "Candlestick series test", - FIFO: "FIFO / ECG Chart Performance Test", - MOUNTAIN: "Mountain Chart Performance Test", - SERIES_COMPRESSION: "Series Compression Test", - MULTI_CHART: "Multi Chart Performance Test", - HEATMAP: "Uniform Heatmap Performance Test", - POINTCLOUD_3D: "3D Point Cloud Performance Test", - SURFACE_3D: "3D Surface Performance Test" -}; -const CHARTS = generateCharts(); -const TESTS = generateTests(); - - -document.addEventListener('DOMContentLoaded', async function () { - await initIndexedDB(); - await buildResultsSection(); - addDownloadButton(); -}); - -function generateCharts () { - const charts = []; - charts.push({ - name: 'SciChart.js', - path: 'scichart/scichart.html' - }); - charts.push({ - name: 'Highcharts', - path: 'highcharts/highcharts.html', - custom: [ - { - path: 'highcharts/highcharts_stock_charts.html', - test: E_TEST_NAME.CANDLESTICK - } - ] - }); - charts.push({ - name: 'Chart.js', - path: 'chartjs/chartjs.html', - custom: [ - { - path: 'chartjs/chartjs_candlestick.html', - test: E_TEST_NAME.CANDLESTICK - } - ] - }); - charts.push({ - name: 'Plotly.js', path: 'plotly/plotly.html' - }); - charts.push({ - name: 'Apache ECharts', path: 'echarts/echarts.html' - }); - charts.push({ - name: 'uPlot', - path: 'uPlot/uPlot.html', - }); - charts.push({ - name: 'ChartGPU', - path: 'chartgpu/chartgpu.html', - }); - return charts; -} - -function generateTests () { - const tests = []; - tests.push(""); - for (key in E_TEST_NAME) { - tests.push(E_TEST_NAME[key]); - } - return tests; -} - - -// Cache for loaded test support data -const testSupportCache = new Map(); - -// IndexedDB setup for results display -let db = null; -const DB_NAME = 'ChartPerformanceResults'; -const DB_VERSION = 1; -const STORE_NAME = 'testResults'; - -async function initIndexedDB() { - return new Promise((resolve, reject) => { - const request = indexedDB.open(DB_NAME, DB_VERSION); - - request.onerror = () => reject(request.error); - request.onsuccess = () => { - db = request.result; - resolve(); - }; - - request.onupgradeneeded = (event) => { - const database = event.target.result; - if (!database.objectStoreNames.contains(STORE_NAME)) { - const store = database.createObjectStore(STORE_NAME, { keyPath: 'id' }); - store.createIndex('chartLibrary', 'chartLibrary', { unique: false }); - store.createIndex('testCase', 'testCase', { unique: false }); - } - }; - }); -} - -async function getAllTestResults() { - console.log('=== getAllTestResults CALLED ==='); - - if (!db) { - console.error('Database not initialized for getAllTestResults'); - return []; - } - - console.log('Database available for retrieval:', !!db); - - try { - const transaction = db.transaction([STORE_NAME], 'readonly'); - const store = transaction.objectStore(STORE_NAME); - - return new Promise((resolve, reject) => { - const request = store.getAll(); - - request.onsuccess = (event) => { - const results = event.target.result || []; - console.log('=== RETRIEVAL SUCCESS ==='); - console.log('Retrieved from IndexedDB:', results.length, 'records'); - - results.forEach((result, index) => { - console.log(`Record ${index + 1}:`, { - id: result.id, - chartLibrary: result.chartLibrary, - testCase: result.testCase, - resultsCount: result.results?.length, - timestamp: result.timestamp, - fullRecord: result - }); - }); - - resolve(results); - }; - - request.onerror = (event) => { - console.error('=== RETRIEVAL ERROR ==='); - console.error('IndexedDB retrieval error:', event.target.error); - reject(event.target.error); - }; - }); - - } catch (error) { - console.error('=== getAllTestResults EXCEPTION ==='); - console.error('Exception in getAllTestResults:', error); - console.error('Exception stack:', error.stack); - return []; - } -} - -async function loadTestSupport(chartName) { - if (testSupportCache.has(chartName)) { - return testSupportCache.get(chartName); - } - - // Map chart names to their test script paths - const scriptPaths = { - 'SciChart.js': 'scichart/scichart_tests.js', - 'Chart.js': 'chartjs/chartjs_tests.js', - 'Highcharts': 'highcharts/highcharts_tests.js', - 'Plotly.js': 'plotly/plotly_tests.js', - 'Apache ECharts': 'echarts/echarts_tests.js', - 'uPlot': 'uPlot/uPlot_tests.js', - 'ChartGPU': 'chartgpu/chartgpu_tests.js' - }; - - const scriptPath = scriptPaths[chartName]; - if (!scriptPath) { - // Unknown chart, assume all tests supported - const allTests = Object.values(E_TEST_NAME); - testSupportCache.set(chartName, allTests); - return allTests; - } - - try { - // Create a temporary script element to load the test file - const script = document.createElement('script'); - script.src = scriptPath; - - // Wait for script to load - await new Promise((resolve, reject) => { - script.onload = resolve; - script.onerror = reject; - document.head.appendChild(script); - }); - - // Try to get supported tests from the loaded script - let supportedTests; - if (typeof window.getSupportedTests === 'function') { - supportedTests = window.getSupportedTests(); - } else { - // Fallback: assume all tests supported if function not found - supportedTests = Object.values(E_TEST_NAME); - } - - // Clean up the script element - document.head.removeChild(script); - - // Cache the result - testSupportCache.set(chartName, supportedTests); - return supportedTests; - - } catch (error) { - console.warn(`Failed to load test support for ${chartName}:`, error); - // Fallback: assume all tests supported - const allTests = Object.values(E_TEST_NAME); - testSupportCache.set(chartName, allTests); - return allTests; - } -} - -function checkTestSupport(chartName, testName) { - // For now, return true and let the async loading happen in buildTestsTable - // This is a synchronous function but we need async loading - return true; -} - -async function buildResultsSection() { - const resultsContainer = document.getElementById('resultsContainer'); - if (!resultsContainer) { - // Create results container if it doesn't exist - const container = document.createElement('div'); - container.id = 'resultsContainer'; - container.style.marginTop = '40px'; - document.body.appendChild(container); - return buildResultsSection(); - } - - // Clear existing content - resultsContainer.innerHTML = '

Test Cases / Results

'; - - try { - const allResults = await getAllTestResults(); - - // Group results by test case - const resultsByTestCase = {}; - allResults.forEach(result => { - console.log('Processing result:', result); - if (!resultsByTestCase[result.testCase]) { - resultsByTestCase[result.testCase] = {}; - } - resultsByTestCase[result.testCase][result.chartLibrary] = result.results; - }); - - console.log('Results grouped by test case:', resultsByTestCase); - - // Load test support data for all charts first - const supportPromises = CHARTS.map(chart => loadTestSupport(chart.name)); - await Promise.all(supportPromises); - - // Create tables for each test case - Object.keys(E_TEST_NAME).forEach(testKey => { - const testName = E_TEST_NAME[testKey]; - const testResults = resultsByTestCase[testName] || {}; - - const section = document.createElement('div'); - section.style.marginBottom = '30px'; - - const heading = document.createElement('h3'); - heading.style.display = 'flex'; - heading.style.alignItems = 'center'; - heading.style.gap = '20px'; - heading.style.marginBottom = '10px'; - - const titleSpan = document.createElement('span'); - titleSpan.textContent = testName; - heading.appendChild(titleSpan); - - // Add RUN buttons for each chart library - const runButtonsContainer = document.createElement('div'); - runButtonsContainer.style.display = 'flex'; - runButtonsContainer.style.gap = '10px'; - runButtonsContainer.style.flexWrap = 'wrap'; - - // Find the test group ID for this test name - const testGroupId = Object.keys(E_TEST_NAME).find(key => E_TEST_NAME[key] === testName); - const testGroupIndex = testGroupId ? Object.keys(E_TEST_NAME).indexOf(testGroupId) + 1 : null; - - CHARTS.forEach(chart => { - // Check if this test is supported by this chart library - const supportedTests = testSupportCache.get(chart.name) || Object.values(E_TEST_NAME); - const isSupported = supportedTests.includes(testName); - - if (isSupported && testGroupIndex) { - let href = chart.path || ''; - - // Check for custom test paths - if (chart.custom && chart.custom.length > 0) { - const customTest = chart.custom.find((customItem) => customItem.test === testName); - if (customTest) { - href = customTest.path; - } - } - - const runLink = document.createElement('a'); - runLink.textContent = `RUN ${chart.name}`; - runLink.className = 'run-test-link'; - runLink.href = `${href}?test_group_id=${testGroupIndex}`; - runLink.target = '_blank'; - runLink.rel = 'noopener noreferrer'; - runLink.style.padding = '5px 10px'; - runLink.style.fontSize = '12px'; - runLink.style.backgroundColor = '#007bff'; - runLink.style.color = 'white'; - runLink.style.border = 'none'; - runLink.style.borderRadius = '3px'; - runLink.style.cursor = 'pointer'; - runLink.style.textDecoration = 'none'; - runLink.style.display = 'inline-block'; - - runLink.addEventListener('mouseenter', () => { - runLink.style.backgroundColor = '#0056b3'; - }); - - runLink.addEventListener('mouseleave', () => { - runLink.style.backgroundColor = '#007bff'; - }); - - runButtonsContainer.appendChild(runLink); - } - }); - - heading.appendChild(runButtonsContainer); - section.appendChild(heading); - - const table = createResultsTable(testName, testResults); - table.classList.add('results-ready') - section.appendChild(table); - - resultsContainer.appendChild(section); - }); - - } catch (error) { - console.error('Failed to build results section:', error); - resultsContainer.innerHTML = '

Test Cases / Results

Error loading results from database.

'; - } -} - -function createResultsTable(testName, testResults) { - const table = document.createElement('table'); - table.style.borderCollapse = 'collapse'; - table.style.width = '100%'; - table.style.marginBottom = '20px'; - - // Create header row - const headerRow = table.insertRow(); - headerRow.style.backgroundColor = '#f0f0f0'; - headerRow.style.fontWeight = 'bold'; - - // Add parameter columns - const paramsHeader = headerRow.insertCell(); - paramsHeader.textContent = 'Parameters'; - paramsHeader.style.border = '1px solid #ccc'; - paramsHeader.style.padding = '8px'; - paramsHeader.style.textAlign = 'left'; - - // Add chart library columns - CHARTS.forEach(chart => { - const cell = headerRow.insertCell(); - cell.textContent = `${chart.name} (Avg FPS)`; - cell.style.border = '1px solid #ccc'; - cell.style.padding = '8px'; - cell.style.textAlign = 'center'; - console.log(`Added header for chart: ${chart.name}`); - }); - - // Get all possible parameter combinations from test configurations - const paramCombinations = new Set(); - - // Add parameter combinations from existing results - Object.values(testResults).forEach(results => { - if (results && Array.isArray(results)) { - results.forEach(result => { - if (result.config) { - const params = `${result.config.points || 0} points, ${result.config.series || 0} series${result.config.charts ? `, ${result.config.charts} charts` : ''}`; - paramCombinations.add(params); - } - }); - } - }); - - // Add all possible parameter combinations from test group configurations - // This ensures we show all test cases even if no results exist yet - const testGroups = { - 1: { name: 'N line series M points', tests: [ - { series: 100, points: 100 }, { series: 500, points: 500 }, { series: 1000, points: 1000 }, - { series: 2000, points: 2000 }, { series: 4000, points: 4000 }, { series: 8000, points: 8000 } - ]}, - 2: { name: 'Brownian Motion Scatter Series', tests: [ - { series: 1, points: 1000 }, { series: 1, points: 10000 }, { series: 1, points: 50000 }, - { series: 1, points: 100000 }, { series: 1, points: 200000 }, { series: 1, points: 500000 }, - { series: 1, points: 1000000 }, { series: 1, points: 5000000 }, { series: 1, points: 10000000 } - ]}, - 3: { name: 'Line series which is unsorted in x', tests: [ - { series: 1, points: 1000 }, { series: 1, points: 10000 }, { series: 1, points: 50000 }, - { series: 1, points: 100000 }, { series: 1, points: 200000 }, { series: 1, points: 500000 }, - { series: 1, points: 1000000 }, { series: 1, points: 5000000 }, { series: 1, points: 10000000 } - ]}, - 4: { name: 'Point series, sorted, updating y-values', tests: [ - { series: 1, points: 1000 }, { series: 1, points: 10000 }, { series: 1, points: 50000 }, - { series: 1, points: 100000 }, { series: 1, points: 200000 }, { series: 1, points: 500000 }, - { series: 1, points: 1000000 }, { series: 1, points: 5000000 }, { series: 1, points: 10000000 } - ]}, - 5: { name: 'Column chart with data ascending in X', tests: [ - { series: 1, points: 1000 }, { series: 1, points: 10000 }, { series: 1, points: 50000 }, - { series: 1, points: 100000 }, { series: 1, points: 200000 }, { series: 1, points: 500000 }, - { series: 1, points: 1000000 }, { series: 1, points: 5000000 }, { series: 1, points: 10000000 } - ]}, - 6: { name: 'Candlestick series test', tests: [ - { series: 1, points: 1000 }, { series: 1, points: 10000 }, { series: 1, points: 50000 }, - { series: 1, points: 100000 }, { series: 1, points: 200000 }, { series: 1, points: 500000 }, - { series: 1, points: 1000000 }, { series: 1, points: 5000000 }, { series: 1, points: 10000000 } - ]}, - 7: { name: 'FIFO / ECG Chart Performance Test', tests: [ - { series: 5, points: 100 }, { series: 5, points: 10000 }, { series: 5, points: 100000 }, - { series: 5, points: 1000000 }, { series: 5, points: 5000000 }, { series: 5, points: 10000000 } - ]}, - 8: { name: 'Mountain Chart Performance Test', tests: [ - { series: 1, points: 1000 }, { series: 1, points: 10000 }, { series: 1, points: 50000 }, - { series: 1, points: 100000 }, { series: 1, points: 200000 }, { series: 1, points: 500000 }, - { series: 1, points: 1000000 }, { series: 1, points: 5000000 }, { series: 1, points: 10000000 } - ]}, - 9: { name: 'Series Compression Test', tests: [ - { series: 1, points: 1000 }, { series: 1, points: 10000 }, { series: 1, points: 100000 }, - { series: 1, points: 1000000 }, { series: 1, points: 10000000 } - ]}, - 10: { name: 'Multi Chart Performance Test', tests: [ - { series: 1, points: 10000, charts: 1 }, { series: 1, points: 10000, charts: 2 }, - { series: 1, points: 10000, charts: 4 }, { series: 1, points: 10000, charts: 8 }, - { series: 1, points: 10000, charts: 16 }, { series: 1, points: 10000, charts: 32 }, - { series: 1, points: 10000, charts: 64 }, { series: 1, points: 10000, charts: 128 } - ]}, - 11: { name: 'Uniform Heatmap Performance Test', tests: [ - { series: 1, points: 100 }, { series: 1, points: 200 }, { series: 1, points: 500 }, - { series: 1, points: 1000 }, { series: 1, points: 2000 }, { series: 1, points: 4000 }, - { series: 1, points: 8000 }, { series: 1, points: 16000 } - ]}, - 12: { name: '3D Point Cloud Performance Test', tests: [ - { series: 1, points: 100 }, { series: 1, points: 1000 }, { series: 1, points: 10000 }, - { series: 1, points: 100000 }, { series: 1, points: 1000000 }, { series: 1, points: 2000000 }, - { series: 1, points: 4000000 } - ]}, - 13: { name: '3D Surface Performance Test', tests: [ - { series: 1, points: 100 }, { series: 1, points: 200 }, { series: 1, points: 500 }, - { series: 1, points: 1000 }, { series: 1, points: 2000 }, { series: 1, points: 4000 }, - { series: 1, points: 8000 } - ]} - }; - - // Find the matching test group and add all its parameter combinations - Object.values(testGroups).forEach(group => { - if (group.name === testName) { - group.tests.forEach(test => { - const params = `${test.points || 0} points, ${test.series || 0} series${test.charts ? `, ${test.charts} charts` : ''}`; - paramCombinations.add(params); - }); - } - }); - - // Convert to sorted array - const sortedParams = Array.from(paramCombinations).sort((a, b) => { - // Extract point count for sorting - const aPoints = parseInt(a.match(/(\d+) points/)?.[1] || '0'); - const bPoints = parseInt(b.match(/(\d+) points/)?.[1] || '0'); - return aPoints - bPoints; - }); - - // Collect all FPS values for heatmap calculation - const allFpsValues = []; - Object.values(testResults).forEach(results => { - if (results && Array.isArray(results)) { - results.forEach(result => { - if (result.averageFPS && result.averageFPS > 0) { - allFpsValues.push(result.averageFPS); - } - }); - } - }); - - const minFps = allFpsValues.length > 0 ? Math.min(...allFpsValues) : 0; - const maxFps = allFpsValues.length > 0 ? Math.max(...allFpsValues) : 100; - - // Create data rows - sortedParams.forEach(paramStr => { - const row = table.insertRow(); - - // Parameters cell - const paramCell = row.insertCell(); - paramCell.textContent = paramStr; - paramCell.style.border = '1px solid #ccc'; - paramCell.style.padding = '8px'; - paramCell.style.fontWeight = 'bold'; - - // Chart library cells - CHARTS.forEach(chart => { - const cell = row.insertCell(); - cell.style.border = '1px solid #ccc'; - cell.style.padding = '8px'; - cell.style.textAlign = 'center'; - - // Find matching result for this chart and parameters - // Try both exact chart name and chart name with version - let chartResults = testResults[chart.name]; - if (!chartResults) { - // Try to find by partial match (chart name might include version) - const chartKey = Object.keys(testResults).find(key => key.startsWith(chart.name)); - if (chartKey) { - chartResults = testResults[chartKey]; - console.log(`Found results using partial match: ${chartKey} for ${chart.name}`); - } - } - let fps = null; - - console.log(`Looking for results for ${chart.name}, paramStr: ${paramStr}`); - console.log('Chart results:', chartResults); - - if (chartResults && Array.isArray(chartResults)) { - console.log(`Found ${chartResults.length} results for ${chart.name}`); - const matchingResult = chartResults.find(result => { - if (!result.config) { - console.log('Result has no config:', result); - return false; - } - const resultParams = `${result.config.points || 0} points, ${result.config.series || 0} series${result.config.charts ? `, ${result.config.charts} charts` : ''}`; - console.log(`Comparing "${resultParams}" with "${paramStr}"`); - return resultParams === paramStr; - }); - - console.log('Matching result:', matchingResult); - if (matchingResult) { - // Check if the result has an error condition - if (matchingResult.isErrored && matchingResult.errorReason) { - cell.textContent = matchingResult.errorReason; - cell.style.backgroundColor = '#ffcccc'; // Red background for errors - cell.style.color = '#cc0000'; // Dark red text - cell.style.fontWeight = 'bold'; - } else if (matchingResult.averageFPS) { - fps = matchingResult.averageFPS; - console.log(`Found FPS: ${fps}`); - } - } - } else { - console.log(`No chart results found for ${chart.name} or not an array`); - } - - if (fps !== null) { - cell.textContent = fps.toFixed(2); - // Apply heatmap colouring - cell.style.backgroundColor = getFpsHeatmapColor(fps, minFps, maxFps); - } else if (!cell.textContent) { // Only set default if no error message was set - cell.textContent = '-'; - cell.style.backgroundColor = '#f9f9f9'; - cell.style.color = '#999'; - } - }); - }); - - return table; -} - -function getFpsHeatmapColor(fps, minFps, maxFps) { - if (fps === null || fps === undefined) return 'transparent'; - - // Use 60 FPS as the maximum for green colouring - const targetMaxFps = 60; - - // Normalise FPS to 0-1 range, capping at 60 FPS - const normalised = Math.min(fps / targetMaxFps, 1); - - // Create gradient: red (0 FPS) -> orange (30 FPS) -> green (60+ FPS) - let red, green, blue; - - if (normalised < 0.5) { - // Red to Orange (0 to 30 FPS) - const t = normalised * 2; // 0 to 1 - red = 255; - green = Math.round(165 * t); // 0 to 165 (orange) - blue = 0; - } else { - // Orange to Green (30 to 60+ FPS) - const t = (normalised - 0.5) * 2; // 0 to 1 - red = Math.round(255 * (1 - t)); // 255 to 0 - green = Math.round(165 + (90 * t)); // 165 to 255 - blue = 0; - } - - // Add alpha for readability - return `rgba(${red}, ${green}, ${blue}, 0.6)`; -} - -function addDownloadButton() { - const buttonContainer = document.getElementById('downloadButtonContainer'); - if (!buttonContainer) return; - - // Create download button - const downloadButton = document.createElement('button'); - downloadButton.textContent = 'Download Results JSON'; - downloadButton.style.padding = '10px 20px'; - downloadButton.style.fontSize = '14px'; - downloadButton.style.backgroundColor = '#28a745'; - downloadButton.style.color = 'white'; - downloadButton.style.border = 'none'; - downloadButton.style.borderRadius = '4px'; - downloadButton.style.cursor = 'pointer'; - downloadButton.style.fontWeight = 'bold'; - - downloadButton.addEventListener('mouseenter', () => { - downloadButton.style.backgroundColor = '#218838'; - }); - - downloadButton.addEventListener('mouseleave', () => { - downloadButton.style.backgroundColor = '#28a745'; - }); - - downloadButton.addEventListener('click', async () => { - try { - const allResults = await getAllTestResults(); - - if (allResults.length === 0) { - alert('No results available to download.'); - return; - } - - // Create JSON blob - const jsonData = JSON.stringify(allResults, null, 2); - const blob = new Blob([jsonData], { type: 'application/json' }); - - // Create download link - const url = URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = url; - - // Generate filename with timestamp - const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); - link.download = `chart-performance-results-${timestamp}.json`; - - // Trigger download - document.body.appendChild(link); - link.click(); - - // Cleanup - document.body.removeChild(link); - URL.revokeObjectURL(url); - } catch (error) { - console.error('Failed to download results:', error); - alert('Failed to download results. Check console for details.'); - } - }); - - buttonContainer.appendChild(downloadButton); -} +const E_TEST_NAME = { + N_X_M: "N line series M points", + SCATTER: "Brownian Motion Scatter Series", + LINE: "Line series which is unsorted in x", + POINT_LINE: "Point series, sorted, updating y-values", + COLUMN: "Column chart with data ascending in X", + CANDLESTICK: "Candlestick series test", + FIFO: "FIFO / ECG Chart Performance Test", + MOUNTAIN: "Mountain Chart Performance Test", + SERIES_COMPRESSION: "Series Compression Test", + MULTI_CHART: "Multi Chart Performance Test", + HEATMAP: "Uniform Heatmap Performance Test", + POINTCLOUD_3D: "3D Point Cloud Performance Test", + SURFACE_3D: "3D Surface Performance Test" +}; +const CHARTS = generateCharts(); +const TESTS = generateTests(); + + +document.addEventListener('DOMContentLoaded', async function () { + await initIndexedDB(); + await buildResultsSection(); + addDownloadButton(); +}); + +function generateCharts () { + const charts = []; + charts.push({ + name: 'SciChart.js', + path: 'scichart/scichart.html' + }); + charts.push({ + name: 'Highcharts', + path: 'highcharts/highcharts.html', + custom: [ + { + path: 'highcharts/highcharts_stock_charts.html', + test: E_TEST_NAME.CANDLESTICK + } + ] + }); + charts.push({ + name: 'Chart.js', + path: 'chartjs/chartjs.html', + custom: [ + { + path: 'chartjs/chartjs_candlestick.html', + test: E_TEST_NAME.CANDLESTICK + } + ] + }); + charts.push({ + name: 'Plotly.js', path: 'plotly/plotly.html' + }); + charts.push({ + name: 'Apache ECharts', path: 'echarts/echarts.html' + }); + charts.push({ + name: 'uPlot', + path: 'uPlot/uPlot.html', + }); + charts.push({ + name: 'ChartGPU', + path: 'chartgpu/chartgpu.html', + }); + charts.push({ + name: 'Lcjs', + path: 'lcjsv4/lcjs.html' + }); + return charts; +} + +function generateTests () { + const tests = []; + tests.push(""); + for (key in E_TEST_NAME) { + tests.push(E_TEST_NAME[key]); + } + return tests; +} + + +// Cache for loaded test support data +const testSupportCache = new Map(); + +// IndexedDB setup for results display +let db = null; +const DB_NAME = 'ChartPerformanceResults'; +const DB_VERSION = 1; +const STORE_NAME = 'testResults'; + +async function initIndexedDB() { + return new Promise((resolve, reject) => { + const request = indexedDB.open(DB_NAME, DB_VERSION); + + request.onerror = () => reject(request.error); + request.onsuccess = () => { + db = request.result; + resolve(); + }; + + request.onupgradeneeded = (event) => { + const database = event.target.result; + if (!database.objectStoreNames.contains(STORE_NAME)) { + const store = database.createObjectStore(STORE_NAME, { keyPath: 'id' }); + store.createIndex('chartLibrary', 'chartLibrary', { unique: false }); + store.createIndex('testCase', 'testCase', { unique: false }); + } + }; + }); +} + +async function getAllTestResults() { + console.log('=== getAllTestResults CALLED ==='); + + if (!db) { + console.error('Database not initialized for getAllTestResults'); + return []; + } + + console.log('Database available for retrieval:', !!db); + + try { + const transaction = db.transaction([STORE_NAME], 'readonly'); + const store = transaction.objectStore(STORE_NAME); + + return new Promise((resolve, reject) => { + const request = store.getAll(); + + request.onsuccess = (event) => { + const results = event.target.result || []; + console.log('=== RETRIEVAL SUCCESS ==='); + console.log('Retrieved from IndexedDB:', results.length, 'records'); + + results.forEach((result, index) => { + console.log(`Record ${index + 1}:`, { + id: result.id, + chartLibrary: result.chartLibrary, + testCase: result.testCase, + resultsCount: result.results?.length, + timestamp: result.timestamp, + fullRecord: result + }); + }); + + resolve(results); + }; + + request.onerror = (event) => { + console.error('=== RETRIEVAL ERROR ==='); + console.error('IndexedDB retrieval error:', event.target.error); + reject(event.target.error); + }; + }); + + } catch (error) { + console.error('=== getAllTestResults EXCEPTION ==='); + console.error('Exception in getAllTestResults:', error); + console.error('Exception stack:', error.stack); + return []; + } +} + +async function loadTestSupport(chartName) { + if (testSupportCache.has(chartName)) { + return testSupportCache.get(chartName); + } + + // Map chart names to their test script paths + const scriptPaths = { + 'SciChart.js': 'scichart/scichart_tests.js', + 'Chart.js': 'chartjs/chartjs_tests.js', + 'Highcharts': 'highcharts/highcharts_tests.js', + 'Plotly.js': 'plotly/plotly_tests.js', + 'Apache ECharts': 'echarts/echarts_tests.js', + 'uPlot': 'uPlot/uPlot_tests.js', + 'ChartGPU': 'chartgpu/chartgpu_tests.js', + 'Lcjs': 'lcjsv4/lcjs_tests.js' + }; + + const scriptPath = scriptPaths[chartName]; + if (!scriptPath) { + // Unknown chart, assume all tests supported + const allTests = Object.values(E_TEST_NAME); + testSupportCache.set(chartName, allTests); + return allTests; + } + + try { + // Create a temporary script element to load the test file + const script = document.createElement('script'); + script.src = scriptPath; + + // Wait for script to load + await new Promise((resolve, reject) => { + script.onload = resolve; + script.onerror = reject; + document.head.appendChild(script); + }); + + // Try to get supported tests from the loaded script + let supportedTests; + if (typeof window.getSupportedTests === 'function') { + supportedTests = window.getSupportedTests(); + } else { + // Fallback: assume all tests supported if function not found + supportedTests = Object.values(E_TEST_NAME); + } + + // Clean up the script element + document.head.removeChild(script); + + // Cache the result + testSupportCache.set(chartName, supportedTests); + return supportedTests; + + } catch (error) { + console.warn(`Failed to load test support for ${chartName}:`, error); + // Fallback: assume all tests supported + const allTests = Object.values(E_TEST_NAME); + testSupportCache.set(chartName, allTests); + return allTests; + } +} + +function checkTestSupport(chartName, testName) { + // For now, return true and let the async loading happen in buildTestsTable + // This is a synchronous function but we need async loading + return true; +} + +async function buildResultsSection() { + const resultsContainer = document.getElementById('resultsContainer'); + if (!resultsContainer) { + // Create results container if it doesn't exist + const container = document.createElement('div'); + container.id = 'resultsContainer'; + container.style.marginTop = '40px'; + document.body.appendChild(container); + return buildResultsSection(); + } + + // Clear existing content + resultsContainer.innerHTML = '

Test Cases / Results

'; + + try { + const allResults = await getAllTestResults(); + + // Group results by test case + const resultsByTestCase = {}; + allResults.forEach(result => { + console.log('Processing result:', result); + if (!resultsByTestCase[result.testCase]) { + resultsByTestCase[result.testCase] = {}; + } + resultsByTestCase[result.testCase][result.chartLibrary] = result.results; + }); + + console.log('Results grouped by test case:', resultsByTestCase); + + // Load test support data for all charts first + const supportPromises = CHARTS.map(chart => loadTestSupport(chart.name)); + await Promise.all(supportPromises); + + // Create tables for each test case + Object.keys(E_TEST_NAME).forEach(testKey => { + const testName = E_TEST_NAME[testKey]; + const testResults = resultsByTestCase[testName] || {}; + + const section = document.createElement('div'); + section.style.marginBottom = '30px'; + + const heading = document.createElement('h3'); + heading.style.display = 'flex'; + heading.style.alignItems = 'center'; + heading.style.gap = '20px'; + heading.style.marginBottom = '10px'; + + const titleSpan = document.createElement('span'); + titleSpan.textContent = testName; + heading.appendChild(titleSpan); + + // Add RUN buttons for each chart library + const runButtonsContainer = document.createElement('div'); + runButtonsContainer.style.display = 'flex'; + runButtonsContainer.style.gap = '10px'; + runButtonsContainer.style.flexWrap = 'wrap'; + + // Find the test group ID for this test name + const testGroupId = Object.keys(E_TEST_NAME).find(key => E_TEST_NAME[key] === testName); + const testGroupIndex = testGroupId ? Object.keys(E_TEST_NAME).indexOf(testGroupId) + 1 : null; + + CHARTS.forEach(chart => { + // Check if this test is supported by this chart library + const supportedTests = testSupportCache.get(chart.name) || Object.values(E_TEST_NAME); + const isSupported = supportedTests.includes(testName); + + if (isSupported && testGroupIndex) { + let href = chart.path || ''; + + // Check for custom test paths + if (chart.custom && chart.custom.length > 0) { + const customTest = chart.custom.find((customItem) => customItem.test === testName); + if (customTest) { + href = customTest.path; + } + } + + const runLink = document.createElement('a'); + runLink.textContent = `RUN ${chart.name}`; + runLink.className = 'run-test-link'; + runLink.href = `${href}?test_group_id=${testGroupIndex}`; + runLink.target = '_blank'; + runLink.rel = 'noopener noreferrer'; + runLink.style.padding = '5px 10px'; + runLink.style.fontSize = '12px'; + runLink.style.backgroundColor = '#007bff'; + runLink.style.color = 'white'; + runLink.style.border = 'none'; + runLink.style.borderRadius = '3px'; + runLink.style.cursor = 'pointer'; + runLink.style.textDecoration = 'none'; + runLink.style.display = 'inline-block'; + + runLink.addEventListener('mouseenter', () => { + runLink.style.backgroundColor = '#0056b3'; + }); + + runLink.addEventListener('mouseleave', () => { + runLink.style.backgroundColor = '#007bff'; + }); + + runButtonsContainer.appendChild(runLink); + } + }); + + heading.appendChild(runButtonsContainer); + section.appendChild(heading); + + const table = createResultsTable(testName, testResults); + table.classList.add('results-ready') + section.appendChild(table); + + resultsContainer.appendChild(section); + }); + + } catch (error) { + console.error('Failed to build results section:', error); + resultsContainer.innerHTML = '

Test Cases / Results

Error loading results from database.

'; + } +} + +function createResultsTable(testName, testResults) { + const table = document.createElement('table'); + table.style.borderCollapse = 'collapse'; + table.style.width = '100%'; + table.style.marginBottom = '20px'; + + // Create header row + const headerRow = table.insertRow(); + headerRow.style.backgroundColor = '#f0f0f0'; + headerRow.style.fontWeight = 'bold'; + + // Add parameter columns + const paramsHeader = headerRow.insertCell(); + paramsHeader.textContent = 'Parameters'; + paramsHeader.style.border = '1px solid #ccc'; + paramsHeader.style.padding = '8px'; + paramsHeader.style.textAlign = 'left'; + + // Add chart library columns + CHARTS.forEach(chart => { + const cell = headerRow.insertCell(); + cell.textContent = `${chart.name} (Avg FPS)`; + cell.style.border = '1px solid #ccc'; + cell.style.padding = '8px'; + cell.style.textAlign = 'center'; + console.log(`Added header for chart: ${chart.name}`); + }); + + // Get all possible parameter combinations from test configurations + const paramCombinations = new Set(); + + // Add parameter combinations from existing results + Object.values(testResults).forEach(results => { + if (results && Array.isArray(results)) { + results.forEach(result => { + if (result.config) { + const params = `${result.config.points || 0} points, ${result.config.series || 0} series${result.config.charts ? `, ${result.config.charts} charts` : ''}`; + paramCombinations.add(params); + } + }); + } + }); + + // Add all possible parameter combinations from test group configurations + // This ensures we show all test cases even if no results exist yet + const testGroups = { + 1: { name: 'N line series M points', tests: [ + { series: 100, points: 100 }, { series: 500, points: 500 }, { series: 1000, points: 1000 }, + { series: 2000, points: 2000 }, { series: 4000, points: 4000 }, { series: 8000, points: 8000 } + ]}, + 2: { name: 'Brownian Motion Scatter Series', tests: [ + { series: 1, points: 1000 }, { series: 1, points: 10000 }, { series: 1, points: 50000 }, + { series: 1, points: 100000 }, { series: 1, points: 200000 }, { series: 1, points: 500000 }, + { series: 1, points: 1000000 }, { series: 1, points: 5000000 }, { series: 1, points: 10000000 } + ]}, + 3: { name: 'Line series which is unsorted in x', tests: [ + { series: 1, points: 1000 }, { series: 1, points: 10000 }, { series: 1, points: 50000 }, + { series: 1, points: 100000 }, { series: 1, points: 200000 }, { series: 1, points: 500000 }, + { series: 1, points: 1000000 }, { series: 1, points: 5000000 }, { series: 1, points: 10000000 } + ]}, + 4: { name: 'Point series, sorted, updating y-values', tests: [ + { series: 1, points: 1000 }, { series: 1, points: 10000 }, { series: 1, points: 50000 }, + { series: 1, points: 100000 }, { series: 1, points: 200000 }, { series: 1, points: 500000 }, + { series: 1, points: 1000000 }, { series: 1, points: 5000000 }, { series: 1, points: 10000000 } + ]}, + 5: { name: 'Column chart with data ascending in X', tests: [ + { series: 1, points: 1000 }, { series: 1, points: 10000 }, { series: 1, points: 50000 }, + { series: 1, points: 100000 }, { series: 1, points: 200000 }, { series: 1, points: 500000 }, + { series: 1, points: 1000000 }, { series: 1, points: 5000000 }, { series: 1, points: 10000000 } + ]}, + 6: { name: 'Candlestick series test', tests: [ + { series: 1, points: 1000 }, { series: 1, points: 10000 }, { series: 1, points: 50000 }, + { series: 1, points: 100000 }, { series: 1, points: 200000 }, { series: 1, points: 500000 }, + { series: 1, points: 1000000 }, { series: 1, points: 5000000 }, { series: 1, points: 10000000 } + ]}, + 7: { name: 'FIFO / ECG Chart Performance Test', tests: [ + { series: 5, points: 100 }, { series: 5, points: 10000 }, { series: 5, points: 100000 }, + { series: 5, points: 1000000 }, { series: 5, points: 5000000 }, { series: 5, points: 10000000 } + ]}, + 8: { name: 'Mountain Chart Performance Test', tests: [ + { series: 1, points: 1000 }, { series: 1, points: 10000 }, { series: 1, points: 50000 }, + { series: 1, points: 100000 }, { series: 1, points: 200000 }, { series: 1, points: 500000 }, + { series: 1, points: 1000000 }, { series: 1, points: 5000000 }, { series: 1, points: 10000000 } + ]}, + 9: { name: 'Series Compression Test', tests: [ + { series: 1, points: 1000 }, { series: 1, points: 10000 }, { series: 1, points: 100000 }, + { series: 1, points: 1000000 }, { series: 1, points: 10000000 } + ]}, + 10: { name: 'Multi Chart Performance Test', tests: [ + { series: 1, points: 10000, charts: 1 }, { series: 1, points: 10000, charts: 2 }, + { series: 1, points: 10000, charts: 4 }, { series: 1, points: 10000, charts: 8 }, + { series: 1, points: 10000, charts: 16 }, { series: 1, points: 10000, charts: 32 }, + { series: 1, points: 10000, charts: 64 }, { series: 1, points: 10000, charts: 128 } + ]}, + 11: { name: 'Uniform Heatmap Performance Test', tests: [ + { series: 1, points: 100 }, { series: 1, points: 200 }, { series: 1, points: 500 }, + { series: 1, points: 1000 }, { series: 1, points: 2000 }, { series: 1, points: 4000 }, + { series: 1, points: 8000 }, { series: 1, points: 16000 } + ]}, + 12: { name: '3D Point Cloud Performance Test', tests: [ + { series: 1, points: 100 }, { series: 1, points: 1000 }, { series: 1, points: 10000 }, + { series: 1, points: 100000 }, { series: 1, points: 1000000 }, { series: 1, points: 2000000 }, + { series: 1, points: 4000000 } + ]}, + 13: { name: '3D Surface Performance Test', tests: [ + { series: 1, points: 100 }, { series: 1, points: 200 }, { series: 1, points: 500 }, + { series: 1, points: 1000 }, { series: 1, points: 2000 }, { series: 1, points: 4000 }, + { series: 1, points: 8000 } + ]} + }; + + // Find the matching test group and add all its parameter combinations + Object.values(testGroups).forEach(group => { + if (group.name === testName) { + group.tests.forEach(test => { + const params = `${test.points || 0} points, ${test.series || 0} series${test.charts ? `, ${test.charts} charts` : ''}`; + paramCombinations.add(params); + }); + } + }); + + // Convert to sorted array + const sortedParams = Array.from(paramCombinations).sort((a, b) => { + // Extract point count for sorting + const aPoints = parseInt(a.match(/(\d+) points/)?.[1] || '0'); + const bPoints = parseInt(b.match(/(\d+) points/)?.[1] || '0'); + return aPoints - bPoints; + }); + + // Collect all FPS values for heatmap calculation + const allFpsValues = []; + Object.values(testResults).forEach(results => { + if (results && Array.isArray(results)) { + results.forEach(result => { + if (result.averageFPS && result.averageFPS > 0) { + allFpsValues.push(result.averageFPS); + } + }); + } + }); + + const minFps = allFpsValues.length > 0 ? Math.min(...allFpsValues) : 0; + const maxFps = allFpsValues.length > 0 ? Math.max(...allFpsValues) : 100; + + // Create data rows + sortedParams.forEach(paramStr => { + const row = table.insertRow(); + + // Parameters cell + const paramCell = row.insertCell(); + paramCell.textContent = paramStr; + paramCell.style.border = '1px solid #ccc'; + paramCell.style.padding = '8px'; + paramCell.style.fontWeight = 'bold'; + + // Chart library cells + CHARTS.forEach(chart => { + const cell = row.insertCell(); + cell.style.border = '1px solid #ccc'; + cell.style.padding = '8px'; + cell.style.textAlign = 'center'; + + // Find matching result for this chart and parameters + // Try both exact chart name and chart name with version + let chartResults = testResults[chart.name]; + if (!chartResults) { + // Try to find by partial match (chart name might include version) + const chartKey = Object.keys(testResults).find(key => key.startsWith(chart.name)); + if (chartKey) { + chartResults = testResults[chartKey]; + console.log(`Found results using partial match: ${chartKey} for ${chart.name}`); + } + } + let fps = null; + + console.log(`Looking for results for ${chart.name}, paramStr: ${paramStr}`); + console.log('Chart results:', chartResults); + + if (chartResults && Array.isArray(chartResults)) { + console.log(`Found ${chartResults.length} results for ${chart.name}`); + const matchingResult = chartResults.find(result => { + if (!result.config) { + console.log('Result has no config:', result); + return false; + } + const resultParams = `${result.config.points || 0} points, ${result.config.series || 0} series${result.config.charts ? `, ${result.config.charts} charts` : ''}`; + console.log(`Comparing "${resultParams}" with "${paramStr}"`); + return resultParams === paramStr; + }); + + console.log('Matching result:', matchingResult); + if (matchingResult) { + // Check if the result has an error condition + if (matchingResult.isErrored && matchingResult.errorReason) { + cell.textContent = matchingResult.errorReason; + cell.style.backgroundColor = '#ffcccc'; // Red background for errors + cell.style.color = '#cc0000'; // Dark red text + cell.style.fontWeight = 'bold'; + } else if (matchingResult.averageFPS) { + fps = matchingResult.averageFPS; + console.log(`Found FPS: ${fps}`); + } + } + } else { + console.log(`No chart results found for ${chart.name} or not an array`); + } + + if (fps !== null) { + cell.textContent = fps.toFixed(2); + // Apply heatmap colouring + cell.style.backgroundColor = getFpsHeatmapColor(fps, minFps, maxFps); + } else if (!cell.textContent) { // Only set default if no error message was set + cell.textContent = '-'; + cell.style.backgroundColor = '#f9f9f9'; + cell.style.color = '#999'; + } + }); + }); + + return table; +} + +function getFpsHeatmapColor(fps, minFps, maxFps) { + if (fps === null || fps === undefined) return 'transparent'; + + // Use 60 FPS as the maximum for green colouring + const targetMaxFps = 60; + + // Normalise FPS to 0-1 range, capping at 60 FPS + const normalised = Math.min(fps / targetMaxFps, 1); + + // Create gradient: red (0 FPS) -> orange (30 FPS) -> green (60+ FPS) + let red, green, blue; + + if (normalised < 0.5) { + // Red to Orange (0 to 30 FPS) + const t = normalised * 2; // 0 to 1 + red = 255; + green = Math.round(165 * t); // 0 to 165 (orange) + blue = 0; + } else { + // Orange to Green (30 to 60+ FPS) + const t = (normalised - 0.5) * 2; // 0 to 1 + red = Math.round(255 * (1 - t)); // 255 to 0 + green = Math.round(165 + (90 * t)); // 165 to 255 + blue = 0; + } + + // Add alpha for readability + return `rgba(${red}, ${green}, ${blue}, 0.6)`; +} + +function addDownloadButton() { + const buttonContainer = document.getElementById('downloadButtonContainer'); + if (!buttonContainer) return; + + // Create download button + const downloadButton = document.createElement('button'); + downloadButton.textContent = 'Download Results JSON'; + downloadButton.style.padding = '10px 20px'; + downloadButton.style.fontSize = '14px'; + downloadButton.style.backgroundColor = '#28a745'; + downloadButton.style.color = 'white'; + downloadButton.style.border = 'none'; + downloadButton.style.borderRadius = '4px'; + downloadButton.style.cursor = 'pointer'; + downloadButton.style.fontWeight = 'bold'; + + downloadButton.addEventListener('mouseenter', () => { + downloadButton.style.backgroundColor = '#218838'; + }); + + downloadButton.addEventListener('mouseleave', () => { + downloadButton.style.backgroundColor = '#28a745'; + }); + + downloadButton.addEventListener('click', async () => { + try { + const allResults = await getAllTestResults(); + + if (allResults.length === 0) { + alert('No results available to download.'); + return; + } + + // Create JSON blob + const jsonData = JSON.stringify(allResults, null, 2); + const blob = new Blob([jsonData], { type: 'application/json' }); + + // Create download link + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + + // Generate filename with timestamp + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + link.download = `chart-performance-results-${timestamp}.json`; + + // Trigger download + document.body.appendChild(link); + link.click(); + + // Cleanup + document.body.removeChild(link); + URL.revokeObjectURL(url); + } catch (error) { + console.error('Failed to download results:', error); + alert('Failed to download results. Check console for details.'); + } + }); + + buttonContainer.appendChild(downloadButton); +} diff --git a/public/lcjsv4/README.md b/public/lcjsv4/README.md new file mode 100644 index 0000000..7578929 --- /dev/null +++ b/public/lcjsv4/README.md @@ -0,0 +1,24 @@ +# Performance Test LightningChart JS v4.2.2 + +## Library Information + +LightningChart JS (LCJS) is loaded from CDN: +- CDN URL: https://cdn.jsdelivr.net/npm/@lightningchart/lcjs@4.2.2/dist/lcjs.iife.js +- Version: 4.2.2 +- Format: IIFE (Immediately Invoked Function Expression) + +## Supported Tests + +- N line series M points +- Brownian Motion Scatter Series +- Line series which is unsorted in x +- Point series, sorted, updating y-values +- Column chart with data ascending in X +- Candlestick series test +- FIFO / ECG Chart Performance Test +- Mountain Chart Performance Test +- Series Compression Test +- Multi Chart Performance Test +- Uniform Heatmap Performance Test +- 3D Point Cloud Performance Test +- 3D Surface Performance Test diff --git a/public/lcjsv4/lcjs.html b/public/lcjsv4/lcjs.html new file mode 100644 index 0000000..71617dd --- /dev/null +++ b/public/lcjsv4/lcjs.html @@ -0,0 +1,22 @@ + + + + + Stress Test LightningChart JS v4 + + + + + + + + +

Stress Test LightningChart JS v4

+
+
+
+
+
+ Download Result + + diff --git a/public/lcjsv4/lcjs_tests.js b/public/lcjsv4/lcjs_tests.js new file mode 100644 index 0000000..be30b65 --- /dev/null +++ b/public/lcjsv4/lcjs_tests.js @@ -0,0 +1,1293 @@ +'use strict'; + +function eLibName() { + return 'Lcjs'; +} + +function eLibVersion() { + return '4.2.2'; +} + +function getSupportedTests() { + return [ + "N line series M points", + "Brownian Motion Scatter Series", + "Line series which is unsorted in x", + "Point series, sorted, updating y-values", + "Column chart with data ascending in X", + "Candlestick series test", + "FIFO / ECG Chart Performance Test", + "Mountain Chart Performance Test", + "Series Compression Test", + "Multi Chart Performance Test", + "Uniform Heatmap Performance Test", + "3D Point Cloud Performance Test", + "3D Surface Performance Test" + ]; +} + +/** + * LINE_PERFORMANCE_TEST + * @param seriesNum + * @param pointsNum + * @returns {{appendData: ()=>void, deleteChart: ()=>void, updateChart: ()=>void, createChart: () => Promise, generateData: () => void}} + */ +function eLinePerformanceTest(seriesNum, pointsNum) { + const { lightningChart, SolidLine, SolidFill, ColorRGBA, emptyFill, disableThemeEffects, Themes } = lcjs; + + let chart; + let delta; + let DATA; + let yAxis; + + const createChart = async () => { + chart = lightningChart() + .ChartXY({ + container: document.getElementById('chart-root'), + disableAnimations: true, + theme: disableThemeEffects(Themes.darkGold) + }) + .setTitleFillStyle(emptyFill) + .setPadding({ top: 10, bottom: 10, left: 10, right: 10 }); + const xAxis = chart.getDefaultAxisX().setAnimationScroll(undefined); + yAxis = chart.getDefaultAxisY().setAnimationScroll(undefined); + }; + + const generateData = () => { + const xyValuesArrArr = []; + const colorValuesArr = []; + for (let i = 0; i < seriesNum; i++) { + xyValuesArrArr.push([]); + const r = Math.random() * 255; + const g = Math.random() * 255; + const b = Math.random() * 255; + colorValuesArr.push(new ColorRGBA(r, g, b)); + + // Generate points + let prevYValue = 0; + for (let j = 0; j < pointsNum; j++) { + const curYValue = Math.random() * 10 - 5; + const x = j; + const y = prevYValue + curYValue; + xyValuesArrArr[i].push({ x, y }); + prevYValue += curYValue; + } + } + + DATA = { xyValuesArrArr, colorValuesArr }; + }; + + const appendData = () => { + const { xyValuesArrArr, colorValuesArr } = DATA; + + for (let i = 0; i < seriesNum; i++) { + chart + .addLineSeries({ + dataPattern: { + pattern: 'ProgressiveX', + regularProgressiveStep: true, + }, + }) + .setCursorEnabled(false) + .setStrokeStyle( + new SolidLine({ + thickness: 2, + fillStyle: new SolidFill({ color: colorValuesArr[i] }), + }) + ) + .add(xyValuesArrArr[i]); + } + + yAxis.fit(); + const min = yAxis.getInterval().start; + const max = yAxis.getInterval().end; + const maxVal = Math.max(Math.abs(min), Math.abs(max)); + delta = maxVal / 300; + }; + + const updateChart = (_frame) => { + const currentInterval = yAxis.getInterval(); + yAxis.setInterval({ start: currentInterval.start - delta, end: currentInterval.end + delta }); + return seriesNum * DATA.xyValuesArrArr[0].length; + }; + + const deleteChart = () => { + chart?.dispose(); + const chartDiv = document.getElementById('chart-root'); + chartDiv.innerHTML = ''; + }; + + return { + createChart, + generateData, + appendData, + updateChart, + deleteChart, + }; +} + +/** + * SCATTER_PERFORMANCE_TEST + * @param seriesNum + * @param pointsNum + * @returns {{appendData: ()=>void, deleteChart: ()=>void, updateChart: ()=>void, createChart: () => Promise, generateData: () => void}} + */ +function eScatterPerformanceTest(seriesNum, pointsNum) { + const { lightningChart, emptyFill, PointShape, SolidFill, ColorRGBA, disableThemeEffects, Themes } = lcjs; + + let DATA; + let chart; + let yAxis; + let scatterSeries; + + const X_MAX = 100; + const Y_MAX = 50; + + const createChart = async () => { + const EXTRA = 10; + chart = lightningChart() + .ChartXY({ + container: document.getElementById('chart-root'), + disableAnimations: true, + theme: disableThemeEffects(Themes.darkGold) + }) + .setPadding({ top: 10, bottom: 10, left: 10, right: 10 }) + .setTitleFillStyle(emptyFill); + const xAxis = chart.getDefaultAxisX().setAnimationScroll(undefined); + xAxis.setInterval({ start: 0 - EXTRA, end: X_MAX + EXTRA }); + yAxis = chart.getDefaultAxisY().setAnimationScroll(undefined); + yAxis.setInterval({ start: 0 - EXTRA, end: Y_MAX + EXTRA }); + }; + + let newXYValuesArr = []; + const generateNextPoints = (pointsNum$, xyValuesArr) => { + // Every frame we call generateNextPoints. Re-use the same array if possible to avoid GC + newXYValuesArr = pointsNum$ > newXYValuesArr.length ? new Array(pointsNum$) : newXYValuesArr; + + for (let i = 0; i < pointsNum$; i++) { + const prevXValue = xyValuesArr ? xyValuesArr[i].x : Math.round(Math.random() * X_MAX); + const x = prevXValue + (Math.random() - 0.5); + const prevYValue = xyValuesArr ? xyValuesArr[i].y : Math.round(Math.random() * Y_MAX); + const y = prevYValue + (Math.random() - 0.5); + newXYValuesArr[i] = { x, y }; + } + + return newXYValuesArr; + }; + + const generateData = () => { + DATA = generateNextPoints(pointsNum, undefined); + }; + + const appendData = () => { + const newXYValuesArr = DATA; + scatterSeries = chart + .addPointSeries({ pointShape: PointShape.Circle }) + .setName('Scatter data') + .setCursorEnabled(false) + .setPointSize(5) + .setPointFillStyle( + new SolidFill({ + color: ColorRGBA(0, 255, 0), + }) + ) + .add(newXYValuesArr); + }; + + const updateChart = () => { + DATA = generateNextPoints(pointsNum, DATA); + const xyValuesArr = DATA; + scatterSeries.clear(); + scatterSeries.add(xyValuesArr); + return scatterSeries.getPointAmount(); + }; + + const deleteChart = () => { + chart?.dispose(); + const chartDiv = document.getElementById('chart-root'); + chartDiv.innerHTML = ''; + }; + + return { + createChart, + generateData, + appendData, + updateChart, + deleteChart, + }; +} + +/** + * SCATTER_LINE_PERFORMANCE_TEST + * @param seriesNum + * @param pointsNum + * @returns {{appendData: ()=>void, deleteChart: ()=>void, updateChart: ()=>void, createChart: () => Promise, generateData: () => void}} + */ +function eScatterLinePerformanceTest(seriesNum, pointsNum) { + const { lightningChart, SolidLine, SolidFill, emptyFill, ColorHEX, disableThemeEffects, Themes } = lcjs; + + let DATA; + let chart; + let series; + + const X_MAX = 100; + const Y_MAX = 50; + + const createChart = async () => { + const EXTRA = 10; + + chart = lightningChart() + .ChartXY({ + container: document.getElementById('chart-root'), + disableAnimations: true, + theme: disableThemeEffects(Themes.darkGold) + }) + .setPadding({top: 10, bottom: 10, left: 10, right: 10}) + .setTitleFillStyle(emptyFill); + const xAxis = chart.getDefaultAxisX().setAnimationScroll(undefined).setChartInteractions(false); + xAxis.setInterval({start: 0 - EXTRA, end: X_MAX + EXTRA}); + + const yAxis = chart.getDefaultAxisY().setAnimationScroll(undefined); + yAxis.setInterval({start: 0 - EXTRA, end: Y_MAX +EXTRA}); + }; + + let newXYValuesArr = []; + const generateNextPoints = (pointsNum$, xyValuesArr) => { + // Every frame we call generateNextPoints. Re-use the array if it's the same size to avoid GC + newXYValuesArr = pointsNum$ > newXYValuesArr.length ? new Array(pointsNum$) : newXYValuesArr; + + for (let i = 0; i < pointsNum$; i++) { + const prevXValue = xyValuesArr ? xyValuesArr[i].x : Math.round(Math.random() * X_MAX); + const x = prevXValue + (Math.random() - 0.5); + const prevYValue = xyValuesArr ? xyValuesArr[i].y : Math.round(Math.random() * Y_MAX); + const y = prevYValue + (Math.random() - 0.5); + newXYValuesArr[i] = { x, y }; + } + + return newXYValuesArr; + }; + + const generateData = () => { + DATA = generateNextPoints(pointsNum, undefined); + }; + + const appendData = () => { + const newXYValuesArr = DATA; + series = chart + .addLineSeries() + .setCursorEnabled(false) + .setStrokeStyle(new SolidLine({ thickness: 2, fillStyle: new SolidFill({ color: ColorHEX('#00FF00') }) })); + series.add(newXYValuesArr); + }; + + const updateChart = (_frame) => { + DATA = generateNextPoints(pointsNum, DATA); + const xyValuesArr = DATA; + series.clear(); + series.add(xyValuesArr); + return series.getPointAmount(); + }; + + const deleteChart = () => { + chart?.dispose(); + const chartDiv = document.getElementById('chart-root'); + chartDiv.innerHTML = ''; + }; + + return { + createChart, + generateData, + appendData, + updateChart, + deleteChart, + }; +} + +/** + * POINT_LINE_PERFORMANCE_TEST + * @param seriesNum + * @param pointsNum + * @returns {{appendData: ()=>void, deleteChart: ()=>void, updateChart: ()=>void, createChart: () => Promise, generateData: () => void}} + */ +function ePointLinePerformanceTest(seriesNum, pointsNum) { + const { lightningChart, SolidLine, SolidFill, emptyFill, ColorHEX, ColorRGBA, disableThemeEffects, Themes, PointShape } = lcjs; + + let DATA; + let chart; + let series; + + const Y_MAX = 50; + + const createChart = async () => { + const EXTRA = 10; + + chart = lightningChart() + .ChartXY({ + container: document.getElementById('chart-root'), + disableAnimations: true, + theme: disableThemeEffects(Themes.darkGold) + }) + .setPadding({top: 10, bottom: 10, left: 10, right: 10}) + .setTitleFillStyle(emptyFill); + const xAxis = chart.getDefaultAxisX().setAnimationScroll(undefined).setChartInteractions(false); + + const yAxis = chart.getDefaultAxisY().setAnimationScroll(undefined); + yAxis.setInterval({start: 0 - EXTRA, end: Y_MAX + EXTRA}); + }; + + let newXYValuesArr = []; + const generateNextPoints = (pointsNum$, xyValuesArr) => { + // Every frame we call generateNextPoints. Re-use the array if it's the same size to avoid GC + newXYValuesArr = pointsNum$ > newXYValuesArr.length ? new Array(pointsNum$) : newXYValuesArr; + + for (let i = 0; i < pointsNum$; i++) { + const prevYValue = xyValuesArr ? xyValuesArr[i].y : Math.round(Math.random() * Y_MAX); + const y = prevYValue + (Math.random() - 0.5); + newXYValuesArr[i] = { x: i, y }; + } + + return newXYValuesArr; + }; + + const generateData = () => { + DATA = generateNextPoints(pointsNum, undefined); + }; + + const appendData = () => { + const newXYValuesArr = DATA; + series = chart + .addPointLineSeries( { pointShape: PointShape.Circle }) + .setPointSize(10) + .setCursorEnabled(false) + .setStrokeStyle(new SolidLine({ thickness: 2, fillStyle: new SolidFill({ color: ColorHEX('#00FF00') }) })) + .setPointFillStyle( + new SolidFill({ + color: ColorRGBA(255, 255, 255), + }) + ) + series.add(newXYValuesArr); + }; + + const updateChart = (_frame) => { + DATA = generateNextPoints(pointsNum, DATA); + const xyValuesArr = DATA; + series.clear(); + series.add(xyValuesArr); + return series.getPointAmount(); + }; + + const deleteChart = () => { + chart?.dispose(); + const chartDiv = document.getElementById('chart-root'); + chartDiv.innerHTML = ''; + }; + + return { + createChart, + generateData, + appendData, + updateChart, + deleteChart, + }; +} + +/** + * COLUMN_PERFORMANCE_TEST + * @param seriesNum + * @param pointsNum + * @returns {{appendData: ()=>void, deleteChart: ()=>void, updateChart: ()=>void, createChart: () => Promise, generateData: () => void}} + */ +function eColumnPerformanceTest(seriesNum, pointsNum) { + const { lightningChart, emptyFill, disableThemeEffects, Themes } = lcjs; + + let DATA; + let chart; + let yAxis; + let delta; + + const createChart = async () => { + chart = lightningChart() + .ChartXY({ + container: document.getElementById('chart-root'), + disableAnimations: true, + theme: disableThemeEffects(Themes.darkGold) + }) + .setPadding({ top: 10, bottom: 10, left: 10, right: 10 }) + .setTitleFillStyle(emptyFill); + const xAxis = chart.getDefaultAxisX().setAnimationScroll(undefined); + yAxis = chart.getDefaultAxisY().setAnimationScroll(undefined); + }; + + const generateData = () => { + const rectArr = []; + + let x = 0; + const figureThickness = 10; + const figureGap = figureThickness * 0.5; + + let prevYValue = 0; + for (let i = 0; i < pointsNum; i++) { + const curYValue = Math.random() * 10 - 5; + prevYValue += curYValue; + + const rectDimensions = { + x: x - figureThickness / 2, + y: 0, + width: figureThickness, + height: prevYValue, + }; + + rectArr.push(rectDimensions); + x += figureThickness + figureGap; + } + + DATA = rectArr; + }; + + const appendData = () => { + const rectangles = chart.addRectangleSeries() + .setCursorEnabled(false); + DATA.forEach((rectDimension) => rectangles.add(rectDimension)); + yAxis.fit(); + const min = yAxis.getInterval().start; + const max = yAxis.getInterval().end; + const maxVal = Math.max(Math.abs(min), Math.abs(max)); + delta = maxVal / 300; + }; + + const updateChart = (_frame) => { + const yAxis = chart.getDefaultAxisY(); + yAxis.setInterval({ start: yAxis.getInterval().start - delta, end: yAxis.getInterval().end + delta }); + return seriesNum * DATA.length; + }; + + const deleteChart = () => { + chart?.dispose(); + const chartDiv = document.getElementById('chart-root'); + chartDiv.innerHTML = ''; + }; + + return { + createChart, + generateData, + appendData, + updateChart, + deleteChart, + }; +} + +/** + * CANDLESTICK_PERFORMANCE_TEST + * @param seriesNum + * @param pointsNum + * @returns {{appendData: ()=>void, deleteChart: ()=>void, updateChart: ()=>void, createChart: () => Promise, generateData: () => void}} + */ +function eCandlestickPerformanceTest(seriesNum, pointsNum) { + const { lightningChart, emptyFill, OHLCFigures, disableThemeEffects, Themes } = lcjs; + + let chart; + let yAxis; + let delta; + let DATA; + + const createChart = async () => { + chart = lightningChart() + .ChartXY({ + container: document.getElementById('chart-root'), + disableAnimations: true, + theme: disableThemeEffects(Themes.darkGold) + }) + .setPadding({ top: 10, bottom: 10, left: 10, right: 10 }) + .setTitleFillStyle(emptyFill); + const xAxis = chart.getDefaultAxisX().setAnimationScroll(undefined); + yAxis = chart.getDefaultAxisY().setAnimationScroll(undefined); + }; + + const generateData = () => { + const ohlcDataArr = []; + + for (let x = 0; x < pointsNum; x++) { + const open = Math.random(); + const close = Math.random(); + const val1 = Math.random(); + const val2 = Math.random(); + const high = Math.max(val1, val2, open, close); + const low = Math.min(val1, val2, open, close); + ohlcDataArr.push([x, open, high, low, close]); + } + DATA = ohlcDataArr; + }; + + const appendData = () => { + const ohlcDataArr = DATA; + const series = chart + .addOHLCSeries({ positiveFigure: OHLCFigures.Candlestick }) + .setCursorEnabled(false) + .setFigureAutoFitting(false); + series.add(ohlcDataArr); + yAxis.fit(); + const min = yAxis.getInterval().start; + const max = yAxis.getInterval().end; + const maxVal = Math.abs(max); + delta = maxVal / 300; + }; + + const updateChart = (_frame) => { + yAxis.setInterval({ start: 0, end: yAxis.getInterval().end + delta }); + return seriesNum * DATA.length; + }; + + const deleteChart = () => { + chart?.dispose(); + const chartDiv = document.getElementById('chart-root'); + chartDiv.innerHTML = ''; + }; + + return { + createChart, + generateData, + appendData, + updateChart, + deleteChart, + }; +} + +/** + * FIFO_ECG_PERFORMANCE_TEST + * @param seriesNum + * @param pointsNum + * @returns {{appendData: ()=>void, deleteChart: ()=>void, updateChart: ()=>void, createChart: () => Promise, generateData: () => void}} */ +function eFifoEcgPerformanceTest(seriesNum, pointsNum, incrementPoints) { + const { lightningChart, SolidLine, SolidFill, ColorHSV, emptyFill, AxisScrollStrategies, disableThemeEffects, Themes } = lcjs; + + let chart; + const appendCount = incrementPoints; + const seriesArr = []; + let index = 0; + let DATA; + + const createChart = async () => { + chart = lightningChart() + .ChartXY({ + container: document.getElementById('chart-root'), + disableAnimations: true, + theme: disableThemeEffects(Themes.darkGold) + }) + .setTitleFillStyle(emptyFill) + .setPadding({ top: 10, bottom: 10, left: 10, right: 10 }); + const xAxis = chart.getDefaultAxisX().setAnimationScroll(undefined); + const yAxis = chart.getDefaultAxisY().setAnimationScroll(undefined); + xAxis.setScrollStrategy(AxisScrollStrategies.progressive).setInterval({ start: 0, end: pointsNum, stopAxisAfter: false }); + }; + + const generateDataInner = (seriesNum$, pointsNum$, startIndex = 0) => { + // Create new arrays each time + const xArr = new Float64Array(pointsNum$); + const yArrArr = []; + + // Create yArrays for each series + for (let i = 0; i < seriesNum$; i++) { + yArrArr[i] = new Float64Array(pointsNum$); + } + + // Generate data into new arrays + for (let i = 0; i < seriesNum$; i++) { + const yOffset = i * 2; + for (let j = 0; j < pointsNum$; j++) { + // we use the same X vector for every series + if (i === 0) xArr[j] = startIndex + j; + const val = Math.random() + yOffset; + yArrArr[i][j] = val; + } + } + + return { xArr, yArrArr }; + }; + + const generateData = () => { + DATA = generateDataInner(seriesNum, pointsNum); + }; + + const appendData = () => { + const { xArr, yArrArr } = DATA; + for (let i = 0; i < seriesNum; i++) { + const series = chart + .addLineSeries({ dataPattern: { pattern: 'ProgressiveX', regularProgressiveStep: true } }) + .setCursorEnabled(false) + .setStrokeStyle(new SolidLine({ fillStyle: new SolidFill({ color: ColorHSV((360 * i) / seriesNum) }) })) + .addArraysXY(xArr, yArrArr[i]); + seriesArr.push(series); + } + index += pointsNum; + }; + + const updateChart = (_frame) => { + const { xArr, yArrArr } = generateDataInner(seriesNum, appendCount, index); + seriesArr.forEach((s, i) => { + s.addArraysXY(xArr, yArrArr[i]); + }); + index += appendCount; + return seriesNum * (pointsNum + index - appendCount); + }; + + const deleteChart = () => { + chart?.dispose(); + const chartDiv = document.getElementById('chart-root'); + chartDiv.innerHTML = ''; + }; + + return { + createChart, + generateData, + appendData, + updateChart, + deleteChart, + }; +} + +/** + * MOUNTAIN_PERFORMANCE_TEST + * @param seriesNum + * @param pointsNum + * @returns {{appendData: ()=>void, deleteChart: ()=>void, updateChart: ()=>void, createChart: () => Promise, generateData: () => void}} + */ +function eMountainPerformanceTest(seriesNum, pointsNum) { + const { lightningChart, emptyFill, AreaSeriesTypes, disableThemeEffects, Themes } = lcjs; + + let chart; + let yAxis; + let delta; + let DATA; + + const createChart = async () => { + chart = lightningChart() + .ChartXY({ + container: document.getElementById('chart-root'), + disableAnimations: true, + theme: disableThemeEffects(Themes.darkGold) + }) + .setPadding({ top: 10, bottom: 10, left: 10, right: 10 }) + .setTitleFillStyle(emptyFill); + chart.getDefaultAxisX().setAnimationScroll(undefined); + yAxis = chart.getDefaultAxisY().setAnimationScroll(undefined); + }; + + const generateData = () => { + const areaRangeSeriesPointsArr = []; + + let prevYValue = 0; + for (let i = 0; i < pointsNum; i++) { + const curYValue = Math.random() * 10 - 5; + prevYValue += curYValue; + areaRangeSeriesPointsArr.push({ x: i, y: prevYValue }); + } + DATA = areaRangeSeriesPointsArr; + }; + + const appendData = () => { + const areaRangeSeriesPointsArr = DATA; + const series = chart.addAreaSeries({ type: AreaSeriesTypes.Bipolar }).setCursorEnabled(false); + series.add(areaRangeSeriesPointsArr); + yAxis = chart.getDefaultAxisY(); + yAxis.fit(); + const min = Math.abs(yAxis.getInterval().start); + const max = Math.abs(yAxis.getInterval().end); + const maxVal = Math.max(min, max); + delta = maxVal / 300; + }; + + const updateChart = (_frame) => { + yAxis.setInterval({ start: yAxis.getInterval().start - delta, end: yAxis.getInterval().end + delta }); + return seriesNum * DATA.length; + }; + + const deleteChart = () => { + chart?.dispose(); + const chartDiv = document.getElementById('chart-root'); + chartDiv.innerHTML = ''; + }; + + return { + createChart, + generateData, + appendData, + updateChart, + deleteChart, + }; +} + +/** + * SERIES_COMPRESSION_PERFORMANCE_TEST + * @param seriesNum + * @param pointsNum + * @returns {{appendData: ()=>void, deleteChart: ()=>void, updateChart: ()=>void, createChart: () => Promise, generateData: () => void}} + */ +function eSeriesCompressionPerformanceTest(seriesNum, pointsNum, incrementPoints) { + const {lightningChart, emptyFill, SolidLine, SolidFill, ColorHEX, disableThemeEffects, Themes} = lcjs; + + let chart; + let yAxis; + let series; + const appendCount = incrementPoints; + let points = 0; + let prevYValue = 0; + let DATA; + + const createChart = async () => { + // Initialise random seed for fair comparison + fastRandomSeed = 1; + + chart = lightningChart() + .ChartXY({ + container: document.getElementById('chart-root'), + disableAnimations: true, + theme: disableThemeEffects(Themes.darkGold) + }) + .setPadding({top: 10, bottom: 10, left: 10, right: 10}) + .setTitleFillStyle(emptyFill); + chart.getDefaultAxisX().setAnimationScroll(undefined); + yAxis = chart.getDefaultAxisY().setAnimationScroll(undefined); + }; + + const generateDataInner = (pointsNum$, startIndex) => { + // Create new arrays each time + const xValuesArr = new Float64Array(pointsNum$); + const yValuesArr = new Float64Array(pointsNum$); + + // Generate data into new arrays + for (let i = 0; i < pointsNum$; i++) { + const curYValue = fastRandom() * 10 - 5; + xValuesArr[i] = startIndex + i; + prevYValue += curYValue; + yValuesArr[i] = prevYValue; + } + points += pointsNum$; + return {xValuesArr, yValuesArr}; + }; + + const generateData = () => { + DATA = generateDataInner(pointsNum, 0); + }; + + const appendData = () => { + const {xValuesArr, yValuesArr} = DATA; + series = chart + .addLineSeries({dataPattern: {pattern: 'ProgressiveX', regularProgressiveStep: true}}) + .setCursorEnabled(false) + .setStrokeStyle(new SolidLine({thickness: 2, fillStyle: new SolidFill({color: ColorHEX('#00FF00')})})); + series.addArraysXY(xValuesArr, yValuesArr); + yAxis.setInterval({start: 0, end: 9}); + }; + + const updateChart = () => { + const {xValuesArr, yValuesArr} = generateDataInner(appendCount, points); + series.addArraysXY(xValuesArr, yValuesArr); + yAxis.fit(); + return series.getPointAmount(); + }; + + const deleteChart = () => { + chart?.dispose(); + const chartDiv = document.getElementById('chart-root'); + chartDiv.innerHTML = ''; + }; + + return { + createChart, + generateData, + appendData, + updateChart, + deleteChart, + }; +} + +/** + * MULTI_CHART_PERFORMANCE_TEST + * @param seriesNum + * @param pointsNum + * @param incrementPoints + * @param chartsNum + * @returns {{appendData: ()=>void, deleteChart: ()=>void, updateChart: ()=>void, createChart: () => Promise, generateData: () => void}} + */ +function eMultiChartPerformanceTest(seriesNum, pointsNum, incrementPoints, chartsNum) { + const {lightningChart, emptyFill, SolidLine, SolidFill, ColorHEX, disableThemeEffects, Themes} = lcjs; + + let charts = []; + let series = []; + const appendCount = incrementPoints; + let points = 0; + let prevYValue = 0; + let DATA; + const chartRootDiv = document.getElementById('chart-root'); + + // Calculate grid dimensions based on number of charts + const getGridDimensions = (numCharts) => { + if (numCharts === 1) return { cols: 1, rows: 1 }; + if (numCharts === 2) return { cols: 2, rows: 1 }; + if (numCharts === 4) return { cols: 2, rows: 2 }; + if (numCharts === 8) return { cols: 4, rows: 2 }; + if (numCharts === 16) return { cols: 4, rows: 4 }; + if (numCharts === 32) return { cols: 8, rows: 4 }; + if (numCharts === 64) return { cols: 8, rows: 8 }; + if (numCharts === 128) return { cols: 16, rows: 8 }; + // Fallback for other numbers + const cols = Math.ceil(Math.sqrt(numCharts)); + const rows = Math.ceil(numCharts / cols); + return { cols, rows }; + }; + + const createChart = async () => { + // Initialise random seed for fair comparison + fastRandomSeed = 1; + + // Clear the chart root + chartRootDiv.innerHTML = ''; + + // Get grid dimensions + const { cols, rows } = getGridDimensions(chartsNum); + const chartWidth = 100 / cols; + const chartHeight = 100 / rows; + + // Create container divs for each chart in grid layout + for (let c = 0; c < chartsNum; c++) { + const chartDiv = document.createElement('div'); + chartDiv.id = `chart-${c}`; + chartDiv.style.width = `${chartWidth}%`; + chartDiv.style.height = `${chartHeight}%`; + chartDiv.style.position = 'absolute'; + chartDiv.style.left = `${(c % cols) * chartWidth}%`; + chartDiv.style.top = `${Math.floor(c / cols) * chartHeight}%`; + chartRootDiv.appendChild(chartDiv); + } + + // Set chart root to use absolute positioning + chartRootDiv.style.position = 'relative'; + + // Create each chart + try { + for (let c = 0; c < chartsNum; c++) { + const chart = lightningChart() + .ChartXY({ + container: document.getElementById(`chart-${c}`), + disableAnimations: true, + theme: disableThemeEffects(Themes.darkGold) + }) + .setPadding({top: 10, bottom: 10, left: 10, right: 10}) + .setTitleFillStyle(emptyFill); + chart.getDefaultAxisX().setAnimationScroll(undefined); + chart.getDefaultAxisY().setAnimationScroll(undefined); + charts.push(chart); + } + } catch (error) { + console.error('Failed to create charts:', error); + charts.forEach(chart => chart?.dispose()); + charts = []; + return false; + } + }; + + const generateDataInner = (pointsNum$, startIndex) => { + // Create new arrays each time + const xValuesArr = new Float64Array(pointsNum$); + const yValuesArr = new Float64Array(pointsNum$); + + // Generate data into new arrays + for (let i = 0; i < pointsNum$; i++) { + const curYValue = fastRandom() * 10 - 5; + xValuesArr[i] = startIndex + i; + prevYValue += curYValue; + yValuesArr[i] = prevYValue; + } + points += pointsNum$; + return {xValuesArr, yValuesArr}; + }; + + const generateData = () => { + DATA = generateDataInner(pointsNum, 0); + }; + + const appendData = () => { + const {xValuesArr, yValuesArr} = DATA; + + // Add data series to each chart + for (let c = 0; c < chartsNum; c++) { + const s = charts[c] + .addLineSeries({dataPattern: {pattern: 'ProgressiveX', regularProgressiveStep: true}}) + .setCursorEnabled(false) + .setStrokeStyle(new SolidLine({thickness: 2, fillStyle: new SolidFill({color: ColorHEX('#00FF00')})})); + s.addArraysXY(xValuesArr, yValuesArr); + series.push(s); + } + }; + + const updateChart = () => { + const {xValuesArr, yValuesArr} = generateDataInner(appendCount, points); + + // Update all charts with the same data + for (let c = 0; c < chartsNum; c++) { + series[c].addArraysXY(xValuesArr, yValuesArr); + } + + return series[0].getPointAmount(); + }; + + const deleteChart = () => { + charts.forEach(chart => chart?.dispose()); + charts = []; + series = []; + }; + + return { + createChart, + generateData, + appendData, + updateChart, + deleteChart, + }; +} + +/** + * 3D_POINTCLOUD_PERFORMANCE_TEST + * @param seriesNum + * @param pointsNum + * @returns {{appendData: ()=>void, deleteChart: ()=>void, updateChart: ()=>void, createChart: () => Promise, generateData: () => void}} + */ +function e3dPointCloudPerformanceTest(seriesNum, pointsNum) { + const { lightningChart, PointSeriesTypes3D, disableThemeEffects, Themes } = lcjs; + + let chart3D; + let pointSeries3D; + let DATA; + const pointCloudSize = pointsNum; // Total number of points in the 3D point cloud + + const createChart = async () => { + chart3D = lightningChart() + .Chart3D({ + container: document.getElementById('chart-root'), + disableAnimations: true, + theme: disableThemeEffects(Themes.darkGold), + legend: { visible: false }, + }); + + // Set Axis titles + chart3D.getDefaultAxisX().setTitle('X Axis'); + chart3D.getDefaultAxisY().setTitle('Y Axis'); + chart3D.getDefaultAxisZ().setTitle('Z Axis'); + + // Set static Axis intervals for consistent view + chart3D.forEachAxis((axis) => axis.setInterval({ start: -100, end: 100 })); + }; + + let newXValuesArr = new Float64Array(0); + let newYValuesArr = new Float64Array(0); + let newZValuesArr = new Float64Array(0); + + const generateNextPoints = (pointsNum$, xValuesArr, yValuesArr, zValuesArr) => { + // Every frame we call generateNextPoints. Re-use the same array if possible to avoid GC + newXValuesArr = pointsNum$ > newXValuesArr.length ? new Float64Array(pointsNum$) : newXValuesArr; + newYValuesArr = pointsNum$ > newYValuesArr.length ? new Float64Array(pointsNum$) : newYValuesArr; + newZValuesArr = pointsNum$ > newZValuesArr.length ? new Float64Array(pointsNum$) : newZValuesArr; + + for (let i = 0; i < pointsNum$; i++) { + const prevXValue = xValuesArr ? xValuesArr[i] : Math.random() * 200 - 100; + const x = prevXValue + (Math.random() - 0.5) * 2; + newXValuesArr[i] = x; + + const prevYValue = yValuesArr ? yValuesArr[i] : Math.random() * 200 - 100; + const y = prevYValue + (Math.random() - 0.5) * 2; + newYValuesArr[i] = y; + + const prevZValue = zValuesArr ? zValuesArr[i] : Math.random() * 200 - 100; + const z = prevZValue + (Math.random() - 0.5) * 2; + newZValuesArr[i] = z; + } + + // Convert to array of objects for LCJS + const points = []; + for (let i = 0; i < pointsNum$; i++) { + points.push({ + x: newXValuesArr[i], + y: newYValuesArr[i], + z: newZValuesArr[i] + }); + } + + return { + xValues: newXValuesArr, + yValues: newYValuesArr, + zValues: newZValuesArr, + points: points + }; + }; + + const generateData = () => { + DATA = generateNextPoints(pointCloudSize, undefined, undefined, undefined); + }; + + const appendData = () => { + // Create Point Cloud Series (variant optimized for rendering minimal detail geometry) + pointSeries3D = chart3D + .addPointSeries({ type: PointSeriesTypes3D.Pixelated }) + .setPointStyle((style) => style.setSize(1)); + + // Add initial data + pointSeries3D.add(DATA.points); + }; + + const updateChart = (_frame) => { + // Generate new Brownian motion 3D points for dynamic updating + DATA = generateNextPoints(pointCloudSize, DATA.xValues, DATA.yValues, DATA.zValues); + + // Clear and update the point series + pointSeries3D.clear().add(DATA.points); + + return pointCloudSize; + }; + + const deleteChart = () => { + chart3D?.dispose(); + const chartDiv = document.getElementById('chart-root'); + chartDiv.innerHTML = ''; + }; + + return { + createChart, + generateData, + appendData, + updateChart, + deleteChart, + }; +} + +/** + * 3D_SURFACE_PERFORMANCE_TEST + * @param seriesNum + * @param pointsNum + * @returns {{appendData: ()=>void, deleteChart: ()=>void, updateChart: ()=>void, createChart: () => Promise, generateData: () => void}} + */ +function e3dSurfacePerformanceTest(seriesNum, pointsNum) { + const { lightningChart, disableThemeEffects, Themes, PalettedFill, LUT, ColorCSS } = lcjs; + + let chart3D; + let surfaceSeries; + let surfaceData; + let DATA; + const surfaceSize = pointsNum; // pointsNum represents the side length of the surface (e.g., 100 = 100x100) + + const createChart = async () => { + chart3D = lightningChart() + .Chart3D({ + container: document.getElementById('chart-root'), + disableAnimations: true, + theme: disableThemeEffects(Themes.darkGold), + legend: { visible: false }, + }); + + // Set Axis titles + chart3D.getDefaultAxisX().setTitle('X Axis'); + chart3D.getDefaultAxisY().setTitle('Y Axis'); + chart3D.getDefaultAxisZ().setTitle('Z Axis'); + + // Set static Axis intervals for consistent view + chart3D.getDefaultAxisX().setInterval({ start: 0, end: surfaceSize }); + chart3D.getDefaultAxisY().setInterval({ start: -0.5, end: 0.5 }); + chart3D.getDefaultAxisZ().setInterval({ start: 0, end: surfaceSize }); + }; + + const zeroArray2D = (dimensions) => { + if (!dimensions) { + return undefined; + } + const array = []; + + for (let i = 0; i < dimensions[0]; ++i) { + array.push(dimensions.length === 1 ? 0 : zeroArray2D(dimensions.slice(1))); + } + + return array; + }; + + const generateData = () => { + // Create a 2D array for surface heights + surfaceData = zeroArray2D([surfaceSize, surfaceSize]); + + // Fill with initial random data + for (let z = 0; z < surfaceSize; z++) { + for (let x = 0; x < surfaceSize; x++) { + surfaceData[z][x] = Math.random() * 0.6 - 0.3; // Random values between -0.3 and 0.3 + } + } + + DATA = { surfaceData }; + }; + + const appendData = () => { + const { surfaceData } = DATA; + + // Create surface series + surfaceSeries = chart3D + .addSurfaceGridSeries({ + columns: surfaceSize, + rows: surfaceSize, + start: { x: 0, z: 0 }, + end: { x: surfaceSize, z: surfaceSize }, + }); + surfaceSeries.setFillStyle(new PalettedFill({ + lookUpProperty: 'y', + lut: new LUT({ + interpolate: true, + percentageValues: true, + steps: [ + { value: -1, color: ColorCSS('red') }, + { value: 1, color: ColorCSS('green') }, + ] + }) + })); + let tempLineSeries = chart3D.addLineSeries(); + const defaultStrokeStyle = tempLineSeries.getStrokeStyle(); + + surfaceSeries.setWireframeStyle(defaultStrokeStyle.setThickness(1)); + tempLineSeries.dispose() + tempLineSeries = undefined + // Set initial surface data + surfaceSeries.invalidateHeightMap(surfaceData); + }; + + let frame = 0; + const updateChart = (_frameNumber) => { + // Generate new surface data with animated wave pattern + const f = frame / 10; + + for (let z = 0; z < surfaceSize; z++) { + const zVal = z - surfaceSize / 2; + for (let x = 0; x < surfaceSize; x++) { + const xVal = x - surfaceSize / 2; + // Create animated wave pattern similar to the SciChart example + const y = (Math.cos(xVal * 0.2 + f) + Math.cos(zVal * 0.2 + f)) / 5; + surfaceData[z][x] = y; + } + } + + // Update the surface with new height data + surfaceSeries.invalidateHeightMap(surfaceData); + frame++; + + return surfaceSize * surfaceSize; + }; + + const deleteChart = () => { + chart3D?.dispose(); + const chartDiv = document.getElementById('chart-root'); + chartDiv.innerHTML = ''; + }; + + return { + createChart, + generateData, + appendData, + updateChart, + deleteChart, + }; +} + +/** + * HEATMAP_PERFORMANCE_TEST + * @param seriesNum + * @param pointsNum + * @returns {{appendData: ()=>void, deleteChart: ()=>void, updateChart: ()=>void, createChart: () => Promise, generateData: () => void}} + */ +function eHeatmapPerformanceTest(seriesNum, pointsNum) { + const { lightningChart, emptyFill, emptyLine, PalettedFill, LUT, ColorRGBA, disableThemeEffects, Themes } = lcjs; + + let chart; + let heatmap; + let zValues; + const heatmapSize = pointsNum; + + const palette = new LUT({ + units: "intensity", + steps: [ + { value: 0, color: ColorRGBA(255, 0, 0) }, + { value: 1, color: ColorRGBA(0, 255, 0) }, + ], + interpolate: true, + }); + + const zeroArray2D = (dimensions /*: number[]*/)/*: number[][]*/ => { + if (!dimensions) { + return undefined; + } + const array = []; + + for (let i = 0; i < dimensions[0]; ++i) { + array.push(dimensions.length === 1 ? 0 : zeroArray2D(dimensions.slice(1))); + } + + return array; + }; + + const createChart = async () => { + chart = lightningChart() + .ChartXY({ + container: document.getElementById('chart-root'), + disableAnimations: true, + theme: disableThemeEffects(Themes.darkGold) + }) + .setTitleFillStyle(emptyFill) + .setPadding({ top: 10, bottom: 10, left: 10, right: 10 }); + + chart.getDefaultAxisX().setAnimationScroll(undefined); + chart.getDefaultAxisY().setAnimationScroll(undefined); + }; + + const generateData = () => { + // Create zValues 2D array with random data + zValues = zeroArray2D([heatmapSize, heatmapSize]); + for (let y = 0; y < heatmapSize; y++) { + for (let x = 0; x < heatmapSize; x++) { + zValues[y][x] = Math.random(); + } + } + }; + + const appendData = () => { + heatmap = chart + .addHeatmapGridSeries({ + dataOrder: 'columns', + columns: heatmapSize, + rows: heatmapSize, + }) + .setFillStyle(new PalettedFill({ lut: palette })) + .setWireframeStyle(emptyLine) + .setMouseInteractions(false) + .setCursorEnabled(false); + + heatmap.invalidateIntensityValues(zValues); + }; + + const updateChart = (_frame) => { + // Generate new random values and fill the zValues array + for (let y = 0; y < heatmapSize; y++) { + for (let x = 0; x < heatmapSize; x++) { + zValues[y][x] = Math.random(); + } + } + heatmap.invalidateIntensityValues(zValues); + + return heatmapSize * heatmapSize; + }; + + const deleteChart = () => { + chart?.dispose(); + const chartDiv = document.getElementById('chart-root'); + chartDiv.innerHTML = ''; + }; + + return { + createChart, + generateData, + appendData, + updateChart, + deleteChart, + }; +}