UNPKG

veritatisquam

Version:
479 lines (427 loc) 11.9 kB
// Internal Imports import { Component } from '../component'; import { Roles, Events, ColorClassNameTypes } from '../../interfaces'; import { Tools } from '../../tools'; // D3 Imports import { select, Selection } from 'd3-selection'; export class Scatter extends Component { type = 'scatter'; scatterData: any; init() { const { events } = this.services; // Highlight correct circle on legend item hovers events.addEventListener( Events.Legend.ITEM_HOVER, this.handleLegendOnHover ); // Un-highlight circles on legend item mouseouts events.addEventListener( Events.Legend.ITEM_MOUSEOUT, this.handleLegendMouseOut ); const { fadeInOnChartHolderMouseover } = this.configs; if (fadeInOnChartHolderMouseover) { // Fade-in scatter circles events.addEventListener( Events.Chart.MOUSEOVER, this.handleChartHolderOnHover ); // Fade-out scatter circles events.addEventListener( Events.Chart.MOUSEOUT, this.handleChartHolderOnMouseOut ); } } filterBasedOnZoomDomain(data) { const domainIdentifier = this.services.cartesianScales.getDomainIdentifier( data ); const zoomDomain = this.model.get('zoomDomain'); if (zoomDomain !== undefined) { return data.filter( (d) => d[domainIdentifier].getTime() > zoomDomain[0].getTime() && d[domainIdentifier].getTime() < zoomDomain[1].getTime() ); } return data; } getScatterData() { const options = this.getOptions(); const { stacked } = this.configs; let scatterData; if (stacked) { const percentage = Object.keys(options.axes).some( (axis) => options.axes[axis].percentage ); scatterData = this.model.getStackedData({ groups: this.configs.groups, percentage, }); } else { scatterData = this.model .getDisplayData(this.configs.groups) .filter((d) => { const rangeIdentifier = this.services.cartesianScales.getRangeIdentifier( d ); return ( d[rangeIdentifier] !== undefined && d[rangeIdentifier] !== null ); }); } // filter out datapoints that aren't part of the zoomed domain return this.filterBasedOnZoomDomain(scatterData); } render(animate: boolean) { const isScatterEnabled = Tools.getProperty(this.getOptions(), 'points', 'enabled') || Tools.getProperty(this.getOptions(), 'bubble', 'enabled'); if (!isScatterEnabled) { return; } // Grab container SVG const svg = this.getContainerSVG({ withinChartClip: true }); const options = this.getOptions(); const { groupMapsTo } = options.data; const domainIdentifier = this.services.cartesianScales.getDomainIdentifier(); // Update data on dot groups const circles = svg .selectAll('circle.dot') .data( this.getScatterData(), (datum) => `${datum[groupMapsTo]}-${datum[domainIdentifier]}` ); // Remove circles that need to be removed circles.exit().attr('opacity', 0).remove(); // Add the dot groups that need to be introduced const enteringCircles = circles .enter() .append('circle') .classed('dot', true) .attr('opacity', 0); // Apply styling & position const circlesToStyle = enteringCircles.merge(circles); this.styleCircles(circlesToStyle, animate); // Add event listeners to elements drawn this.addEventListeners(); } // A value is an anomaly if is above all defined domain and range thresholds isDatapointThresholdAnomaly(datum: any, index: number) { const { handleThresholds } = this.configs; if (!handleThresholds) { return false; } const { cartesianScales } = this.services; const orientation = cartesianScales.getOrientation(); // Get highest domain and range thresholds const [ xThreshold, yThreshold, ] = Tools.flipDomainAndRangeBasedOnOrientation( this.services.cartesianScales.getHighestDomainThreshold(), this.services.cartesianScales.getHighestRangeThreshold(), orientation ); const [ getXValue, getYValue, ] = Tools.flipDomainAndRangeBasedOnOrientation( (d, i) => this.services.cartesianScales.getDomainValue(d, i), (d, i) => this.services.cartesianScales.getRangeValue(d, i), orientation ); // Get datum x and y values const xValue = getXValue(datum, index); const yValue = getYValue(datum, index); // To be an anomaly, the value has to be higher or equal than the threshold value // (if are present, both range and domain threshold values) if (yThreshold && xThreshold) { return ( yValue <= yThreshold.scaleValue && xValue >= xThreshold.scaleValue ); } if (yThreshold) { return yValue <= yThreshold.scaleValue; } if (xThreshold) { return xValue >= xThreshold.scaleValue; } } styleCircles(selection: Selection<any, any, any, any>, animate: boolean) { // Chart options mixed with the internal configurations const options = this.getOptions(); const { filled, fillOpacity } = options.points; const { cartesianScales, transitions } = this.services; const { groupMapsTo } = options.data; const getDomainValue = (d, i) => cartesianScales.getDomainValue(d, i); const getRangeValue = (d, i) => cartesianScales.getRangeValue(d, i); const [ getXValue, getYValue, ] = Tools.flipDomainAndRangeBasedOnOrientation( getDomainValue, getRangeValue, cartesianScales.getOrientation() ); const { fadeInOnChartHolderMouseover } = this.configs; selection .raise() .classed('dot', true) .attr('class', (d) => { const domainIdentifier = cartesianScales.getDomainIdentifier(d); const isFilled = this.model.getIsFilled( d[groupMapsTo], d[domainIdentifier], d, filled ); const classNamesNeeded = isFilled ? [ColorClassNameTypes.FILL, ColorClassNameTypes.STROKE] : [ColorClassNameTypes.STROKE]; return this.model.getColorClassName({ classNameTypes: classNamesNeeded, dataGroupName: d[groupMapsTo], originalClassName: 'dot', }); }) // Set class to highlight the dots that are above all the thresholds, in both directions (vertical and horizontal) .classed('threshold-anomaly', (d, i) => this.isDatapointThresholdAnomaly(d, i) ) .classed('filled', (d) => { const domainIdentifier = cartesianScales.getDomainIdentifier(d); return this.model.getIsFilled( d[groupMapsTo], d[domainIdentifier], d, filled ); }) .classed('unfilled', (d) => { const domainIdentifier = cartesianScales.getDomainIdentifier(d); return !this.model.getIsFilled( d[groupMapsTo], d[domainIdentifier], d, filled ); }) .transition( transitions.getTransition('scatter-update-enter', animate) ) .attr('cx', getXValue) .attr('cy', getYValue) .attr('r', options.points.radius) .style('fill', (d) => { const domainIdentifier = cartesianScales.getDomainIdentifier(d); if ( this.model.getIsFilled( d[groupMapsTo], d[domainIdentifier], d, filled ) ) { return this.model.getFillColor( d[groupMapsTo], d[domainIdentifier], d ); } }) .style('stroke', (d) => { const domainIdentifier = cartesianScales.getDomainIdentifier(d); return this.model.getStrokeColor( d[groupMapsTo], d[domainIdentifier], d ); }) .attr('fill-opacity', filled ? fillOpacity : 1) .attr('opacity', fadeInOnChartHolderMouseover ? 0 : 1) // a11y .attr('role', Roles.GRAPHICS_SYMBOL) .attr('aria-roledescription', 'point') .attr('aria-label', (d) => { const rangeIdentifier = cartesianScales.getRangeIdentifier(d); return d[rangeIdentifier]; }); // Add event listeners to elements drawn this.addEventListeners(); } handleChartHolderOnHover = (event: CustomEvent) => { this.parent .selectAll('circle.dot') .transition( this.services.transitions.getTransition( 'chart-holder-hover-scatter' ) ) .attr('opacity', 1); }; handleChartHolderOnMouseOut = (event: CustomEvent) => { this.parent .selectAll('circle.dot') .transition( this.services.transitions.getTransition( 'chart-holder-mouseout-scatter' ) ) .attr('opacity', 0); }; handleLegendOnHover = (event: CustomEvent) => { const { hoveredElement } = event.detail; const { groupMapsTo } = this.getOptions().data; this.parent .selectAll('circle.dot') .transition( this.services.transitions.getTransition('legend-hover-scatter') ) .attr('opacity', (d) => d[groupMapsTo] !== hoveredElement.datum()['name'] ? 0.3 : 1 ); }; handleLegendMouseOut = (event: CustomEvent) => { this.parent .selectAll('circle.dot') .transition( this.services.transitions.getTransition( 'legend-mouseout-scatter' ) ) .attr('opacity', 1); }; addEventListeners() { const self = this; const { groupMapsTo } = self.getOptions().data; this.parent .selectAll('circle') .on('mouseover', function (datum) { const hoveredElement = select(this); hoveredElement .classed('hovered', true) .attr('class', (d) => self.model.getColorClassName({ classNameTypes: [ColorClassNameTypes.FILL], dataGroupName: d[groupMapsTo], originalClassName: hoveredElement.attr('class'), }) ) .style('fill', (d) => { const domainIdentifier = self.services.cartesianScales.getDomainIdentifier( d ); return self.model.getFillColor( d[groupMapsTo], d[domainIdentifier], d ); }) .classed('unfilled', false); // Show tooltip for single datapoint self.services.events.dispatchEvent(Events.Tooltip.SHOW, { hoveredElement, data: [datum], }); // Dispatch mouse event self.services.events.dispatchEvent( Events.Scatter.SCATTER_MOUSEOVER, { element: hoveredElement, datum, } ); }) .on('mousemove', function (datum) { const hoveredElement = select(this); // Dispatch mouse event self.services.events.dispatchEvent( Events.Scatter.SCATTER_MOUSEMOVE, { element: hoveredElement, datum, } ); self.services.events.dispatchEvent(Events.Tooltip.MOVE); }) .on('click', function (datum) { // Dispatch mouse event self.services.events.dispatchEvent( Events.Scatter.SCATTER_CLICK, { element: select(this), datum, } ); }) .on('mouseout', function (datum) { const hoveredElement = select(this); hoveredElement.classed('hovered', false); if (!self.configs.filled) { const { filled } = self.getOptions().points; const domainIdentifier = self.services.cartesianScales.getDomainIdentifier( datum ); hoveredElement .classed( 'unfilled', !self.model.getIsFilled( datum[groupMapsTo], datum[domainIdentifier], datum, filled ) ) .style("fill", d => filled ? self.model.getFillColor( d[groupMapsTo], d[domainIdentifier], d ) : null ); } // Dispatch mouse event self.services.events.dispatchEvent( Events.Scatter.SCATTER_MOUSEOUT, { element: hoveredElement, datum, } ); // Hide tooltip self.services.events.dispatchEvent(Events.Tooltip.HIDE, { hoveredElement, }); }); } destroy() { // Remove event listeners this.parent .selectAll('circle') .on('mousemove', null) .on('mouseout', null); // Remove legend listeners const { events } = this.services; events.removeEventListener( Events.Legend.ITEM_HOVER, this.handleLegendOnHover ); events.removeEventListener( Events.Legend.ITEM_MOUSEOUT, this.handleLegendMouseOut ); events.removeEventListener( Events.Chart.MOUSEOVER, this.handleChartHolderOnHover ); events.removeEventListener( Events.Chart.MOUSEOUT, this.handleChartHolderOnMouseOut ); } }