1/* 2 * Copyright 2024 Google LLC 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17import { checkArgument, checkNotNull } from '../util/preconditions'; 18import { DataPoint, isNotFound } from './golden'; 19import { VisualTimeline } from './visual-timeline'; 20 21export interface Visualization { 22 height: number; 23 render( 24 timeline: VisualTimeline, 25 dataPoints: Array<DataPoint>, 26 canvas: HTMLCanvasElement 27 ): void; 28} 29 30export const PROBE_COLORS = [ 31 '#E51C23', 32 '#9C27B0', 33 '#5677FC', 34 '#00BCD4', 35 '#259B24', 36 '#CDDC39', 37 '#FFC107', 38 '#795548', 39 '#737373', 40]; 41 42function lighten(hex: string): string { 43 hex = hex.replace(/[^0-9A-F]/gi, ''); 44 var bigint = parseInt(hex, 16); 45 var r = (bigint >> 16) & 255; 46 var g = (bigint >> 8) & 255; 47 var b = bigint & 255; 48 49 return 'rgba(' + r + ',' + g + ',' + b + ',0.15)'; 50} 51 52export class LineGraphVisualization implements Visualization { 53 color: string; 54 fillColor: string; 55 56 height: number = 96; 57 58 constructor( 59 public minValue: number, 60 public maxValue: number, 61 color: string | null 62 ) { 63 checkArgument( 64 minValue < maxValue, 65 `minValue ${minValue} >= maxValue ${maxValue}` 66 ); 67 68 this.color = color ?? PROBE_COLORS[0]; 69 this.fillColor = lighten(this.color); 70 } 71 72 render( 73 timeline: VisualTimeline, 74 dataPoints: Array<DataPoint>, 75 canvas: HTMLCanvasElement 76 ) { 77 const cw = canvas.width; 78 const ch = 90; 79 80 const ctx = checkNotNull(canvas.getContext('2d')); 81 ctx.clearRect(0, 0, cw, canvas.height); 82 ctx.save(); 83 ctx.translate(0, 5); 84 85 const min = this.minValue; 86 const max = this.maxValue; 87 const color = this.color; 88 89 for (let i = 0; i < dataPoints.length; i++) { 90 if (isNotFound(dataPoints[i])) { 91 continue; 92 } 93 const start = i; 94 95 while (i < dataPoints.length && !isNotFound(dataPoints[i])) { 96 i++; 97 } 98 const end = i; 99 100 const bottomLineWidth = 1; 101 102 const mid = min <= 0 && max >= 0 ? 0 : min > 0 ? min : max; 103 104 const midY = (1 - (mid - min) / (max - min)) * ch; 105 106 ctx.save(); 107 try { 108 ctx.fillStyle = this.fillColor; 109 ctx.beginPath(); 110 ctx.moveTo(timeline.frameToPx(start), midY); 111 for (let i = start; i < end; i++) { 112 const value = dataPoints.at(i); 113 if (typeof value !== 'number') { 114 continue; 115 } 116 ctx.lineTo( 117 timeline.frameToPx(i), 118 (1 - (value - min) / (max - min)) * ch 119 ); 120 } 121 ctx.lineTo(timeline.frameToPx(end - 1), midY); 122 ctx.fill(); 123 } finally { 124 ctx.restore(); 125 } 126 ctx.save(); 127 try { 128 ctx.beginPath(); 129 ctx.strokeStyle = color; 130 ctx.lineWidth = bottomLineWidth; 131 132 ctx.moveTo(timeline.frameToPx(start), midY); 133 ctx.lineTo(timeline.frameToPx(end - 1), midY); 134 135 ctx.stroke(); 136 } finally { 137 ctx.restore(); 138 } 139 140 ctx.fillStyle = color; 141 142 for (let i = start; i < end; i++) { 143 const value = dataPoints.at(i); 144 if (typeof value !== 'number') { 145 continue; 146 } 147 ctx.beginPath(); 148 149 ctx.arc( 150 timeline.frameToPx(i), 151 (1 - (value - min) / (max - min)) * ch, 152 2, 153 0, 154 2 * Math.PI 155 ); 156 157 ctx.fill(); 158 } 159 160 ctx.restore(); 161 } 162 } 163} 164 165export class DataPointVisualization implements Visualization { 166 color: string = PROBE_COLORS[0]; 167 height: number = 32; 168 169 render( 170 timeline: VisualTimeline, 171 dataPoints: Array<DataPoint>, 172 canvas: HTMLCanvasElement 173 ) { 174 const cw = canvas.width; 175 176 const ctx = checkNotNull(canvas.getContext('2d')); 177 ctx.clearRect(0, 0, cw, canvas.height); 178 179 ctx.fillStyle = this.color; 180 181 dataPoints.forEach((dataPoint, idx) => { 182 if (isNotFound(dataPoint)) return; 183 ctx.beginPath(); 184 185 ctx.arc( 186 /* x */ timeline.frameToPx(idx), 187 /* y */ 15, 188 /* radius */ 5, 189 /* startAngle */ 0, 190 /* endAngle */ 2 * Math.PI 191 ); 192 ctx.fill(); 193 }); 194 } 195} 196