import { getElementByIdOrThrow , State } from "./utils" ; declare namespace google { namespace charts { function load ( version , options ) ; export class ChartOptions { } export class Line { static convertOptions ( options ) ; } } export namespace visualization { export class DataTable { addColumn ( type , legend ) ; } interface Chart { draw ( chartData , chartOptions ) : void ; } export class ComboChart implements Chart { constructor ( host ) ; draw ( chartData , chartOptions ) ; } export namespace events { interface ChartEventListener { } function addListener ( chart , eventName , handler ) ; function removeListener ( chartSelectEvent ) ; } } } let chartDefined = false ; try { google . charts . load ( 'current' , { packages : [ 'corechart' , 'line' ] } ) ; chartDefined = true ; } catch ( err ) { console . log ( err ) ; } export class StateChart { /** Translates state ID to chart dataset row ID */ private stateIdToRowId = new Map < number , number > ( ) ; /** Translates chart row ID to State ID */ private rowIdToStateId = new Map < number , number > ( ) ; private chart : any ; private chartData : any ; private chartOptions : google . charts . ChartOptions ; private isReDrawing = false ; private chartNeedsReDrawing = false ; constructor ( private onStateSelected ) { this . chartData = new google . visualization . DataTable ( ) ; this . chartData . addColumn ( 'number' , 'State' ) ; this . chartData . addColumn ( 'number' , 'Now' ) ; this . chartData . addColumn ( 'number' , 'Makespan' ) ; this . chartData . addColumn ( 'number' , 'H' ) ; const options = { title : 'Evaluated states' , hAxis : { title : 'State' } , seriesType : 'area' , series : { 0 : { targetAxisIndex : 1 , color : 'green' , lineWidth : 1 } , 1 : { targetAxisIndex : 1 , color : 'lightgreen' , lineWidth : 1 } , 2 : { type : 'line' , targetAxisIndex : 0 , color : 'blue' } } , vAxes : { 0 : { title : 'Heuristic value' , textStyle : { color : 'blue' } , minValue : 0 } , 1 : { title : 'Makespan' , textStyle : { color : 'green' } } } , crosshair : { trigger : 'both' , orientation : 'vertical' , color : 'gray' } } ; this . chartOptions = google . charts . Line . convertOptions ( options ) ; this . chart = this . reSizeChart ( ) ; google . visualization . events . addListener ( this . chart , 'ready' , ( ) => { if ( this . chartNeedsReDrawing ) { this . reDrawChart ( ) ; } } ) ; } reSizeChart ( ) { this . chart = new google . visualization . ComboChart ( getElementByIdOrThrow ( 'chart_div' ) ) ; this . reDrawChart ( ) ; return this . chart ; } private reDrawChart ( ) { if ( this . isReDrawing ) { this . chartNeedsReDrawing = true ; } else { this . isReDrawing = true ; this . chartNeedsReDrawing = false ; new Promise ( ( ) => { this . chart . draw ( this . chartData , this . chartOptions ) ; this . isReDrawing = false ; } ) ; } } param stateId selectChartRow ( stateId ) { if ( ! chartDefined ) { return ; } if ( stateId !== null ) { const rowId = this . stateIdToRowId . get ( stateId ) ; this . chart . setSelection ( [ { row : rowId } ] ) ; } else { this . chart . setSelection ( ) ; } } param newState param batch addStateToChart ( newState , batch ) { if ( this . chartData ) { const rowId = this . chartData . addRow ( [ newState . id , newState . earliestTime , sanitizeNumber ( newState . totalMakespan ) , sanitizeNumber ( newState . h ) , ] ) ; this . addRowId ( rowId , newState . id ) ; if ( ! batch ) { this . reDrawChart ( ) ; } } } param rowId param stateId addRowId ( rowId , stateId ) { this . rowIdToStateId . set ( rowId , stateId ) ; this . stateIdToRowId . set ( stateId , rowId ) ; } endChartBatch ( ) { this . reDrawChart ( ) ; } param state updateStateOnChart ( state ) { const rowId = this . stateIdToRowId . get ( state . id ) ; this . chartData . setValue ( rowId , MAKESPAN_COLUMN , sanitizeNumber ( state . totalMakespan ) ) ; this . chartData . setValue ( rowId , H_COLUMN , sanitizeNumber ( state . h ) ) ; this . reDrawChart ( ) ; } clearChart ( ) { const rowsToRemove = this . chartData . getNumberOfRows ( ) ; this . chartData . removeRows ( 0 , rowsToRemove ) ; this . stateIdToRowId . clear ( ) ; this . rowIdToStateId . clear ( ) ; this . reDrawChart ( ) ; console . log ( "Removed " + rowsToRemove + " rows from the chart data table." ) ; } param offset returns navigateChart ( offset ) { const selection = this . chart . getSelection ( ) ; if ( selection . length > 0 ) { /** todo: actually, not sure if unselected translates to null or undefined */ const selectedRow = selection [ 0 ] . row ?? null ; if ( selectedRow === null ) { return null ; } const newSelectedRow = selectedRow + offset ; if ( newSelectedRow > - 1 && newSelectedRow < this . chartData . getNumberOfRows ( ) ) { this . chart . setSelection ( [ { row : newSelectedRow } ] ) ; return this . rowIdToStateId . get ( newSelectedRow ) ?? null ; } else { return this . rowIdToStateId . get ( selectedRow ) ?? null ; } } return null ; } private chartSelectEvent : google . visualization . events . ChartEventListener | undefined ; subscribeToChartEvents ( ) { this . chartSelectEvent = google . visualization . events . addListener ( this . chart , 'select' , ( ) => { console . log ( "chart selection changed" ) ; const selection = this . chart . getSelection ( ) ; console . log ( selection ) ; if ( selection && selection . length > 0 ) { const newSelectedStateId = this . rowIdToStateId . get ( selection [ 0 ] . row ) ; this . onStateSelected ( newSelectedStateId ?? null ) ; } else { this . onStateSelected ( null ) ; } } ) ; } unsubscribeChartEvents ( ) { if ( this . chartSelectEvent && this . chart ) { google . visualization . events . removeListener ( this . chartSelectEvent ) ; } } } param value function sanitizeNumber ( value ) { if ( value === undefined || ! Number . isFinite ( value ) ) { return null ; } else { return value ; } } const MAKESPAN_COLUMN = 2 ; const H_COLUMN = 3 ;