|
|
@@ -7,7 +7,7 @@ import moment from 'moment';
|
|
|
import Papa, { ParseError, ParseMeta } from 'papaparse';
|
|
|
|
|
|
// Types
|
|
|
-import { TableData, Column, TimeSeries, ColumnType } from '../types';
|
|
|
+import { SeriesData, Field, TimeSeries, FieldType, TableData } from '../types';
|
|
|
|
|
|
// Subset of all parse options
|
|
|
export interface TableParseOptions {
|
|
|
@@ -31,12 +31,12 @@ export interface TableParseDetails {
|
|
|
* @returns a new table that has equal length rows, or the same
|
|
|
* table if no changes were needed
|
|
|
*/
|
|
|
-export function matchRowSizes(table: TableData): TableData {
|
|
|
+export function matchRowSizes(table: SeriesData): SeriesData {
|
|
|
const { rows } = table;
|
|
|
- let { columns } = table;
|
|
|
+ let { fields } = table;
|
|
|
|
|
|
let sameSize = true;
|
|
|
- let size = columns.length;
|
|
|
+ let size = fields.length;
|
|
|
rows.forEach(row => {
|
|
|
if (size !== row.length) {
|
|
|
sameSize = false;
|
|
|
@@ -47,13 +47,13 @@ export function matchRowSizes(table: TableData): TableData {
|
|
|
return table;
|
|
|
}
|
|
|
|
|
|
- // Pad Columns
|
|
|
- if (size !== columns.length) {
|
|
|
- const diff = size - columns.length;
|
|
|
- columns = [...columns];
|
|
|
+ // Pad Fields
|
|
|
+ if (size !== fields.length) {
|
|
|
+ const diff = size - fields.length;
|
|
|
+ fields = [...fields];
|
|
|
for (let i = 0; i < diff; i++) {
|
|
|
- columns.push({
|
|
|
- text: 'Column ' + (columns.length + 1),
|
|
|
+ fields.push({
|
|
|
+ name: 'Field ' + (fields.length + 1),
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
@@ -72,30 +72,30 @@ export function matchRowSizes(table: TableData): TableData {
|
|
|
});
|
|
|
|
|
|
return {
|
|
|
- columns,
|
|
|
+ fields,
|
|
|
rows: fixedRows,
|
|
|
};
|
|
|
}
|
|
|
|
|
|
-function makeColumns(values: any[]): Column[] {
|
|
|
+function makeFields(values: any[]): Field[] {
|
|
|
return values.map((value, index) => {
|
|
|
if (!value) {
|
|
|
- value = 'Column ' + (index + 1);
|
|
|
+ value = 'Field ' + (index + 1);
|
|
|
}
|
|
|
return {
|
|
|
- text: value.toString().trim(),
|
|
|
+ name: value.toString().trim(),
|
|
|
};
|
|
|
});
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Convert CSV text into a valid TableData object
|
|
|
+ * Convert CSV text into a valid SeriesData object
|
|
|
*
|
|
|
* @param text
|
|
|
* @param options
|
|
|
* @param details, if exists the result will be filled with debugging details
|
|
|
*/
|
|
|
-export function parseCSV(text: string, options?: TableParseOptions, details?: TableParseDetails): TableData {
|
|
|
+export function parseCSV(text: string, options?: TableParseOptions, details?: TableParseDetails): SeriesData {
|
|
|
const results = Papa.parse(text, { ...options, dynamicTyping: true, skipEmptyLines: true });
|
|
|
const { data, meta, errors } = results;
|
|
|
|
|
|
@@ -118,7 +118,7 @@ export function parseCSV(text: string, options?: TableParseOptions, details?: Ta
|
|
|
details.errors = errors;
|
|
|
}
|
|
|
return {
|
|
|
- columns: [],
|
|
|
+ fields: [],
|
|
|
rows: [],
|
|
|
};
|
|
|
}
|
|
|
@@ -128,22 +128,35 @@ export function parseCSV(text: string, options?: TableParseOptions, details?: Ta
|
|
|
const header = headerIsNotFirstLine ? [] : results.data.shift();
|
|
|
|
|
|
return matchRowSizes({
|
|
|
- columns: makeColumns(header),
|
|
|
+ fields: makeFields(header),
|
|
|
rows: results.data,
|
|
|
});
|
|
|
}
|
|
|
|
|
|
-function convertTimeSeriesToTableData(timeSeries: TimeSeries): TableData {
|
|
|
+function convertTableToSeriesData(table: TableData): SeriesData {
|
|
|
+ return {
|
|
|
+ // rename the 'text' to 'name' field
|
|
|
+ fields: table.columns.map(c => {
|
|
|
+ const { text, ...field } = c;
|
|
|
+ const f = field as Field;
|
|
|
+ f.name = text;
|
|
|
+ return f;
|
|
|
+ }),
|
|
|
+ rows: table.rows,
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+function convertTimeSeriesToSeriesData(timeSeries: TimeSeries): SeriesData {
|
|
|
return {
|
|
|
name: timeSeries.target,
|
|
|
- columns: [
|
|
|
+ fields: [
|
|
|
{
|
|
|
- text: timeSeries.target || 'Value',
|
|
|
+ name: timeSeries.target || 'Value',
|
|
|
unit: timeSeries.unit,
|
|
|
},
|
|
|
{
|
|
|
- text: 'Time',
|
|
|
- type: ColumnType.time,
|
|
|
+ name: 'Time',
|
|
|
+ type: FieldType.time,
|
|
|
unit: 'dateTimeAsIso',
|
|
|
},
|
|
|
],
|
|
|
@@ -151,10 +164,10 @@ function convertTimeSeriesToTableData(timeSeries: TimeSeries): TableData {
|
|
|
};
|
|
|
}
|
|
|
|
|
|
-export const getFirstTimeColumn = (table: TableData): number => {
|
|
|
- const { columns } = table;
|
|
|
- for (let i = 0; i < columns.length; i++) {
|
|
|
- if (columns[i].type === ColumnType.time) {
|
|
|
+export const getFirstTimeField = (table: SeriesData): number => {
|
|
|
+ const { fields } = table;
|
|
|
+ for (let i = 0; i < fields.length; i++) {
|
|
|
+ if (fields[i].type === FieldType.time) {
|
|
|
return i;
|
|
|
}
|
|
|
}
|
|
|
@@ -170,45 +183,45 @@ const NUMBER = /^\s*-?(\d*\.?\d+|\d+\.?\d*)(e[-+]?\d+)?\s*$/i;
|
|
|
*
|
|
|
* TODO: better Date/Time support! Look for standard date strings?
|
|
|
*/
|
|
|
-export function guessColumnTypeFromValue(v: any): ColumnType {
|
|
|
+export function guessFieldTypeFromValue(v: any): FieldType {
|
|
|
if (isNumber(v)) {
|
|
|
- return ColumnType.number;
|
|
|
+ return FieldType.number;
|
|
|
}
|
|
|
|
|
|
if (isString(v)) {
|
|
|
if (NUMBER.test(v)) {
|
|
|
- return ColumnType.number;
|
|
|
+ return FieldType.number;
|
|
|
}
|
|
|
|
|
|
if (v === 'true' || v === 'TRUE' || v === 'True' || v === 'false' || v === 'FALSE' || v === 'False') {
|
|
|
- return ColumnType.boolean;
|
|
|
+ return FieldType.boolean;
|
|
|
}
|
|
|
|
|
|
- return ColumnType.string;
|
|
|
+ return FieldType.string;
|
|
|
}
|
|
|
|
|
|
if (isBoolean(v)) {
|
|
|
- return ColumnType.boolean;
|
|
|
+ return FieldType.boolean;
|
|
|
}
|
|
|
|
|
|
if (v instanceof Date || v instanceof moment) {
|
|
|
- return ColumnType.time;
|
|
|
+ return FieldType.time;
|
|
|
}
|
|
|
|
|
|
- return ColumnType.other;
|
|
|
+ return FieldType.other;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Looks at the data to guess the column type. This ignores any existing setting
|
|
|
*/
|
|
|
-function guessColumnTypeFromTable(table: TableData, index: number): ColumnType | undefined {
|
|
|
- const column = table.columns[index];
|
|
|
+function guessFieldTypeFromTable(table: SeriesData, index: number): FieldType | undefined {
|
|
|
+ const column = table.fields[index];
|
|
|
|
|
|
// 1. Use the column name to guess
|
|
|
- if (column.text) {
|
|
|
- const name = column.text.toLowerCase();
|
|
|
+ if (column.name) {
|
|
|
+ const name = column.name.toLowerCase();
|
|
|
if (name === 'date' || name === 'time') {
|
|
|
- return ColumnType.time;
|
|
|
+ return FieldType.time;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -216,7 +229,7 @@ function guessColumnTypeFromTable(table: TableData, index: number): ColumnType |
|
|
|
for (let i = 0; i < table.rows.length; i++) {
|
|
|
const v = table.rows[i][index];
|
|
|
if (v !== null) {
|
|
|
- return guessColumnTypeFromValue(v);
|
|
|
+ return guessFieldTypeFromValue(v);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -228,20 +241,20 @@ function guessColumnTypeFromTable(table: TableData, index: number): ColumnType |
|
|
|
* @returns a table Returns a copy of the table with the best guess for each column type
|
|
|
* If the table already has column types defined, they will be used
|
|
|
*/
|
|
|
-export const guessColumnTypes = (table: TableData): TableData => {
|
|
|
- for (let i = 0; i < table.columns.length; i++) {
|
|
|
- if (!table.columns[i].type) {
|
|
|
+export const guessFieldTypes = (table: SeriesData): SeriesData => {
|
|
|
+ for (let i = 0; i < table.fields.length; i++) {
|
|
|
+ if (!table.fields[i].type) {
|
|
|
// Somethign is missing a type return a modified copy
|
|
|
return {
|
|
|
...table,
|
|
|
- columns: table.columns.map((column, index) => {
|
|
|
+ fields: table.fields.map((column, index) => {
|
|
|
if (column.type) {
|
|
|
return column;
|
|
|
}
|
|
|
// Replace it with a calculated version
|
|
|
return {
|
|
|
...column,
|
|
|
- type: guessColumnTypeFromTable(table, index),
|
|
|
+ type: guessFieldTypeFromTable(table, index),
|
|
|
};
|
|
|
}),
|
|
|
};
|
|
|
@@ -251,21 +264,26 @@ export const guessColumnTypes = (table: TableData): TableData => {
|
|
|
return table;
|
|
|
};
|
|
|
|
|
|
-export const isTableData = (data: any): data is TableData => data && data.hasOwnProperty('columns');
|
|
|
+export const isTableData = (data: any): data is SeriesData => data && data.hasOwnProperty('columns');
|
|
|
|
|
|
-export const toTableData = (data: any): TableData => {
|
|
|
- if (data.hasOwnProperty('columns')) {
|
|
|
- return data as TableData;
|
|
|
+export const isSeriesData = (data: any): data is SeriesData => data && data.hasOwnProperty('fields');
|
|
|
+
|
|
|
+export const toSeriesData = (data: any): SeriesData => {
|
|
|
+ if (data.hasOwnProperty('fields')) {
|
|
|
+ return data as SeriesData;
|
|
|
}
|
|
|
if (data.hasOwnProperty('datapoints')) {
|
|
|
- return convertTimeSeriesToTableData(data);
|
|
|
+ return convertTimeSeriesToSeriesData(data);
|
|
|
+ }
|
|
|
+ if (data.hasOwnProperty('columns')) {
|
|
|
+ return convertTableToSeriesData(data);
|
|
|
}
|
|
|
// TODO, try to convert JSON/Array to table?
|
|
|
console.warn('Can not convert', data);
|
|
|
throw new Error('Unsupported data format');
|
|
|
};
|
|
|
|
|
|
-export function sortTableData(data: TableData, sortIndex?: number, reverse = false): TableData {
|
|
|
+export function sortSeriesData(data: SeriesData, sortIndex?: number, reverse = false): SeriesData {
|
|
|
if (isNumber(sortIndex)) {
|
|
|
const copy = {
|
|
|
...data,
|