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,
+ };
+}