github.com/hernad/nomad@v1.6.112/ui/app/components/distribution-bar.js (about)

     1  /**
     2   * Copyright (c) HashiCorp, Inc.
     3   * SPDX-License-Identifier: MPL-2.0
     4   */
     5  
     6  /* eslint-disable ember/no-observers */
     7  import Component from '@ember/component';
     8  import { computed, set } from '@ember/object';
     9  import { observes } from '@ember-decorators/object';
    10  import { run, once } from '@ember/runloop';
    11  import { assign } from '@ember/polyfills';
    12  import { guidFor } from '@ember/object/internals';
    13  import { copy } from 'ember-copy';
    14  import { computed as overridable } from 'ember-overridable-computed';
    15  import d3 from 'd3-selection';
    16  import 'd3-transition';
    17  import WindowResizable from '../mixins/window-resizable';
    18  import styleStringProperty from '../utils/properties/style-string';
    19  import { classNames, classNameBindings } from '@ember-decorators/component';
    20  import classic from 'ember-classic-decorator';
    21  
    22  const sumAggregate = (total, val) => total + val;
    23  
    24  @classic
    25  @classNames('chart', 'distribution-bar')
    26  @classNameBindings('isNarrow:is-narrow')
    27  export default class DistributionBar extends Component.extend(WindowResizable) {
    28    chart = null;
    29    @overridable(() => null) data;
    30    onSliceClick = null;
    31    activeDatum = null;
    32    isNarrow = false;
    33  
    34    @styleStringProperty('tooltipPosition') tooltipStyle;
    35    maskId = null;
    36  
    37    @computed('data')
    38    get _data() {
    39      const data = copy(this.data, true);
    40      const sum = data.mapBy('value').reduce(sumAggregate, 0);
    41  
    42      return data.map(
    43        ({ label, value, className, layers, legendLink, help }, index) => ({
    44          label,
    45          value,
    46          className,
    47          layers,
    48          legendLink,
    49          help,
    50          index,
    51          percent: value / sum,
    52          offset:
    53            data.slice(0, index).mapBy('value').reduce(sumAggregate, 0) / sum,
    54        })
    55      );
    56    }
    57  
    58    didInsertElement() {
    59      super.didInsertElement(...arguments);
    60      const svg = this.element.querySelector('svg');
    61      const chart = d3.select(svg);
    62      const maskId = `dist-mask-${guidFor(this)}`;
    63      this.setProperties({ chart, maskId });
    64  
    65      svg.querySelector('clipPath').setAttribute('id', maskId);
    66  
    67      chart.on('mouseleave', () => {
    68        run(() => {
    69          this.set('isActive', false);
    70          this.set('activeDatum', null);
    71          chart
    72            .selectAll('g')
    73            .classed('active', false)
    74            .classed('inactive', false);
    75        });
    76      });
    77  
    78      this.renderChart();
    79    }
    80  
    81    didUpdateAttrs() {
    82      super.didUpdateAttrs();
    83      this.renderChart();
    84    }
    85  
    86    @observes('_data.@each.{value,label,className}')
    87    updateChart() {
    88      this.renderChart();
    89    }
    90  
    91    // prettier-ignore
    92    /* eslint-disable */
    93    renderChart() {
    94      const { chart, _data, isNarrow } = this;
    95      const width = this.element.querySelector('svg').clientWidth;
    96      const filteredData = _data.filter(d => d.value > 0);
    97      filteredData.forEach((d, index) => {
    98        set(d, 'index', index);
    99      });
   100  
   101      let slices = chart.select('.bars').selectAll('g').data(filteredData, d => d.label);
   102      let sliceCount = filteredData.length;
   103  
   104      slices.exit().remove();
   105  
   106      let slicesEnter = slices.enter()
   107        .append('g')
   108        .on('mouseenter', (ev, d) => {
   109          run(() => {
   110            const slices = this.slices;
   111            const slice = slices.filter(datum => datum.label === d.label);
   112            slices.classed('active', false).classed('inactive', true);
   113            slice.classed('active', true).classed('inactive', false);
   114            this.set('activeDatum', d);
   115  
   116            const box = slice.node().getBBox();
   117            const pos = box.x + box.width / 2;
   118  
   119            // Ensure that the position is set before the tooltip is visible
   120            run.schedule('afterRender', this, () => this.set('isActive', true));
   121            this.set('tooltipPosition', {
   122              left: pos,
   123            });
   124          });
   125        });
   126  
   127      slices = slices.merge(slicesEnter);
   128      slices.attr('class', d => {
   129        const className = d.className || `slice-${_data.indexOf(d)}`
   130        const activeDatum = this.activeDatum;
   131        const isActive = activeDatum && activeDatum.label === d.label;
   132        const isInactive = activeDatum && activeDatum.label !== d.label;
   133        const isClickable = !!this.onSliceClick;
   134        return [
   135          className,
   136          isActive && 'active',
   137          isInactive && 'inactive',
   138          isClickable && 'clickable'
   139        ].compact().join(' ');
   140      }).attr('data-test-slice-label', d => d.className);
   141  
   142      this.set('slices', slices);
   143  
   144      const setWidth = d => {
   145        // Remove a pixel from either side of the slice
   146        let modifier = 2;
   147        if (d.index === 0) modifier--; // But not the left side
   148        if (d.index === sliceCount - 1) modifier--; // But not the right side
   149  
   150        return `${width * d.percent - modifier}px`;
   151      };
   152      const setOffset = d => `${width * d.offset + (d.index === 0 ? 0 : 1)}px`;
   153  
   154      let hoverTargets = slices.selectAll('.target').data(d => [d]);
   155      hoverTargets.enter()
   156          .append('rect')
   157          .attr('class', 'target')
   158          .attr('width', setWidth)
   159          .attr('height', '100%')
   160          .attr('x', setOffset)
   161        .merge(hoverTargets)
   162        .transition()
   163          .duration(200)
   164          .attr('width', setWidth)
   165          .attr('x', setOffset)
   166  
   167      let layers = slices.selectAll('.bar').data((d, i) => {
   168        return new Array(d.layers || 1).fill(assign({ index: i }, d));
   169      });
   170      layers.enter()
   171          .append('rect')
   172          .attr('width', setWidth)
   173          .attr('x', setOffset)
   174          .attr('y', () => isNarrow ? '50%' : 0)
   175          .attr('clip-path', `url(#${this.maskId})`)
   176          .attr('height', () => isNarrow ? '6px' : '100%')
   177          .attr('transform', () => isNarrow ? 'translate(0, -3)' : '')
   178        .merge(layers)
   179          .attr('class', (d, i) => `bar layer-${i}`)
   180        .transition()
   181          .duration(200)
   182          .attr('width', setWidth)
   183          .attr('x', setOffset)
   184  
   185        if (isNarrow) {
   186          d3.select(this.element).select('.mask')
   187            .attr('height', '6px')
   188            .attr('y', '50%');
   189        }
   190  
   191      if (this.onSliceClick) {
   192        slices.on('click', this.onSliceClick);
   193      }
   194    }
   195    /* eslint-enable */
   196  
   197    windowResizeHandler() {
   198      once(this, this.renderChart);
   199    }
   200  }