import * as Chart from "chart.js" ; import { ChartPoint , LinearTickOptions } from "chart.js" ; import * as _ from "lodash" ; import { Helper } from "../../../helper" ; import { AppResourcesModel } from "../../../models/app-resources.model" ; import { PowerBestSplitModel , SpeedUnitDataModel , ZoneModel } from "@elevate/shared/models" ; type GraphTypes = "histogram" | "scatter-line" ; export abstract class AbstractDataView { protected units : string ; protected chart : Chart ; protected canvasId : string ; protected viewTitle : string ; protected content : string ; protected grid : JQuery ; protected hasGraph : boolean ; protected graph : JQuery ; protected graphData : Chart . ChartData ; protected graphTitle : string ; protected mainColor : number [ ] ; protected table : JQuery ; protected appResources : AppResourcesModel ; protected isOwner : boolean ; protected isSegmentEffortView : boolean ; protected activityType : string ; protected speedUnitsData : SpeedUnitDataModel ; private readonly graphType : GraphTypes ; private readonly logXAxis : boolean ; protected constructor ( units ? , graphType ? , logXAxis ? ) { this . content = "" ; this . viewTitle = "" ; this . units = units ; this . hasGraph = true ; this . mainColor = [ 0 , 0 , 0 ] ; this . graphType = graphType || "histogram" ; this . logXAxis = logXAxis ; this . canvasId = Helper . guid ( ) ; } public abstract render ( ) ; protected abstract insertDataIntoGrid ( ) ; public getContent ( ) { return this . content ; } public printNumber ( value , decimals ? ) { return ( _ . isNumber ( value ) && ! _ . isNaN ( value ) && _ . isFinite ( value ) ) ? value . toFixed ( ( decimals ) ? decimals : 0 ) : "-" ; } public setIsSegmentEffortView ( bool ) { this . isSegmentEffortView = bool ; } public setIsAuthorOfViewedActivity ( bool ) { this . isOwner = bool ; } public setGraphTitleFromUnits ( ) { this . graphTitle = ( ( "" + this . units ) . toUpperCase ( ) + " distribution in minutes" ) ; } public setActivityType ( type ) { this . activityType = type ; } public setAppResources ( appResources ) { this . appResources = appResources ; } protected generateSectionTitle ( title ) { return "<h2 style='background-color: rgb(" + this . mainColor [ 0 ] + ", " + this . mainColor [ 1 ] + ", " + this . mainColor [ 2 ] + "); color: white; margin-top: 10px; padding-bottom: 20px; padding-top: 20px;'><span style='padding-left: 10px;'>" + title + "</span></h2>" ; } protected generateCanvasForGraph ( ) { if ( ! this . units ) { console . error ( "View must have unit" ) ; return ; } const graphWidth = window . innerWidth * 0.45 ; let htmlCanvas = "" ; htmlCanvas += "<div>" ; htmlCanvas += "<div>" ; htmlCanvas += "<canvas id=\"" + this . canvasId + "\" height=\"450\" width=\"" + graphWidth + "\"></canvas>" ; htmlCanvas += "</div>" ; this . graph = $ ( htmlCanvas ) ; } protected setupDistributionGraph ( zones , ratio ? ) { if ( ! ratio ) { ratio = 1 ; } const labelsData = [ ] ; let zone ; for ( zone in zones ) { const label = "Z" + ( parseInt ( zone ) + 1 ) + " " + ( zones [ zone ] . from * ratio ) . toFixed ( 1 ) . replace ( ".0" , "" ) + " to " + ( zones [ zone ] . to * ratio ) . toFixed ( 1 ) . replace ( ".0" , "" ) + " " + this . units ; labelsData . push ( label ) ; } const distributionArray = zones . map ( z => Number ( ( z . s / 60 ) . toFixed ( 2 ) ) ) ; this . graphData = { labels : labelsData , datasets : [ { label : this . graphTitle , backgroundColor : "rgba(" + this . mainColor [ 0 ] + ", " + this . mainColor [ 1 ] + ", " + this . mainColor [ 2 ] + ", 0.5)" , borderColor : "rgba(" + this . mainColor [ 0 ] + ", " + this . mainColor [ 1 ] + ", " + this . mainColor [ 2 ] + ", 1)" , borderWidth : 1 , hoverBackgroundColor : "rgba(" + this . mainColor [ 0 ] + ", " + this . mainColor [ 1 ] + ", " + this . mainColor [ 2 ] + ", 0.8)" , hoverBorderColor : "rgba(" + this . mainColor [ 0 ] + ", " + this . mainColor [ 1 ] + ", " + this . mainColor [ 2 ] + ", 1)" , data : distributionArray , } ] , } ; } protected setupScatterLineGraph ( chartPoints ) { this . graphData = { datasets : [ { label : this . graphTitle , backgroundColor : "rgba(" + this . mainColor [ 0 ] + ", " + this . mainColor [ 1 ] + ", " + this . mainColor [ 2 ] + ", 0.5)" , borderColor : "rgba(" + this . mainColor [ 0 ] + ", " + this . mainColor [ 1 ] + ", " + this . mainColor [ 2 ] + ", 1)" , borderWidth : 1 , pointRadius : 0 , hoverBackgroundColor : "rgba(" + this . mainColor [ 0 ] + ", " + this . mainColor [ 1 ] + ", " + this . mainColor [ 2 ] + ", 0.8)" , hoverBorderColor : "rgba(" + this . mainColor [ 0 ] + ", " + this . mainColor [ 1 ] + ", " + this . mainColor [ 2 ] + ", 1)" , data : chartPoints , fill : false , showLine : true } ] , } ; } /**
     * Push grid, graph and table to content view
     */ protected injectToContent ( ) { this . content += "<div class='content'>" ; this . content += this . grid . html ( ) ; this . content += this . graph . html ( ) ; this . content += this . table . html ( ) ; this . content += "</div>" ; } public displayGraph ( ) { if ( ! this . canvasId ) { console . error ( "View Id must exist in " + typeof this ) ; return ; } if ( ! this . hasGraph ) { return ; } const canvas = document . getElementById ( this . canvasId ) as HTMLCanvasElement ; this . chart = this . graphType === "histogram" ? this . generateHistogram ( canvas ) : this . generateScatterLinePlot ( canvas ) ; } private generateHistogram ( canvas ) { return new Chart ( canvas . getContext ( "2d" ) , { type : "bar" , data : this . graphData , options : { tooltips : { custom : this . customTooltipsForZones , } , scales : { yAxes : [ { ticks : { beginAtZero : true , } , } as LinearTickOptions ] , } , } , } ) ; } private generateScatterLinePlot ( canvas ) { const maxXData = _ . max ( this . graphData . datasets . map ( d => _ . max ( ( d . data as Chart . ChartPoint [ ] ) . map ( p => p . x ) ) ) ) || 1 ; return new Chart ( canvas . getContext ( "2d" ) , { type : "scatter" , data : this . graphData , options : { hover : { intersect : false , mode : "nearest" } , tooltips : { intersect : false , mode : "nearest" , callbacks : { label : ( item ) => Number ( item . yLabel ) . toFixed ( 1 ) + this . units + " held during " + Helper . secondsToHHMMSS ( Number ( item . xLabel ) , true ) } } , scales : { xAxes : [ { type : this . logXAxis ? "logarithmic" : "linear" , ticks : { min : 0 , max : maxXData , callback : ( tick ) => { const remain = tick / ( Math . pow ( 10 , Math . floor ( Math . log10 ( tick ) ) ) ) ; if ( remain === 1 || remain === 2 || remain === 5 ) { return Helper . secondsToHHMMSS ( tick , true ) ; } return "" ; } } , } ] , } , } } ) ; } protected customTooltipsForZones ( tooltip ) { if ( ! tooltip || ! tooltip . body || ! tooltip . body [ 0 ] || ! tooltip . body [ 0 ] . lines || ! tooltip . body [ 0 ] . lines [ 0 ] ) { return ; } const lineValue = tooltip . body [ 0 ] . lines [ 0 ] ; const timeInMinutes = _ . first ( lineValue . match ( /[+-]?\d+(\.\d+)?/g ) . map ( ( value ) => { return parseFloat ( value ) ; } ) ) ; tooltip . body [ 0 ] . lines [ 0 ] = "Zone held during " + Helper . secondsToHHMMSS ( parseFloat ( timeInMinutes ) * 60 ) ; } protected setupPointDataTable ( pointDataModel ) { if ( ! this . units ) { console . error ( "View must have units." ) ; return ; } let htmlTable = "" ; htmlTable += "<div>" ; htmlTable += "<div style=\"height:500px; overflow:auto;\">" ; htmlTable += "<table class=\"distributionTable\">" ; htmlTable += "<tr>" ; htmlTable += "<td>TIME</td>" ; htmlTable += "<td>" + this . units . toUpperCase ( ) + "</td>" ; htmlTable += "</tr>" ; htmlTable += pointDataModel . map ( p => { return "<tr>" + "<td>" + Helper . secondsToHHMMSS ( p . time ) + "</td>" + "<td>" + p . watts . toFixed ( 1 ) + "</td>" + "</tr>" ; } ) . join ( "" ) ; htmlTable += "</table>" ; htmlTable += "</div>" ; htmlTable += "</div>" ; this . table = $ ( htmlTable ) ; } protected setupDistributionTable ( zones , ratio ? ) { if ( ! ratio ) { ratio = 1 ; } if ( ! this . units ) { console . error ( "View must have units." ) ; return ; } let htmlTable = "" ; htmlTable += "<div>" ; htmlTable += "<div style=\"height:500px; overflow:auto;\">" ; htmlTable += "<table class=\"distributionTable\">" ; htmlTable += "<tr>" ; htmlTable += "<td>ZONE</td>" ; htmlTable += "<td>FROM " + this . units . toUpperCase ( ) + "</td>" ; htmlTable += "<td>TO " + this . units . toUpperCase ( ) + "</td>" ; htmlTable += "<td>TIME</td>" ; htmlTable += "<td>% ZONE</td>" ; htmlTable += "</tr>" ; let zoneId = 1 ; let zone ; for ( zone in zones ) { htmlTable += "<tr>" ; htmlTable += "<td>Z" + zoneId + "</td>" ; htmlTable += "<td>" + ( zones [ zone ] . from * ratio ) . toFixed ( 1 ) + "</th>" ; htmlTable += "<td>" + ( zones [ zone ] . to * ratio ) . toFixed ( 1 ) + "</th>" ; htmlTable += "<td>" + Helper . secondsToHHMMSS ( zones [ zone ] . s ) + "</td>" ; htmlTable += "<td>" + zones [ zone ] . percentDistrib . toFixed ( 1 ) + "%</td>" ; htmlTable += "</tr>" ; zoneId ++ ; } htmlTable += "</table>" ; htmlTable += "</div>" ; htmlTable += "</div>" ; this . table = $ ( htmlTable ) ; } protected makeGrid ( columns , rows ) { let grid = "" ; grid += "<div>" ; grid += "<div class=\"grid\">" ; grid += "<table>" ; for ( let i = 0 ; i < rows ; i ++ ) { grid += "<tr>" ; for ( let j = 0 ; j < columns ; j ++ ) { grid += "<td data-column=\"" + j + "\" data-row=\"" + i + "\">" ; grid += "</td>" ; } grid += "</tr>" ; } grid += "</table>" ; grid += "</div>" ; grid += "</div>" ; this . grid = $ ( grid ) ; } protected insertContentAtGridPosition ( columnId , rowId , data , title , units , userSettingKey ) { const onClickHtmlBehaviour = "onclick='javascript:window.open(\"" + this . appResources . settingsLink + "#/globalSettings?viewOptionHelperId=" + userSettingKey + "\",\"_blank\");'" ; if ( this . grid ) { const content = "<span class=\"gridDataContainer\" " + onClickHtmlBehaviour + ">" + data + " <span class=\"gridUnits\">" + units + "</span><br /><span class=\"gridTitle\">" + title + "</span></span>" ; this . grid . find ( "[data-column=" + columnId + "][data-row=" + rowId + "]" ) . html ( content ) ; } else { console . error ( "Grid is not initialized" ) ; } } }