From 6dedd2f75e9f841e9eb11269409a32ce5197c8fe Mon Sep 17 00:00:00 2001 From: Janis Hutz Date: Mon, 20 Oct 2025 15:09:24 +0200 Subject: [PATCH] Task 80% complete --- task_2_ts/index.html | 11 ++- task_2_ts/ts/main.ts | 134 ++++++++++++++++++++++++++++++++++-- task_2_ts/ts/persistance.ts | 24 +++++++ task_2_ts/ts/types.d.ts | 9 +++ task_2_ts/ts/util.ts | 48 +++++++++++++ 5 files changed, 213 insertions(+), 13 deletions(-) create mode 100644 task_2_ts/ts/persistance.ts create mode 100644 task_2_ts/ts/util.ts diff --git a/task_2_ts/index.html b/task_2_ts/index.html index 39bf0b3..5d5fb61 100644 --- a/task_2_ts/index.html +++ b/task_2_ts/index.html @@ -59,11 +59,11 @@

Data type

-

Different entries

+

Different entries

-

Min

+

Min

-

Max

+

Max

@@ -72,8 +72,8 @@

Filter active column

- + +
@@ -83,7 +83,6 @@ -
diff --git a/task_2_ts/ts/main.ts b/task_2_ts/ts/main.ts index cad5699..b9a3369 100644 --- a/task_2_ts/ts/main.ts +++ b/task_2_ts/ts/main.ts @@ -1,6 +1,11 @@ import '@fortawesome/fontawesome-free/css/all.css'; import '../css/layout.css'; import '@picocss/pico/css/pico.min.css'; +import { + computeDifferent, + computeMinMax, + numberCheckPredicate, stringOrNumberCheckPredicate +} from './util'; import { listRef, ref } from './rendering'; @@ -11,6 +16,11 @@ import { readCSV } from './csv'; + +// ┌ ┐ +// │ Define refs │ +// └ ┘ +const tableHeaderElement = document.getElementById( 'table-header' )!; const dataList = listRef( document.getElementById( 'table-body' )!, [], @@ -22,7 +32,7 @@ const dataList = listRef( } ); const headerList = listRef( - document.getElementById( 'table-header' )!, + tableHeaderElement, [], 'table-header', { @@ -31,31 +41,56 @@ const headerList = listRef( 'children': [] } ); -const filter = ref( [ document.getElementById( 'filter' )! ], '' ); +const columnEntriesElement = document.getElementById( 'column-entries' )!; +const columnMaxElement = document.getElementById( 'column-max' )!; +const columnMinElement = document.getElementById( 'column-min' )!; +const filter = ref( [], '' ); +const filterInput = document.getElementById( 'filter' )! as HTMLInputElement; const filename = ref( [ document.getElementById( 'data-filename' )! ], '' ); const filetype = ref( [ document.getElementById( 'data-filetype' )! ], '' ); const filesize = ref( [ document.getElementById( 'data-filesize' )! ], '' ); const rowCount = ref( [ document.getElementById( 'data-rowcount' )! ], '' ); const columnName = ref( [ document.getElementById( 'column-selected' )! ], '' ); const columnDatatype = ref( [ document.getElementById( 'column-datatype' )! ], '' ); -const columnEntries = ref( [ document.getElementById( 'column-entries' )! ], '' ); -const columnMax = ref( [ document.getElementById( 'column-max' )! ], '' ); -const columnMin = ref( [ document.getElementById( 'column-min' )! ], '' ); +const columnEntries = ref( [ columnEntriesElement ], 0 ); +const columnMax = ref( [ columnMaxElement ], 0 ); +const columnMin = ref( [ columnMinElement ], 0 ); const fileInput = document.getElementById( 'file-input' )! as HTMLInputElement; +const ascendingSort = ref( [], true ); + +let selectedColumn = ''; + +filterInput.disabled = true; +filterInput.value = ''; -// Bind to file input event +// ┌ ┐ +// │ conditional rendering of some elements │ +// └ ┘ +columnDatatype.addConditionalElementBind( columnMinElement, numberCheckPredicate ); +columnDatatype.addConditionalElementBind( columnMaxElement, numberCheckPredicate ); +columnDatatype.addConditionalElementBind( document.getElementById( 'title-column-min' )!, numberCheckPredicate ); +columnDatatype.addConditionalElementBind( document.getElementById( 'title-column-max' )!, numberCheckPredicate ); +columnDatatype.addConditionalElementBind( columnEntriesElement, stringOrNumberCheckPredicate ); +columnDatatype.addConditionalElementBind( document.getElementById( 'title-column-entries' )!, + stringOrNumberCheckPredicate ); + + +// ┌ ┐ +// │ Bind to file input event │ +// └ ┘ fileInput.addEventListener( 'change', event => { loadFile( event ); } ); + const loadFile = ( event: Event ) => { if ( fileInput.files && fileInput.files.length > 0 ) { const file = fileInput.files[0]!; filename.set( file.name ); filetype.set( file.type ); - filesize.set( String( file.size ) + 'B' ); // TODO: KB / MB conversion stuff + filesize.set( String( file.size ) + 'B' ); // TODO: KB / MB conversion stuff? readCSV( event ) .then( data => { // Row count @@ -66,6 +101,33 @@ const loadFile = ( event: Event ) => { headerList.set( header ); + + // Initialize sorting + for ( let i = 0; i < header.length; i++ ) { + const column = header[ i ]!; + + document.getElementById( 'table-header--' + i )!.addEventListener( 'click', () => { + // TODO: Decide on sorting cycling + // TODO: Add indicator as well + if ( selectedColumn === column ) { + ascendingSort.set( !ascendingSort.get() ); + } else { + // This column will now be the active column + selectedColumn = column; + ascendingSort.set( true ); + const dtype = typeof dataList.get()[0]![ column ]; + + columnDatatype.set( dtype ); + columnName.set( column ); + + if ( dtype === 'string' ) + filterInput.disabled = false; + else + filterInput.disabled = true; + } + } ); + } + // ── Generate list. Need to first generate the correct template ─── // Reset, to not trigger expensive rerender dataList.set( [] ); @@ -92,3 +154,61 @@ const loadFile = ( event: Event ) => { alert( 'No file selected' ); } }; + +// ┌ ┐ +// │ Sorting │ +// └ ┘ +const doSort = () => { + filter.set( '' ); + + if ( columnDatatype.get() === 'string' ) { + columnEntries.set( computeDifferent( dataList.get(), selectedColumn ) ); + + if ( ascendingSort.get() ) { + dataList.sort( ( a, b ) => { + return ( a[ selectedColumn ] as string ).localeCompare( b[ selectedColumn ] as string ); + } ); + } else { + dataList.sort( ( a, b ) => { + return ( b[ selectedColumn ] as string ).localeCompare( a[ selectedColumn ] as string ); + } ); + } + } else if ( columnDatatype.get() === 'number' ) { + const stats = computeMinMax( dataList.get(), selectedColumn ); + + columnMin.set( stats[ 0 ] ); + columnMax.set( stats[ 1 ] ); + columnEntries.set( stats[ 2 ] ); + + if ( ascendingSort.get() ) { + dataList.sort( ( a, b ) => { + return ( a[ selectedColumn ] as number ) - ( b[ selectedColumn ] as number ); + } ); + } else { + dataList.sort( ( a, b ) => { + return ( b[ selectedColumn ] as number ) - ( a[ selectedColumn ] as number ); + } ); + } + } +}; + +columnName.onChange( doSort ); +ascendingSort.onChange( doSort ); + + +// ┌ ┐ +// │ Filtering │ +// └ ┘ +// Bind filter ref to element +filter.bind( filterInput, val => val ); + +// Add listener to change of filter value. +filter.onChange( () => { + if ( columnDatatype.get() === 'string' ) { + dataList.filter( a => { + return ( a[ selectedColumn ] as string ).includes( filter.get() ); + } ); + } else { + dataList.filter( () => true ); + } +} ); diff --git a/task_2_ts/ts/persistance.ts b/task_2_ts/ts/persistance.ts new file mode 100644 index 0000000..168ba95 --- /dev/null +++ b/task_2_ts/ts/persistance.ts @@ -0,0 +1,24 @@ +import { + PersistanceConfig +} from './types'; + +// Using localStorage for persistance +const persistanceStore: PersistanceConfig = JSON.parse( localStorage.getItem( 'persistance' ) ?? '{}' ); + +export const store = ( + filename: string, + size: number, + sorted: string, + active: string +) => { + persistanceStore[ `${ filename }-${ size }` ] = { + 'active': active, + 'sorted': sorted + }; + localStorage.setItem( 'persistance', JSON.stringify( persistanceStore ) ); +}; + + +export const get = ( filename: string, size: number ) => { + return persistanceStore[ `${ filename }-${ size }` ]; +}; diff --git a/task_2_ts/ts/types.d.ts b/task_2_ts/ts/types.d.ts index 407cefc..6cb0c42 100644 --- a/task_2_ts/ts/types.d.ts +++ b/task_2_ts/ts/types.d.ts @@ -2,3 +2,12 @@ export type CSVRecord = Record; export type CSV_Data = CSVRecord[]; + +export interface PersistanceConfigEntry { + 'sorted': string; + 'active': string; +} + +export interface PersistanceConfig { + [filename: `${ string }-${ number }`]: PersistanceConfigEntry +} diff --git a/task_2_ts/ts/util.ts b/task_2_ts/ts/util.ts new file mode 100644 index 0000000..ef6faf3 --- /dev/null +++ b/task_2_ts/ts/util.ts @@ -0,0 +1,48 @@ +import { + CSVRecord +} from './types'; + +export const numberCheckPredicate = ( value: string ) => { + return value === 'number'; +}; + +export const stringOrNumberCheckPredicate = ( value: string ) => { + return value === 'string' || value === 'number'; +}; + + +export const computeMinMax = ( list: CSVRecord[], selectedColumn: string ): [number, number, number] => { + const containsList: number[] = []; + + let min = Number.MAX_VALUE; + let max = Number.MIN_VALUE; + + for ( let i = 0; i < list.length; i++ ) { + const el = list[i]!; + const curr = el[ selectedColumn ]! as number; + + if ( curr < min ) min = curr; + else if ( curr > max ) max = curr; + + if ( !containsList.includes( curr ) ) containsList.push( curr ); + } + + return [ + min, + max, + containsList.length + ]; +}; + +export const computeDifferent = ( list: CSVRecord[], selectedColumn: string ): number => { + const containsList: string[] = []; + + for ( let i = 0; i < list.length; i++ ) { + const el = list[i]!; + const curr = el[ selectedColumn ]! as string; + + if ( !containsList.includes( curr ) ) containsList.push( curr ); + } + + return containsList.length; +};