diff --git a/src/app/editor/components/Preview.vue b/src/app/editor/components/Preview.vue index edd52c5..586f586 100644 --- a/src/app/editor/components/Preview.vue +++ b/src/app/editor/components/Preview.vue @@ -49,9 +49,8 @@ export default { chartOptions() { const chartBridge = (this as any)?.$chartBridge; - return chartBridge - ?.reactiveGet('buildChartOptions', this.reactToParameterUpdates, this.tableCSV) || - {}; + const options = chartBridge?.reactiveGet('buildChartOptions', this.reactToParameterUpdates, this.tableCSV) || {}; + return options; } }, diff --git a/src/app/editor/core/ChartBridge.ts b/src/app/editor/core/ChartBridge.ts index 6c236b0..ba330b7 100644 --- a/src/app/editor/core/ChartBridge.ts +++ b/src/app/editor/core/ChartBridge.ts @@ -6,7 +6,7 @@ import { getSeriesId } from './utils/chartUtils'; import { defaultChartOptions } from './defaultChartOptions'; import { getChartOptionsFromParameters } from './optionsMapper/chartOptionsMapper'; import { getSeriesOptionsFromParameters } from './optionsMapper/seriesOptionsMapper'; -import { GoogleSheetStatus } from '../store/modules/data'; +import { GoogleSheetStatus, dataStore } from '../store/modules/data'; import { Store } from 'vuex'; @@ -145,7 +145,6 @@ export class ChartBridge { const dataSource = this.getStoreParam('dataStore', 'selectedDataSource'); const spreadsheetSetupComplete = this.getStoreParam('dataStore', 'spreadsheetSetupComplete'); - const dataSourceOptions = dataSource === 'googlesheets' && spreadsheetSetupComplete ? { csv: void 0, firstRowAsNames: true, @@ -185,7 +184,6 @@ export class ChartBridge { delete s.index; return id; }); - // Extend parsed results with series options const seriesOptions = this.buildSeriesOptions(seriesIds); if (seriesOptions) { @@ -197,13 +195,16 @@ export class ChartBridge { } }) }; - // Need to update the chart data first to know which (new) series to build options for. // Only once the CSV has been parsed can we build the series options. This is why // series options are built separately. const newOptions = Object.assign(dataOptions, chartSettings); + const plotParametersMap = deepMerge(defaultOptions, newOptions); + + + console.log(plotParametersMap); - return deepMerge(defaultOptions, newOptions); + return plotParametersMap; } diff --git a/src/app/editor/core/optionsMapper/chartMappings.ts b/src/app/editor/core/optionsMapper/chartMappings.ts index d67f046..ad85a16 100644 --- a/src/app/editor/core/optionsMapper/chartMappings.ts +++ b/src/app/editor/core/optionsMapper/chartMappings.ts @@ -1,11 +1,94 @@ import { GenericObject } from '../utils/objects'; +function parseCsvToMap(csvString: string): Map> { + const map = new Map>(); + const rows = csvString.split('\n').map(row => row.trim()).filter(row => row !== ''); + if (rows.length < 2) return map; // return empty map if there's no data or only headers + console.log(rows); + // Process headers + const headers = rows[0].split(';').map(header => + header.toLowerCase().replace(/\s+/g, '') + ); + + // Initialize the map with empty arrays for each header + headers.forEach(header => { + map.set(header, []); + }); + + // Populate the map + for (let i = 1; i < rows.length; i++) { + const values = rows[i].split(';'); + values.forEach((value, index) => { + const key = headers[index]; + map.get(key)?.push(parseFloat(value)); + }); + } + + // Special handling for error bars: identify pairs and group them + const errorBarDataMap = new Map>(); + + // Identify low and high pairs dynamically + headers.filter(header => header.includes('lowvalue')).forEach(lowKey => { + const suffix = lowKey.match(/\d+$/); // matches the trailing number in 'lowvalue1', 'lowvalue2', etc. + if (suffix) { + const highKey = `highvalue${suffix[0]}`; + if (map.has(highKey)) { + const errorBarData = []; + for (let i = 0; i < map.get(lowKey)!.length; i++) { + errorBarData.push([map.get(lowKey)![i], map.get(highKey)![i]]); + } + const errorBarKey = `errorbardata${suffix[0]}`; + errorBarDataMap.set(errorBarKey, errorBarData); + } + }}); + + // Add error bar data to main map + errorBarDataMap.forEach((data, key) => { + map.set(key, data); + }); + + return map; +} + + + export class ChartMappings { public static type(value: string): GenericObject { return { chart: { type: value } }; } + public static series(value: any, chart: GenericObject): Array | GenericObject { + const dataMap = parseCsvToMap(chart.options.data.csv); + const columnNames = Array.from(dataMap.keys()).filter((key, index) => index !== 0 && !key.startsWith('lowvalue') && !key.startsWith('highvalue')); + if (value === 'Errorbar') { + const seriesArray: Array = []; + // Loop through the map to handle error bar data and normal data\ + columnNames.forEach((columnName) => { + if (columnName.startsWith('errorbardata')) { + // Handle error bar series + const errorBarData = dataMap.get(columnName) || []; + const errorBarSeries: GenericObject = { + name: columnName, + type: 'errorbar', + data: errorBarData + }; + seriesArray.push(errorBarSeries); + } else { + // Handle normal data series + const normalData = dataMap.get(columnName) || []; + const normalSeries: GenericObject = { + name: columnName, + type: 'column', + data: normalData + }; + seriesArray.push(normalSeries); + }}); + return seriesArray; + } + return { chart: { type: value } }; + } + public static legendEnabled(value: boolean): GenericObject { return { legend: { enabled: value } }; } diff --git a/src/app/editor/core/optionsMapper/chartOptionsMapper.ts b/src/app/editor/core/optionsMapper/chartOptionsMapper.ts index 4d125be..9c57c26 100644 --- a/src/app/editor/core/optionsMapper/chartOptionsMapper.ts +++ b/src/app/editor/core/optionsMapper/chartOptionsMapper.ts @@ -21,8 +21,16 @@ class ChartOptionsMapper { } public addChartParameter(param: string, value: unknown) { - const newOptions = (ChartMappings as any)[param](value, this.chart); - this.options = deepMerge(this.options, newOptions); + if (value === 'Errorbar') { + const newOptions = ChartMappings.series(value, this.chart); + if (!this.options.series) this.options.series = []; + newOptions.forEach((seriesConfig: any) => { + this.options.series.push(seriesConfig);}); + } else { + // Existing logic + const newOptions = (ChartMappings as any)[param](value, this.chart); + this.options = deepMerge(this.options, newOptions); + } } } @@ -41,5 +49,6 @@ export function getChartOptionsFromParameters( optionsMapper.addChartParameter(param, chartParameters[param]) ); + return optionsMapper.build(); } diff --git a/src/app/editor/core/optionsMapper/seriesMappings.ts b/src/app/editor/core/optionsMapper/seriesMappings.ts index c85c551..fab2913 100644 --- a/src/app/editor/core/optionsMapper/seriesMappings.ts +++ b/src/app/editor/core/optionsMapper/seriesMappings.ts @@ -20,6 +20,9 @@ export class SeriesMappings { } public static seriesType(value: string): GenericObject { + if(!value){ + return { type: '' }; + } return { type: value }; } diff --git a/src/app/editor/core/optionsMapper/seriesOptionsMapper.ts b/src/app/editor/core/optionsMapper/seriesOptionsMapper.ts index 84872c9..9c26def 100644 --- a/src/app/editor/core/optionsMapper/seriesOptionsMapper.ts +++ b/src/app/editor/core/optionsMapper/seriesOptionsMapper.ts @@ -7,14 +7,14 @@ import { GenericObject, deepMerge } from '../utils/objects'; * Map parameters to options object using series mappings and series sonification mappings. */ function toSeriesOptionsObject(seriesParameters: GenericObject): GenericObject { - let res = {}; + let res: GenericObject = {}; + res['type'] = ''; for (const [param, val] of Object.entries(seriesParameters)) { const mapper = (SeriesMappings as any)[param] || (SeriesSonificationMappings as any)[param]; const options = mapper ? mapper(val) : {}; res = deepMerge(res, options); } - return res; } diff --git a/src/app/editor/core/utils/chartUtils.ts b/src/app/editor/core/utils/chartUtils.ts index f283e99..d182c53 100644 --- a/src/app/editor/core/utils/chartUtils.ts +++ b/src/app/editor/core/utils/chartUtils.ts @@ -27,6 +27,9 @@ export const seriesTypes = [{ }, { name: 'Scatter', value: 'scatter' +},{ + name: 'Errorbar', + value: 'Errorbar' }]; export function getSeriesId(series: GenericObject): string { diff --git a/src/app/editor/store/modules/data.ts b/src/app/editor/store/modules/data.ts index f3fc974..e9a1ab2 100644 --- a/src/app/editor/store/modules/data.ts +++ b/src/app/editor/store/modules/data.ts @@ -1,15 +1,15 @@ /* - Data store for the chart/table data. +Data store for the chart/table data. - The source data for the table is kept here, and updating this will - update the data grid. Updating the data grid will trigger a recompute - of the CSV. We are using the table CSV generator in order to preserve - table sorting/filtering, but could consider rolling our own for better - performance. +The source data for the table is kept here, and updating this will +update the data grid. Updating the data grid will trigger a recompute +of the CSV. We are using the table CSV generator in order to preserve +table sorting/filtering, but could consider rolling our own for better +performance. - Updating values in the data grid should be reflected in the source data, - so that the source data always mirrors the data the grid is working with. - */ +Updating values in the data grid should be reflected in the source data, +so that the source data always mirrors the data the grid is working with. +*/ import Vue from 'vue'; import { parseCSV } from '../../core/utils/csvParser'; @@ -41,12 +41,22 @@ function getFillValue(row: GenericObject, rowIx: number, fillData: FillColumnPro } function getPlaceholderData() { - const res = [{ A: 'Index (X value)', B: 'Sensor Data (Y value)' }]; - - for (let i = 0; i < 175; ++i) { + const res = [{ A: 'Index (X value)', B: 'Sensor Data (Y value)', C: 'LowVaLue1', D: 'High Value 1', E: 'Lo wVaLue2', F: 'High Val ue 2' }]; + + for (let i = 0; i < 5; ++i) { + const B = (Math.sin(i / 3) * i / 2); + const C = (B * (0.85 + Math.random() * 0.14)).toFixed(3); // Randomly between 85% to 99% of B + const D = (B * (1.03 + Math.random() * 0.12)).toFixed(3); // Randomly between 103% to 115% of B + const E = (B * (0.85 + Math.random() * 0.14)).toFixed(3); // Randomly between 85% to 99% of B + const F = (B * (1.03 + Math.random() * 0.12)).toFixed(3); // Randomly between 103% to 115% of B + const bValue = B.toFixed(3); res.push({ A: '' + i, - B: (Math.sin(i / 3) * i / 2).toFixed(3) + B: bValue, + C: C, + D: D, + E:E, + F:F }); } @@ -80,6 +90,10 @@ export const dataStore = { return state.tableRowData.length; }, + tableRowData: (state: GenericObject): number => { + return state.tableRowData.length; + }, + tableColumnNamesWithData: (state: GenericObject): Array => { const rows = state.tableRowData; const res: GenericObject = {};