github.com/Ilhicas/nomad@v1.0.4-0.20210304152020-e86851182bc3/ui/app/components/das/recommendation-chart.js (about) 1 import Component from '@glimmer/component'; 2 import { action } from '@ember/object'; 3 import { tracked } from '@glimmer/tracking'; 4 import { next } from '@ember/runloop'; 5 import { htmlSafe } from '@ember/string'; 6 import { get } from '@ember/object'; 7 8 import { scaleLinear } from 'd3-scale'; 9 import d3Format from 'd3-format'; 10 11 const statsKeyToLabel = { 12 min: 'Min', 13 median: 'Median', 14 mean: 'Mean', 15 p99: '99th', 16 max: 'Max', 17 current: 'Current', 18 recommended: 'New', 19 }; 20 21 const formatPercent = d3Format.format('+.0%'); 22 export default class RecommendationChartComponent extends Component { 23 @tracked width; 24 @tracked height; 25 26 @tracked shown = false; 27 28 @tracked showLegend = false; 29 @tracked mouseX; 30 @tracked activeLegendRow; 31 32 get isIncrease() { 33 return this.args.currentValue < this.args.recommendedValue; 34 } 35 36 get directionClass() { 37 if (this.args.disabled) { 38 return 'disabled'; 39 } else if (this.isIncrease) { 40 return 'increase'; 41 } else { 42 return 'decrease'; 43 } 44 } 45 46 get icon() { 47 return { 48 x: 0, 49 y: this.resourceLabel.y - this.iconHeight / 2, 50 width: 20, 51 height: this.iconHeight, 52 name: this.isIncrease ? 'arrow-up' : 'arrow-down', 53 }; 54 } 55 56 gutterWidthLeft = 62; 57 gutterWidthRight = 50; 58 gutterWidth = this.gutterWidthLeft + this.gutterWidthRight; 59 60 iconHeight = 21; 61 62 tickTextHeight = 15; 63 64 edgeTickHeight = 23; 65 centerTickOffset = 6; 66 67 centerY = this.tickTextHeight + this.centerTickOffset + this.edgeTickHeight / 2; 68 69 edgeTickY1 = this.tickTextHeight + this.centerTickOffset; 70 edgeTickY2 = this.tickTextHeight + this.edgeTickHeight + this.centerTickOffset; 71 72 deltaTextY = this.edgeTickY2; 73 74 meanHeight = this.edgeTickHeight * 0.6; 75 p99Height = this.edgeTickHeight * 0.48; 76 maxHeight = this.edgeTickHeight * 0.4; 77 78 deltaTriangleHeight = this.edgeTickHeight / 2.5; 79 80 get statsShapes() { 81 if (this.width) { 82 const maxShapes = this.shapesFor('max'); 83 const p99Shapes = this.shapesFor('p99'); 84 const meanShapes = this.shapesFor('mean'); 85 86 const labelProximityThreshold = 25; 87 88 if (p99Shapes.text.x + labelProximityThreshold > maxShapes.text.x) { 89 maxShapes.text.class = 'right'; 90 } 91 92 if (meanShapes.text.x + labelProximityThreshold > p99Shapes.text.x) { 93 p99Shapes.text.class = 'right'; 94 } 95 96 if (meanShapes.text.x + labelProximityThreshold * 2 > maxShapes.text.x) { 97 p99Shapes.text.class = 'hidden'; 98 } 99 100 return [maxShapes, p99Shapes, meanShapes]; 101 } else { 102 return []; 103 } 104 } 105 106 shapesFor(key) { 107 const stat = this.args.stats[key]; 108 109 const rectWidth = this.xScale(stat); 110 const rectHeight = this[`${key}Height`]; 111 112 const tickX = rectWidth + this.gutterWidthLeft; 113 114 const label = statsKeyToLabel[key]; 115 116 return { 117 class: key, 118 text: { 119 label, 120 x: tickX, 121 y: this.tickTextHeight - 5, 122 class: '', // overridden in statsShapes to align/hide based on proximity 123 }, 124 line: { 125 x1: tickX, 126 y1: this.tickTextHeight, 127 x2: tickX, 128 y2: this.centerY - 2, 129 }, 130 rect: { 131 x: this.gutterWidthLeft, 132 y: (this.edgeTickHeight - rectHeight) / 2 + this.centerTickOffset + this.tickTextHeight, 133 width: rectWidth, 134 height: rectHeight, 135 }, 136 }; 137 } 138 139 get barWidth() { 140 return this.width - this.gutterWidth; 141 } 142 143 get higherValue() { 144 return Math.max(this.args.currentValue, this.args.recommendedValue); 145 } 146 147 get maximumX() { 148 return Math.max(this.higherValue, get(this.args.stats, 'max') || Number.MIN_SAFE_INTEGER); 149 } 150 151 get lowerValue() { 152 return Math.min(this.args.currentValue, this.args.recommendedValue); 153 } 154 155 get xScale() { 156 return scaleLinear() 157 .domain([0, this.maximumX]) 158 .rangeRound([0, this.barWidth]); 159 } 160 161 get lowerValueWidth() { 162 return this.gutterWidthLeft + this.xScale(this.lowerValue); 163 } 164 165 get higherValueWidth() { 166 return this.gutterWidthLeft + this.xScale(this.higherValue); 167 } 168 169 get center() { 170 if (this.width) { 171 return { 172 x1: this.gutterWidthLeft, 173 y1: this.centerY, 174 x2: this.width - this.gutterWidthRight, 175 y2: this.centerY, 176 }; 177 } else { 178 return null; 179 } 180 } 181 182 get resourceLabel() { 183 const text = this.args.resource === 'CPU' ? 'CPU' : 'Mem'; 184 185 return { 186 text, 187 x: this.gutterWidthLeft - 10, 188 y: this.centerY, 189 }; 190 } 191 192 get deltaRect() { 193 if (this.isIncrease) { 194 return { 195 x: this.lowerValueWidth, 196 y: this.edgeTickY1, 197 width: this.shown ? this.higherValueWidth - this.lowerValueWidth : 0, 198 height: this.edgeTickHeight, 199 }; 200 } else { 201 return { 202 x: this.shown ? this.lowerValueWidth : this.higherValueWidth, 203 y: this.edgeTickY1, 204 width: this.shown ? this.higherValueWidth - this.lowerValueWidth : 0, 205 height: this.edgeTickHeight, 206 }; 207 } 208 } 209 210 get deltaTriangle() { 211 const directionXMultiplier = this.isIncrease ? 1 : -1; 212 let translateX; 213 214 if (this.shown) { 215 translateX = this.isIncrease ? this.higherValueWidth : this.lowerValueWidth; 216 } else { 217 translateX = this.isIncrease ? this.lowerValueWidth : this.higherValueWidth; 218 } 219 220 return { 221 style: htmlSafe(`transform: translateX(${translateX}px)`), 222 points: ` 223 0,${this.center.y1} 224 0,${this.center.y1 - this.deltaTriangleHeight / 2} 225 ${(directionXMultiplier * this.deltaTriangleHeight) / 2},${this.center.y1} 226 0,${this.center.y1 + this.deltaTriangleHeight / 2} 227 `, 228 }; 229 } 230 231 get deltaLines() { 232 if (this.isIncrease) { 233 return { 234 original: { 235 x: this.lowerValueWidth, 236 }, 237 delta: { 238 style: htmlSafe( 239 `transform: translateX(${this.shown ? this.higherValueWidth : this.lowerValueWidth}px)` 240 ), 241 }, 242 }; 243 } else { 244 return { 245 original: { 246 x: this.higherValueWidth, 247 }, 248 delta: { 249 style: htmlSafe( 250 `transform: translateX(${this.shown ? this.lowerValueWidth : this.higherValueWidth}px)` 251 ), 252 }, 253 }; 254 } 255 } 256 257 get deltaText() { 258 const yOffset = 17; 259 const y = this.deltaTextY + yOffset; 260 261 const lowerValueText = { 262 anchor: 'end', 263 x: this.lowerValueWidth, 264 y, 265 }; 266 267 const higherValueText = { 268 anchor: 'start', 269 x: this.higherValueWidth, 270 y, 271 }; 272 273 const percentText = formatPercent( 274 (this.args.recommendedValue - this.args.currentValue) / this.args.currentValue 275 ); 276 277 const percent = { 278 x: (lowerValueText.x + higherValueText.x) / 2, 279 y, 280 text: percentText, 281 }; 282 283 if (this.isIncrease) { 284 return { 285 original: lowerValueText, 286 delta: higherValueText, 287 percent, 288 }; 289 } else { 290 return { 291 original: higherValueText, 292 delta: lowerValueText, 293 percent, 294 }; 295 } 296 } 297 298 get chartHeight() { 299 return this.deltaText.original.y + 1; 300 } 301 302 get tooltipStyle() { 303 if (this.showLegend) { 304 return htmlSafe(`left: ${this.mouseX}px`); 305 } 306 307 return undefined; 308 } 309 310 get sortedStats() { 311 if (this.args.stats) { 312 const statsWithCurrentAndRecommended = { 313 ...this.args.stats, 314 current: this.args.currentValue, 315 recommended: this.args.recommendedValue, 316 }; 317 318 return Object.keys(statsWithCurrentAndRecommended) 319 .map(key => ({ label: statsKeyToLabel[key], value: statsWithCurrentAndRecommended[key] })) 320 .sortBy('value'); 321 } else { 322 return []; 323 } 324 } 325 326 @action 327 isShown() { 328 next(() => { 329 this.shown = true; 330 }); 331 } 332 333 @action 334 onResize() { 335 this.width = this.svgElement.clientWidth; 336 this.height = this.svgElement.clientHeight; 337 } 338 339 @action 340 storeSvgElement(element) { 341 this.svgElement = element; 342 } 343 344 @action 345 setLegendPosition(mouseMoveEvent) { 346 this.showLegend = true; 347 this.mouseX = mouseMoveEvent.layerX; 348 } 349 350 @action 351 setActiveLegendRow(row) { 352 this.activeLegendRow = row; 353 } 354 355 @action 356 unsetActiveLegendRow() { 357 this.activeLegendRow = undefined; 358 } 359 }