UNPKG

veritatisquam

Version:
286 lines (249 loc) 7.4 kB
// Internal Imports import { Tools } from '../../tools'; import { Bar } from './bar'; import { Roles, Events, CartesianOrientations, ColorClassNameTypes, } from '../../interfaces'; // D3 Imports import { select } from 'd3-selection'; export class StackedBar extends Bar { type = 'stacked-bar'; init() { const eventsFragment = this.services.events; // Highlight correct circle on legend item hovers eventsFragment.addEventListener( Events.Legend.ITEM_HOVER, this.handleLegendOnHover ); // Un-highlight circles on legend item mouseouts eventsFragment.addEventListener( Events.Legend.ITEM_MOUSEOUT, this.handleLegendMouseOut ); } render(animate: boolean) { // Grab container SVG const svg = this.getContainerSVG({ withinChartClip: true }); // Chart options mixed with the internal configurations const options = this.getOptions(); const { groupMapsTo } = options.data; // Create the data and keys that'll be used by the stack layout const stackData = this.model.getStackedData({ groups: this.configs.groups, }); // Update data on all bar groups const barGroups = svg .selectAll('g.bars') .data(stackData, (d) => (d.length > 0 ? d[0][groupMapsTo] : null)); // Remove elements that need to be exited // We need exit at the top here to make sure that // Data filters are processed before entering new elements // Or updating existing ones barGroups.exit().attr('opacity', 0).remove(); // Add bar groups that need to be introduced barGroups .enter() .append('g') .classed('bars', true) .attr('role', Roles.GROUP) .attr('data-name', 'bars'); // Update data on all bars const bars = svg .selectAll('g.bars') .selectAll('path.bar') .data( (d) => d, (d) => d.data.sharedStackKey ); // Remove bars that need to be removed bars.exit().remove(); bars.enter() .append('path') .merge(bars) .classed('bar', true) .transition( this.services.transitions.getTransition( 'bar-update-enter', animate ) ) .attr('class', (d) => this.model.getColorClassName({ classNameTypes: [ColorClassNameTypes.FILL], dataGroupName: d[groupMapsTo], originalClassName: 'bar', }) ) .style('fill', (d) => this.model.getFillColor(d[groupMapsTo])) .attr('d', (d, i) => { const key = d.data.sharedStackKey; /* * Orientation support for horizontal/vertical bar charts * Determine coordinates needed for a vertical set of paths * to draw the bars needed, and pass those coordinates down to * generateSVGPathString() to decide whether it needs to flip them */ const barWidth = this.getBarWidth(); const x0 = this.services.cartesianScales.getDomainValue(key, i) - barWidth / 2; const x1 = x0 + barWidth; const y0 = this.services.cartesianScales.getRangeValue(d[0], i); let y1 = this.services.cartesianScales.getRangeValue(d[1], i); // don't show if part of bar is out of zoom domain if (this.isOutsideZoomedDomain(x0, x1)) { return; } // Add the divider gap if ( Math.abs(y1 - y0) > 0 && Math.abs(y1 - y0) > options.bars.dividerSize ) { if ( this.services.cartesianScales.getOrientation() === CartesianOrientations.VERTICAL ) { y1 += 1; } else { y1 -= 1; } } return Tools.generateSVGPathString( { x0, x1, y0, y1 }, this.services.cartesianScales.getOrientation() ); }) .attr('opacity', 1) // a11y .attr('role', Roles.GRAPHICS_SYMBOL) .attr('aria-roledescription', 'bar') .attr('aria-label', (d) => d[1] - d[0]); // Add event listeners for the above elements this.addEventListeners(); } // Highlight elements that match the hovered legend item handleLegendOnHover = (event: CustomEvent) => { const { hoveredElement } = event.detail; const { groupMapsTo } = this.model.getOptions().data; this.parent .selectAll('path.bar') .transition( this.services.transitions.getTransition('legend-hover-bar') ) .attr('opacity', (d) => d[groupMapsTo] !== hoveredElement.datum()['name'] ? 0.3 : 1 ); }; // Un-highlight all elements handleLegendMouseOut = (event: CustomEvent) => { this.parent .selectAll('path.bar') .transition( this.services.transitions.getTransition('legend-mouseout-bar') ) .attr('opacity', 1); }; addEventListeners() { const options = this.getOptions(); const { groupMapsTo } = options.data; const self = this; this.parent .selectAll('path.bar') .on('mouseover', function (datum) { const hoveredElement = select(this); hoveredElement.classed('hovered', true); hoveredElement.transition( self.services.transitions.getTransition( 'graph_element_mouseover_fill_update' ) ); // Dispatch mouse event self.services.events.dispatchEvent(Events.Bar.BAR_MOUSEOVER, { element: hoveredElement, datum, }); const displayData = self.model.getDisplayData( self.configs.groups ); let matchingDataPoint = displayData.find((d) => { const domainIdentifier = self.services.cartesianScales.getDomainIdentifier( d ); const rangeIdentifier = self.services.cartesianScales.getRangeIdentifier( d ); return ( d[rangeIdentifier] === datum.data[datum.group] && d[domainIdentifier].toString() === datum.data.sharedStackKey && d[groupMapsTo] === datum.group ); }); if (matchingDataPoint === undefined) { // use the primary range and domain ids const domainIdentifier = self.services.cartesianScales.getDomainIdentifier(); const rangeIdentifier = self.services.cartesianScales.getRangeIdentifier(); matchingDataPoint = { [domainIdentifier]: datum.data.sharedStackKey, [rangeIdentifier]: datum.data[datum.group], [groupMapsTo]: datum.group, }; } // Show tooltip self.services.events.dispatchEvent(Events.Tooltip.SHOW, { hoveredElement, data: [matchingDataPoint], }); }) .on('mousemove', function (datum) { const hoveredElement = select(this); // Dispatch mouse event self.services.events.dispatchEvent(Events.Bar.BAR_MOUSEMOVE, { element: hoveredElement, datum, }); self.services.events.dispatchEvent(Events.Tooltip.MOVE); }) .on('click', function (datum) { // Dispatch mouse event self.services.events.dispatchEvent(Events.Bar.BAR_CLICK, { element: select(this), datum, }); }) .on('mouseout', function (datum) { const hoveredElement = select(this); hoveredElement.classed('hovered', false); // Dispatch mouse event self.services.events.dispatchEvent(Events.Bar.BAR_MOUSEOUT, { element: hoveredElement, datum, }); // Hide tooltip self.services.events.dispatchEvent(Events.Tooltip.HIDE, { hoveredElement, }); }); } destroy() { // Remove event listeners this.parent .selectAll('path.bar') .on('mouseover', null) .on('mousemove', null) .on('mouseout', null); // Remove legend listeners const eventsFragment = this.services.events; eventsFragment.removeEventListener( Events.Legend.ITEM_HOVER, this.handleLegendOnHover ); eventsFragment.removeEventListener( Events.Legend.ITEM_MOUSEOUT, this.handleLegendMouseOut ); } }