import _ from 'lodash' ; import sha256 from 'crypto-js/sha256' ; import { INDEX_COLUMN } from '@/common/constants' ; import { SubsetItem } from '@/data/package/subset-package' ; import { ValueType , generateCsv , parseCsv } from '@/data/parser' ; import { isContinuousDomain , valueComparator } from '@/data/util' ; export type TabularRow = Array < number | string > ; export type TabularRows = TabularRow [ ] ; export interface TabularColumn { index : number ; name : string ; type : ValueType ; hasDuplicate : boolean ; } interface SubTableOptions { indexColumn ? : boolean ; forScale ? : boolean ; } const arrayIndices = ( indices ) : number [ ] => { return indices instanceof Set ? Array . from ( indices ) : indices ; } ; /** Creates a hash value based on the dataset content. */ const hash = ( columns , rows ) : string => { const columnStr = columns . map ( column => column . name ) . join ( ',' ) ; const rowStr = rows . map ( row => row . map ( e => e . toString ( ) ) . join ( ',' ) ) . join ( ',' ) ; return sha256 ( columnStr + ',' + rowStr ) . toString ( ) ; } ; export default class TabularDataset { public static deserialize ( jsonStr ) { const obj = JSON . parse ( jsonStr ) ; const dataset = new TabularDataset ( { columns : obj . columns , rows : obj . rows } ) ; dataset . setName ( obj . name ) ; return dataset ; } public static fromColumnsAndRows ( columns , rows ) { return parseCsv ( generateCsv ( columns , rows ) ) ; } private name : string = '' ; private columns : TabularColumn [ ] = [ ] ; private rows : TabularRows = [ ] ; private hash : string = '' ; constructor ( { columns , rows } ) { this . columns = columns ; this . rows = rows ; this . hash = hash ( columns , rows ) ; } public getName ( ) { return this . name ; } public getColumnIndex ( columnName ) { const column = this . columns . find ( col => col . name === columnName ) ; return column ? column . index : null ; } public setName ( name ) { this . name = name ; } public numRows ( ) { return this . rows . length ; } public numColumns ( ) { return this . columns . length ; } /**
     * Retrieves the raw value of a cell, with a given row and column.
     */ public getCell ( item , columnIndex ) { const rowIndex = item instanceof Object ? ( item as SubsetItem ) . index : item ; if ( columnIndex === INDEX_COLUMN ) { return rowIndex ; } return this . rows [ rowIndex ] [ columnIndex ] ; } /**
     * Retrieves a normalized value of a cell that works with ScaleTime.
     */ public getCellForScale ( item , columnIndex ) { const rowIndex = item instanceof Object ? ( item as SubsetItem ) . index : item ; const value = this . rows [ rowIndex ] [ columnIndex ] ; if ( columnIndex === INDEX_COLUMN ) { return rowIndex ; } return this . columns [ columnIndex ] . type === ValueType . DATE ? new Date ( value ) . getTime ( ) : value ; } public getColumn ( index ) { return this . columns [ index ] ; } public getColumns ( ) { return this . columns ; } public getColumnType ( columnIndex ) { if ( columnIndex === INDEX_COLUMN ) { return ValueType . INT ; } return this . columns [ columnIndex ] . type ; } public getColumnName ( columnIndex ) { if ( columnIndex === INDEX_COLUMN ) { return '[index]' ; } return this . columns [ columnIndex ] . name ; } public isDateColumn ( index ) { return this . columns [ index ] . type === ValueType . DATE ; } public getRow ( itemIndex ) { return this . rows [ itemIndex ] ; } public getRows ( ) { return this . rows ; } /**
     * Gets a subset of rows with values on a subset of columns.
     */ public subRowsOnSubColumns ( rowIndices , columnIndices , options ? ) { rowIndices = arrayIndices ( rowIndices ) ; columnIndices = arrayIndices ( columnIndices ) ; return rowIndices . map ( rowIndex => this . rowOnSubColumns ( rowIndex , columnIndices , options ) ) ; } /**
     * Gets all the rows with values on a subset of columns.
     */ public allRowsOnSubColumns ( columnIndices , options ? ) { columnIndices = arrayIndices ( columnIndices ) ; return this . rows . map ( ( row , rowIndex ) => this . rowOnSubColumns ( rowIndex , columnIndices , options ) ) ; } /**
     * Gets one row with values on a subset of columns.
     */ public rowOnSubColumns ( rowIndex , columnIndices , options_ ? ) { const options = options_ || { } ; columnIndices = arrayIndices ( columnIndices ) ; const row = options . indexColumn ? [ rowIndex ] : [ ] ; columnIndices . forEach ( columnIndex => { const value = this . rows [ rowIndex ] [ columnIndex ] ; if ( options . forScale && this . columns [ columnIndex ] . type === ValueType . DATE ) { row . push ( new Date ( value ) . getTime ( ) ) ; } else { row . push ( value ) ; } } ) ; return row ; } /**
     * Computes the value domain on a given column for a given set of items. If items are not given, then all items
     * are considered.
     */ public getDomain ( columnIndex , items ? ) { items = items || _ . range ( this . numRows ( ) ) ; if ( ! items . length ) { return [ ] ; } const columnType = this . columns [ columnIndex ] . type ; const comparator = valueComparator ( columnType ) ; if ( isContinuousDomain ( columnType ) ) { let min ; let max ; items . forEach ( itemIndex => { const value = this . rows [ itemIndex ] [ columnIndex ] ; if ( min === undefined || comparator ( value , min ) < 0 ) { min = value ; } if ( max === undefined || comparator ( value , max ) > 0 ) { max = value ; } } ) ; if ( min === undefined || max === undefined ) { return [ ] ; } return [ min , max ] ; } else { return _ . uniq ( items . map ( index => this . rows [ index ] [ columnIndex ] ) ) . sort ( comparator ) ; } } /**
     * Computes the sorted values that appear on a column, w.r.t. to an optional list of items.
     * If "distinct" is set, return only unique values.
     */ public getDomainValues ( columnIndex , items ? , distinct ? ) { items = items || _ . range ( this . numRows ( ) ) ; if ( columnIndex === INDEX_COLUMN ) { return items ; } const values = items . map ( index => this . rows [ index ] [ columnIndex ] ) . sort ( valueComparator ( this . getColumnType ( columnIndex ) ) ) ; return distinct ? _ . uniq ( values ) : values ; } /**
     * Checks if a given column has duplicate values on the given items. If no items are given, return if a given column
     * has duplicates from the original dataset.
     * Values are compared by their string values.
     */ public hasDuplicates ( columnIndex , items ? ) { if ( columnIndex === INDEX_COLUMN ) { return false ; } items = items || _ . range ( this . numRows ( ) ) ; const valueSet = new Set < string > ( ) ; for ( const itemIndex of items ) { const value = this . rows [ itemIndex ] [ columnIndex ] . toString ( ) ; if ( valueSet . has ( value ) ) { return true ; } } return false ; } public getHash ( ) { return this . hash ; } public serialize ( ) { return JSON . stringify ( this ) ; } }