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 }