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  }