UNPKG

@carbon/charts

Version:
582 lines 28.5 kB
var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); // Internal Imports import { Component } from '../component'; import { DOMUtils } from '../../services'; import { Events, Roles, ColorClassNameTypes } from '../../interfaces'; import { Tools } from '../../tools'; import { radialLabelPlacement, radToDeg, polarToCartesianCoords, distanceBetweenPointOnCircAndVerticalDiameter, } from '../../services/angle-utils'; import * as Configuration from '../../configuration'; // D3 Imports import { select } from 'd3-selection'; import { scaleBand, scaleLinear } from 'd3-scale'; import { max, extent } from 'd3-array'; import { lineRadial, curveLinearClosed } from 'd3-shape'; // used to make transitions var oldYScale; var Radar = /** @class */ (function (_super) { __extends(Radar, _super); function Radar() { var _this = _super !== null && _super.apply(this, arguments) || this; _this.type = 'radar'; // append temporarily the label to get the exact space that it occupies _this.getLabelDimensions = function (label) { var tmpTick = DOMUtils.appendOrSelect(_this.getContainerSVG(), "g.tmp-tick"); var tmpTickText = DOMUtils.appendOrSelect(tmpTick, "text").text(label); var _a = DOMUtils.getSVGElementSize(tmpTickText.node(), { useBBox: true }), width = _a.width, height = _a.height; tmpTick.remove(); return { width: width, height: height }; }; // Given a flat array of objects, if there are missing data on key, // creates corresponding data with value = null _this.normalizeFlatData = function (dataset) { var options = _this.getOptions(); var _a = Tools.getProperty(options, 'radar', 'axes'), angle = _a.angle, value = _a.value; var groupMapsTo = Tools.getProperty(options, 'data', 'groupMapsTo'); var completeBlankData = Tools.flatMapDeep(_this.uniqueKeys.map(function (key) { return _this.uniqueGroups.map(function (group) { var _a; return (_a = {}, _a[angle] = key, _a[groupMapsTo] = group, _a[value] = null, _a); }); })); return Tools.merge(completeBlankData, dataset); }; // Given a a grouped array of objects, if there are missing data on key, // creates corresponding data with value = null _this.normalizeGroupedData = function (dataset) { var options = _this.getOptions(); var _a = Tools.getProperty(options, 'radar', 'axes'), angle = _a.angle, value = _a.value; var groupMapsTo = Tools.getProperty(options, 'data', 'groupMapsTo'); return dataset.map(function (_a) { var name = _a.name, data = _a.data; var completeBlankData = _this.uniqueKeys.map(function (k) { var _a; return (_a = {}, _a[groupMapsTo] = name, _a[angle] = k, _a[value] = null, _a); }); return { name: name, data: Tools.merge(completeBlankData, data) }; }); }; _this.handleLegendOnHover = function (event) { var hoveredElement = event.detail.hoveredElement; _this.parent .selectAll('g.blobs path') .transition(_this.services.transitions.getTransition('legend-hover-blob')) .style('fill-opacity', function (group) { if (group.name !== hoveredElement.datum().name) { return Configuration.radar.opacity.unselected; } return Configuration.radar.opacity.selected; }) .style('stroke-opacity', function (group) { if (group.name !== hoveredElement.datum().name) { return Configuration.radar.opacity.unselected; } return 1; }); }; _this.handleLegendMouseOut = function (event) { _this.parent .selectAll('g.blobs path') .transition(_this.services.transitions.getTransition('legend-mouseout-blob')) .style('fill-opacity', Configuration.radar.opacity.selected) .style('stroke-opacity', 1); }; return _this; } Radar.prototype.init = function () { var events = this.services.events; // Highlight correct line legend item hovers events.addEventListener(Events.Legend.ITEM_HOVER, this.handleLegendOnHover); // Un-highlight lines on legend item mouseouts events.addEventListener(Events.Legend.ITEM_MOUSEOUT, this.handleLegendMouseOut); }; Radar.prototype.render = function (animate) { var _this = this; if (animate === void 0) { animate = true; } var svg = this.getContainerSVG(); var _a = DOMUtils.getSVGElementSize(this.parent, { useAttrs: true, }), width = _a.width, height = _a.height; var data = this.model.getData(); var displayData = this.model.getDisplayData(); var groupedData = this.model.getGroupedData(); var options = this.getOptions(); var _b = Tools.getProperty(options, 'radar', 'axes'), angle = _b.angle, value = _b.value; var groupMapsTo = Tools.getProperty(options, 'data', 'groupMapsTo'); var _c = Configuration.radar, xLabelPadding = _c.xLabelPadding, yLabelPadding = _c.yLabelPadding, yTicksNumber = _c.yTicksNumber, minRange = _c.minRange, xAxisRectHeight = _c.xAxisRectHeight; this.uniqueKeys = Array.from(new Set(data.map(function (d) { return d[angle]; }))); this.uniqueGroups = Array.from(new Set(data.map(function (d) { return d[groupMapsTo]; }))); this.displayDataNormalized = this.normalizeFlatData(displayData); this.groupedDataNormalized = this.normalizeGroupedData(groupedData); var labelHeight = this.getLabelDimensions(this.uniqueKeys[0]).height; var margin = 2 * (labelHeight + yLabelPadding); var size = Math.min(width, height); var diameter = size - margin; var radius = diameter / 2; if (radius <= 0) { return; } // given a key, return the corresponding angle in radiants // rotated by -PI/2 because we want angle 0° at -y (12 o’clock) var xScale = scaleBand() .domain(this.displayDataNormalized.map(function (d) { return d[angle]; })) .range([0, 2 * Math.PI].map(function (a) { return a - Math.PI / 2; })); var yScale = scaleLinear() .domain([ 0, max(this.displayDataNormalized.map(function (d) { return d[value]; })), ]) .range([minRange, radius]) .nice(yTicksNumber); var yTicks = yScale.ticks(yTicksNumber); var colorScale = function (group) { return _this.model.getFillColor(group); }; // constructs a new radial line generator // the angle accessor returns the angle in radians with 0° at -y (12 o’clock) // so map back the angle var radialLineGenerator = lineRadial() .angle(function (d) { return xScale(d[angle]) + Math.PI / 2; }) .radius(function (d) { return yScale(d[value]); }) .curve(curveLinearClosed); // this line generator is necessary in order to make a transition of a value from the // position it occupies using the old scale to the position it occupies using the new scale var oldRadialLineGenerator = lineRadial() .angle(radialLineGenerator.angle()) .radius(function (d) { return (oldYScale ? oldYScale(d[value]) : minRange); }) .curve(radialLineGenerator.curve()); // compute the space that each x label needs var horizSpaceNeededByEachXLabel = this.uniqueKeys.map(function (key) { var tickWidth = _this.getLabelDimensions(key).width; // compute the distance between the point that the label rapresents and the vertical diameter var distanceFromDiameter = distanceBetweenPointOnCircAndVerticalDiameter(xScale(key), radius); // the space each label occupies is the sum of these two values return tickWidth + distanceFromDiameter; }); var leftPadding = max(horizSpaceNeededByEachXLabel); // center coordinates var c = { x: leftPadding + xLabelPadding, y: height / 2, }; ///////////////////////////// // Drawing the radar ///////////////////////////// // y axes var yAxes = DOMUtils.appendOrSelect(svg, 'g.y-axes').attr('role', Roles.GROUP); var yAxisUpdate = yAxes .selectAll('path') .data(yTicks, function (tick) { return tick; }); // for each tick, create array of data corresponding to the points composing the shape var shapeData = function (tick) { return _this.uniqueKeys.map(function (key) { var _a; return (_a = {}, _a[angle] = key, _a[value] = tick, _a); }); }; yAxisUpdate.join(function (enter) { return enter .append('path') .attr('role', Roles.GRAPHICS_SYMBOL) .attr('opacity', 0) .attr('transform', "translate(" + c.x + ", " + c.y + ")") .attr('fill', 'none') .attr('d', function (tick) { return oldRadialLineGenerator(shapeData(tick)); }) .call(function (selection) { return selection .transition(_this.services.transitions.getTransition('radar_y_axes_enter', animate)) .attr('opacity', 1) .attr('d', function (tick) { return radialLineGenerator(shapeData(tick)); }); }); }, function (update) { return update.call(function (selection) { return selection .transition(_this.services.transitions.getTransition('radar_y_axes_update', animate)) .attr('opacity', 1) .attr('transform', "translate(" + c.x + ", " + c.y + ")") .attr('d', function (tick) { return radialLineGenerator(shapeData(tick)); }); }); }, function (exit) { return exit.call(function (selection) { return selection .transition(_this.services.transitions.getTransition('radar_y_axes_exit', animate)) .attr('d', function (tick) { return radialLineGenerator(shapeData(tick)); }) .attr('opacity', 0) .remove(); }); }); // x axes var xAxes = DOMUtils.appendOrSelect(svg, 'g.x-axes').attr('role', Roles.GROUP); var xAxisUpdate = xAxes .selectAll('line') .data(this.uniqueKeys, function (key) { return key; }); xAxisUpdate.join(function (enter) { return enter .append('line') .attr('role', Roles.GRAPHICS_SYMBOL) .attr('opacity', 0) .attr('class', function (key) { return "x-axis-" + Tools.kebabCase(key); }) // replace spaces with - .attr('stroke-dasharray', '0') .attr('x1', function (key) { return polarToCartesianCoords(xScale(key), 0, c).x; }) .attr('y1', function (key) { return polarToCartesianCoords(xScale(key), 0, c).y; }) .attr('x2', function (key) { return polarToCartesianCoords(xScale(key), 0, c).x; }) .attr('y2', function (key) { return polarToCartesianCoords(xScale(key), 0, c).y; }) .call(function (selection) { return selection .transition(_this.services.transitions.getTransition('radar_x_axes_enter', animate)) .attr('opacity', 1) .attr('x1', function (key) { return polarToCartesianCoords(xScale(key), yScale.range()[0], c).x; }) .attr('y1', function (key) { return polarToCartesianCoords(xScale(key), yScale.range()[0], c).y; }) .attr('x2', function (key) { return polarToCartesianCoords(xScale(key), yScale.range()[1], c).x; }) .attr('y2', function (key) { return polarToCartesianCoords(xScale(key), yScale.range()[1], c).y; }); }); }, function (update) { return update.call(function (selection) { return selection .transition(_this.services.transitions.getTransition('radar_x_axes_update', animate)) .attr('opacity', 1) .attr('x1', function (key) { return polarToCartesianCoords(xScale(key), yScale.range()[0], c).x; }) .attr('y1', function (key) { return polarToCartesianCoords(xScale(key), yScale.range()[0], c).y; }) .attr('x2', function (key) { return polarToCartesianCoords(xScale(key), yScale.range()[1], c).x; }) .attr('y2', function (key) { return polarToCartesianCoords(xScale(key), yScale.range()[1], c).y; }); }); }, function (exit) { return exit.call(function (selection) { return selection .transition(_this.services.transitions.getTransition('radar_x_axes_exit', animate)) .attr('opacity', 0) .remove(); }); }); // x labels var xLabels = DOMUtils.appendOrSelect(svg, 'g.x-labels').attr('role', Roles.GROUP); var xLabelUpdate = xLabels.selectAll('text').data(this.uniqueKeys); xLabelUpdate.join(function (enter) { return enter .append('text') .text(function (key) { return key; }) .attr('opacity', 0) .attr('x', function (key) { return polarToCartesianCoords(xScale(key), yScale.range()[1] + xLabelPadding, c).x; }) .attr('y', function (key) { return polarToCartesianCoords(xScale(key), yScale.range()[1] + xLabelPadding, c).y; }) .style('text-anchor', function (key) { return radialLabelPlacement(xScale(key)).textAnchor; }) .style('dominant-baseline', function (key) { return radialLabelPlacement(xScale(key)).dominantBaseline; }) .call(function (selection) { return selection .transition(_this.services.transitions.getTransition('radar_x_labels_enter', animate)) .attr('opacity', 1); }); }, function (update) { return update.call(function (selection) { return selection .transition(_this.services.transitions.getTransition('radar_x_labels_update', animate)) .attr('opacity', 1) .attr('x', function (key) { return polarToCartesianCoords(xScale(key), yScale.range()[1] + xLabelPadding, c).x; }) .attr('y', function (key) { return polarToCartesianCoords(xScale(key), yScale.range()[1] + xLabelPadding, c).y; }); }); }, function (exit) { return exit.call(function (selection) { return selection .transition(_this.services.transitions.getTransition('radar_x_labels_exit', animate)) .attr('opacity', 0) .remove(); }); }); // blobs var blobs = DOMUtils.appendOrSelect(svg, 'g.blobs').attr('role', Roles.GROUP); var blobUpdate = blobs .selectAll('path') .data(this.groupedDataNormalized, function (group) { return group.name; }); blobUpdate.join(function (enter) { return enter .append('path') .attr('class', function (group) { return _this.model.getColorClassName({ classNameTypes: [ ColorClassNameTypes.FILL, ColorClassNameTypes.STROKE, ], dataGroupName: group.name, originalClassName: 'blob', }); }) .attr('role', Roles.GRAPHICS_SYMBOL) .attr('opacity', 0) .attr('transform', "translate(" + c.x + ", " + c.y + ")") .style('fill', function (group) { return colorScale(group.name); }) .style('fill-opacity', Configuration.radar.opacity.selected) .style('stroke', function (group) { return colorScale(group.name); }) .attr('d', function (group) { return oldRadialLineGenerator(group.data); }) .call(function (selection) { return selection .transition(_this.services.transitions.getTransition('radar_blobs_enter', animate)) .attr('opacity', 1) .attr('d', function (group) { return radialLineGenerator(group.data); }); }); }, function (update) { update .attr('class', function (group) { return _this.model.getColorClassName({ classNameTypes: [ ColorClassNameTypes.FILL, ColorClassNameTypes.STROKE, ], dataGroupName: group.name, originalClassName: 'blob', }); }) .style('fill', function (group) { return colorScale(group.name); }) .style('stroke', function (group) { return colorScale(group.name); }); update.call(function (selection) { return selection .transition(_this.services.transitions.getTransition('radar_blobs_update', animate)) .attr('opacity', 1) .attr('transform', "translate(" + c.x + ", " + c.y + ")") .attr('d', function (group) { return radialLineGenerator(group.data); }); }); }, function (exit) { return exit.call(function (selection) { return selection .transition(_this.services.transitions.getTransition('radar_blobs_exit', animate)) .attr('d', function (group) { return radialLineGenerator(group.data); }) .attr('opacity', 0) .remove(); }); }); // data dots var dots = DOMUtils.appendOrSelect(svg, 'g.dots').attr('role', Roles.GROUP); var dotsUpdate = dots .selectAll('circle') .data(this.displayDataNormalized); dotsUpdate .join(function (enter) { return enter.append('circle').attr('role', Roles.GRAPHICS_SYMBOL); }, function (update) { return update; }, function (exit) { return exit.remove(); }) .attr('class', function (d) { return _this.model.getColorClassName({ classNameTypes: [ColorClassNameTypes.FILL], dataGroupName: d[groupMapsTo], originalClassName: Tools.kebabCase(d[angle]), }); }) .attr('cx', function (d) { return polarToCartesianCoords(xScale(d[angle]), yScale(d[value]), c).x; }) .attr('cy', function (d) { return polarToCartesianCoords(xScale(d[angle]), yScale(d[value]), c).y; }) .attr('r', 0) .attr('opacity', 0) .style('fill', function (d) { return colorScale(d[groupMapsTo]); }); // rectangles var xAxesRect = DOMUtils.appendOrSelect(svg, 'g.x-axes-rect').attr('role', Roles.GROUP); var xAxisRectUpdate = xAxesRect .selectAll('rect') .data(this.uniqueKeys); xAxisRectUpdate .join(function (enter) { return enter.append('rect').attr('role', Roles.GRAPHICS_SYMBOL); }, function (update) { return update; }, function (exit) { return exit.remove(); }) .attr('x', c.x) .attr('y', c.y - xAxisRectHeight / 2) .attr('width', yScale.range()[1]) .attr('height', xAxisRectHeight) .style('fill', 'red') .style('fill-opacity', 0) .attr('transform', function (key) { return "rotate(" + radToDeg(xScale(key)) + ", " + c.x + ", " + c.y + ")"; }); // y labels (show only the min and the max labels) var yLabels = DOMUtils.appendOrSelect(svg, 'g.y-labels').attr('role', Roles.GROUP); var yLabelUpdate = yLabels.selectAll('text').data(extent(yTicks)); yLabelUpdate.join(function (enter) { return enter .append('text') .attr('opacity', 0) .text(function (tick) { return tick; }) .attr('x', function (tick) { return polarToCartesianCoords(-Math.PI / 2, yScale(tick), c).x + yLabelPadding; }) .attr('y', function (tick) { return polarToCartesianCoords(-Math.PI / 2, yScale(tick), c).y; }) .style('text-anchor', 'start') .style('dominant-baseline', 'middle') .call(function (selection) { return selection .transition(_this.services.transitions.getTransition('radar_y_labels_enter', animate)) .attr('opacity', 1); }); }, function (update) { return update.call(function (selection) { return selection .transition(_this.services.transitions.getTransition('radar_y_labels_update', animate)) .text(function (tick) { return tick; }) .attr('opacity', 1) .attr('x', function (tick) { return polarToCartesianCoords(-Math.PI / 2, yScale(tick), c).x + yLabelPadding; }) .attr('y', function (tick) { return polarToCartesianCoords(-Math.PI / 2, yScale(tick), c).y; }); }); }, function (exit) { return exit.call(function (selection) { return selection .transition(_this.services.transitions.getTransition('radar_y_labels_exit', animate)) .attr('opacity', 0) .remove(); }); }); var alignment = Tools.getProperty(options, 'radar', 'alignment'); var alignmentOffset = DOMUtils.getAlignmentOffset(alignment, svg, this.getParent()); svg.attr('transform', "translate(" + alignmentOffset + ", 0)"); // Add event listeners this.addEventListeners(); oldYScale = yScale; // save the current scale as the old one }; Radar.prototype.destroy = function () { // Remove event listeners this.parent .selectAll('.x-axes-rect > rect') .on('mouseover', null) .on('mousemove', null) .on('mouseout', null); // Remove legend listeners var eventsFragment = this.services.events; eventsFragment.removeEventListener(Events.Legend.ITEM_HOVER, this.handleLegendOnHover); eventsFragment.removeEventListener(Events.Legend.ITEM_MOUSEOUT, this.handleLegendMouseOut); }; Radar.prototype.addEventListeners = function () { var self = this; var angle = Tools.getProperty(this.getOptions(), 'radar').axes.angle; // events on x axes rects this.parent .selectAll('.x-axes-rect > rect') .on('mouseover', function (datum) { var hoveredElement = select(this); // Dispatch mouse event self.services.events.dispatchEvent(Events.Radar.X_AXIS_MOUSEOVER, { element: hoveredElement, datum: datum, }); var axisLine = self.parent.select(".x-axes .x-axis-" + Tools.kebabCase(datum)); var dots = self.parent.selectAll(".dots circle." + Tools.kebabCase(datum)); // Change style axisLine .classed('hovered', true) .attr('stroke-dasharray', '4 4'); dots.classed('hovered', true) .attr('opacity', 1) .attr('r', Configuration.radar.dotsRadius); // get the items that should be highlighted var itemsToHighlight = self.displayDataNormalized.filter(function (d) { return d[angle] === datum; }); var options = self.getOptions(); var groupMapsTo = options.data.groupMapsTo; var valueMapsTo = Tools.getProperty(options, 'radar', 'axes', 'value'); // Show tooltip self.services.events.dispatchEvent(Events.Tooltip.SHOW, { hoveredElement: hoveredElement, items: itemsToHighlight .filter(function (datum) { return typeof datum[valueMapsTo] === 'number'; }) .map(function (datum) { return ({ label: datum[groupMapsTo], value: datum[valueMapsTo], color: self.model.getFillColor(datum[groupMapsTo]), class: self.model.getColorClassName({ classNameTypes: [ColorClassNameTypes.TOOLTIP], dataGroupName: datum[groupMapsTo], }), }); }), }); }) .on('mousemove', function (datum) { var hoveredElement = select(this); // Dispatch mouse event self.services.events.dispatchEvent(Events.Radar.X_AXIS_MOUSEMOVE, { element: hoveredElement, datum: datum, }); self.services.events.dispatchEvent(Events.Tooltip.MOVE); }) .on('click', function (datum) { // Dispatch mouse event self.services.events.dispatchEvent(Events.Radar.X_AXIS_CLICK, { element: select(this), datum: datum, }); }) .on('mouseout', function (datum) { var hoveredElement = select(this); var axisLine = self.parent.select(".x-axes .x-axis-" + Tools.kebabCase(datum)); var dots = self.parent.selectAll(".dots circle." + Tools.kebabCase(datum)); // Change style axisLine .classed('hovered', false) .attr('stroke-dasharray', '0'); dots.classed('hovered', false).attr('opacity', 0).attr('r', 0); // Dispatch mouse event self.services.events.dispatchEvent(Events.Radar.X_AXIS_MOUSEOUT, { element: hoveredElement, datum: datum, }); // Hide tooltip self.services.events.dispatchEvent(Events.Tooltip.HIDE); }); }; return Radar; }(Component)); export { Radar }; //# sourceMappingURL=../../../src/components/graphs/radar.js.map