github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/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 =
    68      this.tickTextHeight + this.centerTickOffset + this.edgeTickHeight / 2;
    69  
    70    edgeTickY1 = this.tickTextHeight + this.centerTickOffset;
    71    edgeTickY2 =
    72      this.tickTextHeight + this.edgeTickHeight + this.centerTickOffset;
    73  
    74    deltaTextY = this.edgeTickY2;
    75  
    76    meanHeight = this.edgeTickHeight * 0.6;
    77    p99Height = this.edgeTickHeight * 0.48;
    78    maxHeight = this.edgeTickHeight * 0.4;
    79  
    80    deltaTriangleHeight = this.edgeTickHeight / 2.5;
    81  
    82    get statsShapes() {
    83      if (this.width) {
    84        const maxShapes = this.shapesFor('max');
    85        const p99Shapes = this.shapesFor('p99');
    86        const meanShapes = this.shapesFor('mean');
    87  
    88        const labelProximityThreshold = 25;
    89  
    90        if (p99Shapes.text.x + labelProximityThreshold > maxShapes.text.x) {
    91          maxShapes.text.class = 'right';
    92        }
    93  
    94        if (meanShapes.text.x + labelProximityThreshold > p99Shapes.text.x) {
    95          p99Shapes.text.class = 'right';
    96        }
    97  
    98        if (meanShapes.text.x + labelProximityThreshold * 2 > maxShapes.text.x) {
    99          p99Shapes.text.class = 'hidden';
   100        }
   101  
   102        return [maxShapes, p99Shapes, meanShapes];
   103      } else {
   104        return [];
   105      }
   106    }
   107  
   108    shapesFor(key) {
   109      const stat = this.args.stats[key];
   110  
   111      const rectWidth = this.xScale(stat);
   112      const rectHeight = this[`${key}Height`];
   113  
   114      const tickX = rectWidth + this.gutterWidthLeft;
   115  
   116      const label = statsKeyToLabel[key];
   117  
   118      return {
   119        class: key,
   120        text: {
   121          label,
   122          x: tickX,
   123          y: this.tickTextHeight - 5,
   124          class: '', // overridden in statsShapes to align/hide based on proximity
   125        },
   126        line: {
   127          x1: tickX,
   128          y1: this.tickTextHeight,
   129          x2: tickX,
   130          y2: this.centerY - 2,
   131        },
   132        rect: {
   133          x: this.gutterWidthLeft,
   134          y:
   135            (this.edgeTickHeight - rectHeight) / 2 +
   136            this.centerTickOffset +
   137            this.tickTextHeight,
   138          width: rectWidth,
   139          height: rectHeight,
   140        },
   141      };
   142    }
   143  
   144    get barWidth() {
   145      return this.width - this.gutterWidth;
   146    }
   147  
   148    get higherValue() {
   149      return Math.max(this.args.currentValue, this.args.recommendedValue);
   150    }
   151  
   152    get maximumX() {
   153      return Math.max(
   154        this.higherValue,
   155        get(this.args.stats, 'max') || Number.MIN_SAFE_INTEGER
   156      );
   157    }
   158  
   159    get lowerValue() {
   160      return Math.min(this.args.currentValue, this.args.recommendedValue);
   161    }
   162  
   163    get xScale() {
   164      return scaleLinear()
   165        .domain([0, this.maximumX])
   166        .rangeRound([0, this.barWidth]);
   167    }
   168  
   169    get lowerValueWidth() {
   170      return this.gutterWidthLeft + this.xScale(this.lowerValue);
   171    }
   172  
   173    get higherValueWidth() {
   174      return this.gutterWidthLeft + this.xScale(this.higherValue);
   175    }
   176  
   177    get center() {
   178      if (this.width) {
   179        return {
   180          x1: this.gutterWidthLeft,
   181          y1: this.centerY,
   182          x2: this.width - this.gutterWidthRight,
   183          y2: this.centerY,
   184        };
   185      } else {
   186        return null;
   187      }
   188    }
   189  
   190    get resourceLabel() {
   191      const text = this.args.resource === 'CPU' ? 'CPU' : 'Mem';
   192  
   193      return {
   194        text,
   195        x: this.gutterWidthLeft - 10,
   196        y: this.centerY,
   197      };
   198    }
   199  
   200    get deltaRect() {
   201      if (this.isIncrease) {
   202        return {
   203          x: this.lowerValueWidth,
   204          y: this.edgeTickY1,
   205          width: this.shown ? this.higherValueWidth - this.lowerValueWidth : 0,
   206          height: this.edgeTickHeight,
   207        };
   208      } else {
   209        return {
   210          x: this.shown ? this.lowerValueWidth : this.higherValueWidth,
   211          y: this.edgeTickY1,
   212          width: this.shown ? this.higherValueWidth - this.lowerValueWidth : 0,
   213          height: this.edgeTickHeight,
   214        };
   215      }
   216    }
   217  
   218    get deltaTriangle() {
   219      const directionXMultiplier = this.isIncrease ? 1 : -1;
   220      let translateX;
   221  
   222      if (this.shown) {
   223        translateX = this.isIncrease
   224          ? this.higherValueWidth
   225          : this.lowerValueWidth;
   226      } else {
   227        translateX = this.isIncrease
   228          ? this.lowerValueWidth
   229          : this.higherValueWidth;
   230      }
   231  
   232      return {
   233        style: htmlSafe(`transform: translateX(${translateX}px)`),
   234        points: `
   235          0,${this.center.y1}
   236          0,${this.center.y1 - this.deltaTriangleHeight / 2}
   237          ${(directionXMultiplier * this.deltaTriangleHeight) / 2},${
   238          this.center.y1
   239        }
   240          0,${this.center.y1 + this.deltaTriangleHeight / 2}
   241        `,
   242      };
   243    }
   244  
   245    get deltaLines() {
   246      if (this.isIncrease) {
   247        return {
   248          original: {
   249            x: this.lowerValueWidth,
   250          },
   251          delta: {
   252            style: htmlSafe(
   253              `transform: translateX(${
   254                this.shown ? this.higherValueWidth : this.lowerValueWidth
   255              }px)`
   256            ),
   257          },
   258        };
   259      } else {
   260        return {
   261          original: {
   262            x: this.higherValueWidth,
   263          },
   264          delta: {
   265            style: htmlSafe(
   266              `transform: translateX(${
   267                this.shown ? this.lowerValueWidth : this.higherValueWidth
   268              }px)`
   269            ),
   270          },
   271        };
   272      }
   273    }
   274  
   275    get deltaText() {
   276      const yOffset = 17;
   277      const y = this.deltaTextY + yOffset;
   278  
   279      const lowerValueText = {
   280        anchor: 'end',
   281        x: this.lowerValueWidth,
   282        y,
   283      };
   284  
   285      const higherValueText = {
   286        anchor: 'start',
   287        x: this.higherValueWidth,
   288        y,
   289      };
   290  
   291      const percentText = formatPercent(
   292        (this.args.recommendedValue - this.args.currentValue) /
   293          this.args.currentValue
   294      );
   295  
   296      const percent = {
   297        x: (lowerValueText.x + higherValueText.x) / 2,
   298        y,
   299        text: percentText,
   300      };
   301  
   302      if (this.isIncrease) {
   303        return {
   304          original: lowerValueText,
   305          delta: higherValueText,
   306          percent,
   307        };
   308      } else {
   309        return {
   310          original: higherValueText,
   311          delta: lowerValueText,
   312          percent,
   313        };
   314      }
   315    }
   316  
   317    get chartHeight() {
   318      return this.deltaText.original.y + 1;
   319    }
   320  
   321    get tooltipStyle() {
   322      if (this.showLegend) {
   323        return htmlSafe(`left: ${this.mouseX}px`);
   324      }
   325  
   326      return undefined;
   327    }
   328  
   329    get sortedStats() {
   330      if (this.args.stats) {
   331        const statsWithCurrentAndRecommended = {
   332          ...this.args.stats,
   333          current: this.args.currentValue,
   334          recommended: this.args.recommendedValue,
   335        };
   336  
   337        return Object.keys(statsWithCurrentAndRecommended)
   338          .map((key) => ({
   339            label: statsKeyToLabel[key],
   340            value: statsWithCurrentAndRecommended[key],
   341          }))
   342          .sortBy('value');
   343      } else {
   344        return [];
   345      }
   346    }
   347  
   348    @action
   349    isShown() {
   350      next(() => {
   351        this.shown = true;
   352      });
   353    }
   354  
   355    @action
   356    onResize() {
   357      this.width = this.svgElement.clientWidth;
   358      this.height = this.svgElement.clientHeight;
   359    }
   360  
   361    @action
   362    storeSvgElement(element) {
   363      this.svgElement = element;
   364    }
   365  
   366    @action
   367    setLegendPosition(mouseMoveEvent) {
   368      this.showLegend = true;
   369      this.mouseX = mouseMoveEvent.layerX;
   370    }
   371  
   372    @action
   373    setActiveLegendRow(row) {
   374      this.activeLegendRow = row;
   375    }
   376  
   377    @action
   378    unsetActiveLegendRow() {
   379      this.activeLegendRow = undefined;
   380    }
   381  }