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