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