From da3a1f198bd8baae1601aa516844bab60ed9c92d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Apr 2026 20:24:05 +0000 Subject: [PATCH 1/3] Initial plan From c9998b56bd8a17fdf80cbd0a3cda6eef62bf7276 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Apr 2026 20:28:30 +0000 Subject: [PATCH 2/3] Auto-detect tab-delimited CSV files by inspecting first line delimiters Agent-Logs-Url: https://github.com/microsoft/SandDance/sessions/a48a5e1c-2717-40cd-b665-024693b751a1 Co-authored-by: danmarshall <11507384+danmarshall@users.noreply.github.com> --- packages/sanddance-explorer/src/dataLoader.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/sanddance-explorer/src/dataLoader.ts b/packages/sanddance-explorer/src/dataLoader.ts index 42bb6dd44..549f9634b 100644 --- a/packages/sanddance-explorer/src/dataLoader.ts +++ b/packages/sanddance-explorer/src/dataLoader.ts @@ -11,20 +11,31 @@ import { } from './interfaces'; import { SandDance } from '@msrvida/sanddance-react'; +export function guessDelimiterType(text: string, type: DataFileType): DataFileType { + if (type !== 'csv') return type; + const firstLine = text.split(/\r?\n/)[0]; + // Strip quoted strings to avoid counting delimiters inside quotes + const unquoted = firstLine.replace(/"[^"]*"/g, '""'); + const tabCount = (unquoted.match(/\t/g) || []).length; + const commaCount = (unquoted.match(/,/g) || []).length; + return tabCount > commaCount ? 'tsv' : 'csv'; +} + export const loadDataFile = (dataFile: DataFile, columnTypes?: SandDance.types.ColumnTypeMap) => new Promise((resolve, reject) => { const vega = SandDance.VegaMorphCharts.base.vega; const loader = vega.loader(); function handleRawText(text: string) { let data: object[]; + const type = guessDelimiterType(text, dataFile.type); try { - data = vega.read(text, { type: dataFile.type, parse: {} }); + data = vega.read(text, { type, parse: {} }); } catch (e) { reject(e); } if (data) { - loadDataArray(data, dataFile.type, columnTypes).then(dc => { + loadDataArray(data, type, columnTypes).then(dc => { if (dataFile.snapshotsUrl) { fetch(dataFile.snapshotsUrl) .then(response => response.json()) From 8226757fd661b9f5d9fabdc6aa44008be250b867 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 01:20:28 +0000 Subject: [PATCH 3/3] Add delimiter dropdown to DataBrowser for manual CSV/TSV override Agent-Logs-Url: https://github.com/microsoft/SandDance/sessions/2c5b6167-e21b-4fdb-8d7b-9e36d2af61c1 Co-authored-by: danmarshall <11507384+danmarshall@users.noreply.github.com> --- packages/sanddance-explorer/src/dataLoader.ts | 3 ++- .../src/dialogs/dataBrowser.tsx | 25 ++++++++++++++++++- packages/sanddance-explorer/src/explorer.tsx | 7 +++++- packages/sanddance-explorer/src/interfaces.ts | 2 ++ packages/sanddance-explorer/src/language.ts | 3 +++ 5 files changed, 37 insertions(+), 3 deletions(-) diff --git a/packages/sanddance-explorer/src/dataLoader.ts b/packages/sanddance-explorer/src/dataLoader.ts index 549f9634b..55e5bbf20 100644 --- a/packages/sanddance-explorer/src/dataLoader.ts +++ b/packages/sanddance-explorer/src/dataLoader.ts @@ -27,7 +27,7 @@ export const loadDataFile = (dataFile: DataFile, columnTypes?: SandDance.types.C function handleRawText(text: string) { let data: object[]; - const type = guessDelimiterType(text, dataFile.type); + const type = dataFile.noTypeGuess ? dataFile.type : guessDelimiterType(text, dataFile.type); try { data = vega.read(text, { type, parse: {} }); } @@ -36,6 +36,7 @@ export const loadDataFile = (dataFile: DataFile, columnTypes?: SandDance.types.C } if (data) { loadDataArray(data, type, columnTypes).then(dc => { + dc.type = type; // communicate the actual parsed type back to the caller if (dataFile.snapshotsUrl) { fetch(dataFile.snapshotsUrl) .then(response => response.json()) diff --git a/packages/sanddance-explorer/src/dialogs/dataBrowser.tsx b/packages/sanddance-explorer/src/dialogs/dataBrowser.tsx index 94a3f7522..cd5347cf0 100644 --- a/packages/sanddance-explorer/src/dialogs/dataBrowser.tsx +++ b/packages/sanddance-explorer/src/dialogs/dataBrowser.tsx @@ -4,7 +4,7 @@ */ import { base } from '../base'; -import { DataExportHandler } from '../interfaces'; +import { DataExportHandler, DataFile, DataFileType } from '../interfaces'; import { DataExportPicker, removeExtensions } from '../controls/dataExporter'; import { DataItem } from '../controls/dataItem'; import { DataScopeId } from '../controls/dataScope'; @@ -49,6 +49,8 @@ export interface Props { displayName: string; explorer: Explorer_Class; onUpdateColumnTypes: (columnTypes: SandDance.types.ColumnTypeMap) => void; + dataFile?: DataFile; + onReloadFileType?: (type: DataFileType) => void; } export function DataBrowser(props: Props) { @@ -87,6 +89,27 @@ export function DataBrowser(props: Props) { props.onDataScopeClick(o.key as DataScopeId); }} /> + {props.dataFile && (props.dataFile.type === 'csv' || props.dataFile.type === 'tsv') && props.onReloadFileType && ( + { + props.onReloadFileType(o.key as DataFileType); + }} + /> + )} {!props.data &&
} {props.data && !props.data.length &&
{props.zeroMessage}
} {!!length &&
diff --git a/packages/sanddance-explorer/src/explorer.tsx b/packages/sanddance-explorer/src/explorer.tsx index 8a5ff478b..f6e8779fd 100644 --- a/packages/sanddance-explorer/src/explorer.tsx +++ b/packages/sanddance-explorer/src/explorer.tsx @@ -495,7 +495,7 @@ function _Explorer(_props: Props) { const newState: Partial = { camera: undefined, columns: {}, - dataFile, + dataFile: dataContent.type ? { ...dataFile, type: dataContent.type } : dataFile, dataContent, snapshots: dataContent.snapshots || this.state.snapshots, autoCompleteDistinctValues: {}, @@ -1284,6 +1284,11 @@ function _Explorer(_props: Props) { onUpdateColumnTypes={columnTypes => { this.load(this.state.dataFile, null, { prefs: this.prefs, columnTypes }); }} + dataFile={this.state.dataFile} + onReloadFileType={type => { + const dataFile = { ...this.state.dataFile, type, noTypeGuess: true }; + this.load(dataFile, null, { prefs: this.prefs }); + }} /> ); } diff --git a/packages/sanddance-explorer/src/interfaces.ts b/packages/sanddance-explorer/src/interfaces.ts index 945be9db9..78f575226 100644 --- a/packages/sanddance-explorer/src/interfaces.ts +++ b/packages/sanddance-explorer/src/interfaces.ts @@ -19,12 +19,14 @@ export interface DataFile { rawText?: string; snapshots?: Snapshot[]; type: DataFileType; + noTypeGuess?: boolean; } export interface DataContent { data: object[]; columns: SandDance.types.Column[]; snapshots?: Snapshot[]; + type?: DataFileType; } export type DataExportType = DataFileType | 'html'; diff --git a/packages/sanddance-explorer/src/language.ts b/packages/sanddance-explorer/src/language.ts index 1dcf351a2..9f232899c 100644 --- a/packages/sanddance-explorer/src/language.ts +++ b/packages/sanddance-explorer/src/language.ts @@ -90,6 +90,9 @@ export const strings = { labelColumnQuantitativeMean: 'Mean', labelDataBrowser: 'Data browser', labelDataScope: 'Scope', + labelDelimiter: 'Delimiter', + labelDelimiterComma: 'Comma (,)', + labelDelimiterTab: 'Tab', labelExport: 'Export Data', labelExportFormat: 'File format', labelExportCSV: '.CSV - Comma separated values',