github.com/Ilhicas/nomad@v1.0.4-0.20210304152020-e86851182bc3/ui/app/components/distribution-bar.js (about)

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