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