UNPKG

@testboxlab/react-bubble-chart-d3

Version:

ReactJS component to display data as a bubble chart using D3.

408 lines (376 loc) 10.7 kB
import React, { Component, createRef } from "react"; import ReactDOM from "react-dom"; import PropTypes from "prop-types"; import * as d3 from "d3"; export default class BubbleChart extends Component { constructor(props) { super(props); this.renderChart = this.renderChart.bind(this); this.renderBubbles = this.renderBubbles.bind(this); this.renderLegend = this.renderLegend.bind(this); this.svg = createRef(); } componentDidMount() { this.renderChart(); } componentDidUpdate() { const { width, height } = this.props; if (width !== 0 && height !== 0) { this.renderChart(); } } render() { const { width, height } = this.props; return <svg width={width} height={height} ref={this.svg} />; } renderChart() { const { overflow, graph, data, height, width, padding, showLegend, showValue, legendPercentage, charsBeforeSplit, } = this.props; // Reset the svg element to a empty state. this.svg.current.innerHTML = ""; // Allow bubbles overflowing its SVG container in visual aspect if props(overflow) is true. if (overflow) this.svg.current.style.overflow = "visible"; const bubblesWidth = showLegend ? width * (1 - legendPercentage / 100) : width; const legendWidth = width - bubblesWidth; const color = d3.scaleOrdinal(d3.schemeCategory20c); const pack = d3 .pack() .size([bubblesWidth * graph.zoom, bubblesWidth * graph.zoom]) .padding(padding); // Process the data to have a hierarchy structure; const root = d3 .hierarchy({ children: data }) .sum(function (d) { return d.value; }) .sort(function (a, b) { return b.value - a.value; }) .each((d) => { if (d.data.label) { d.label = d.data.label; d.id = d.data.label.toLowerCase().replace(/ |\//g, "-"); } }); // Pass the data to the pack layout to calculate the distribution. const nodes = pack(root).leaves(); // Call to the function that draw the bubbles. this.renderBubbles(bubblesWidth, height, nodes, color); // Call to the function that draw the legend. if (showLegend) { this.renderLegend(legendWidth, height, bubblesWidth, nodes, color); } } renderBubbles(width, height, nodes, color) { const { graph, data, bubbleClickFun, valueFont, labelFont, showValue, charsBeforeSplit, } = this.props; const splitByFirstSpace = (str) => { const index = str.indexOf(" "); if (index === -1 || str.length <= charsBeforeSplit) { return [str]; } else { return [str.slice(0, index), str.slice(index + 1)]; } }; var insertLinebreaks = function (d) { var text = d3.select(this); var words = splitByFirstSpace(text.text()); text.text(""); for (var i = 0; i < words.length; i++) { var tspan = text.append("tspan").text(words[i]); if (i >= 0) tspan.attr("x", 0).attr("dy", "15"); } }; const bubbleChart = d3 .select(this.svg.current) .append("g") .attr("class", "bubble-chart") .attr("transform", function (d) { return ( "translate(" + (width - width * graph.zoom) / 2 + "," + (height - height * graph.zoom) / 2 + ")" ); }); const node = bubbleChart .selectAll(".node") .data(nodes) .enter() .append("g") .attr("class", "node") .attr("transform", function (d) { return "translate(" + d.x + "," + d.y + ")"; }) .on("click", function (d) { bubbleClickFun(d.label); }); node .append("circle") .attr("id", function (d) { return d.id; }) .attr("r", function (d) { return d.r; }) .style("fill", function (d) { return d.data.color ? d.data.color : color(nodes.indexOf(d)); }) .style("z-index", 1); node .append("clipPath") .attr("id", function (d) { return "clip-" + d.id; }) .append("use") .attr("xlink:href", function (d) { return "#" + d.id; }); if (showValue) { node .append("text") .attr("class", "value-text") .style("font-size", `${valueFont.size}px`) .attr("clip-path", function (d) { return "url(#clip-" + d.id + ")"; }) .style("font-weight", (d) => { return valueFont.weight ? valueFont.weight : 600; }) .style("font-family", valueFont.family) .style("fill", () => { return valueFont.color ? valueFont.color : "#000"; }) .style("stroke", () => { return valueFont.lineColor ? valueFont.lineColor : "#000"; }) .style("stroke-width", () => { return valueFont.lineWeight ? valueFont.lineWeight : 0; }) .text(function (d) { return d.value; }); } node .append("text") .attr("class", "label-text") .attr("text-anchor", "middle") .attr("dominant-baseline", "middle") .style("font-size", `${labelFont.size}px`) .attr("clip-path", function (d) { return "url(#clip-" + d.id + ")"; }) .style("font-weight", (d) => { return labelFont.weight ? labelFont.weight : 600; }) .style("font-family", labelFont.family) .style("fill", () => { return labelFont.color ? labelFont.color : "#000"; }) .style("stroke", () => { return labelFont.lineColor ? labelFont.lineColor : "#000"; }) .style("stroke-width", () => { return labelFont.lineWeight ? labelFont.lineWeight : 0; }) .text(function (d) { return d.label; }) .each(insertLinebreaks); // Center the texts inside the circles. d3.selectAll(".label-text") .attr("x", function (d) { const self = d3.select(this); const width = self.node().getBBox().width; return -(width / 2); }) .style("opacity", function (d) { const self = d3.select(this); const width = self.node().getBBox().width; d.hideLabel = width * 1.05 > d.r * 2; return d.hideLabel ? 0 : 1; }) .attr("y", function (d) { const self = d3.select(this); const height = self.node().getBBox().height; return -(height / 2 + 10); }); // Center the texts inside the circles. d3.selectAll(".value-text") .attr("x", function (d) { const self = d3.select(this); const width = self.node().getBBox().width; return -(width / 2); }) .attr("y", function (d) { if (d.hideLabel) { return valueFont.size / 3; } else { return -valueFont.size * 0.5; } }); node.append("title").text(function (d) { return d.label; }); } renderLegend(width, height, offset, nodes, color) { const { data, legendClickFun, legendFont } = this.props; const bubble = d3.select(".bubble-chart"); const bubbleHeight = bubble.node().getBBox().height; const legend = d3 .select(this.svg.current) .append("g") .attr("transform", function () { return `translate(${offset},${bubbleHeight * 0.05})`; }) .attr("class", "legend"); let textOffset = 0; const texts = legend .selectAll(".legend-text") .data(nodes) .enter() .append("g") .attr("transform", (d, i) => { const offset = textOffset; textOffset += legendFont.size + 10; return `translate(0,${offset})`; }) .on("mouseover", function (d) { d3.select("#" + d.id).attr("r", d.r * 1.04); }) .on("mouseout", function (d) { const r = d.r - d.r * 0.04; d3.select("#" + d.id).attr("r", r); }) .on("click", function (d) { legendClickFun(d.label); }); texts .append("rect") .attr("width", 30) .attr("height", legendFont.size) .attr("x", 0) .attr("y", -legendFont.size) .style("fill", "transparent"); texts .append("rect") .attr("width", legendFont.size) .attr("height", legendFont.size) .attr("x", 0) .attr("y", -legendFont.size) .style("fill", function (d) { return d.data.color ? d.data.color : color(nodes.indexOf(d)); }); texts .append("text") .style("font-size", `${legendFont.size}px`) .style("font-weight", (d) => { return legendFont.weight ? legendFont.weight : 600; }) .style("font-family", legendFont.family) .style("fill", () => { return legendFont.color ? legendFont.color : "#000"; }) .style("stroke", () => { return legendFont.lineColor ? legendFont.lineColor : "#000"; }) .style("stroke-width", () => { return legendFont.lineWeight ? legendFont.lineWeight : 0; }) .attr("x", (d) => { return legendFont.size + 10; }) .attr("y", 0) .text((d) => { return d.label; }); } } BubbleChart.propTypes = { overflow: PropTypes.bool, graph: PropTypes.shape({ zoom: PropTypes.number, offsetX: PropTypes.number, offsetY: PropTypes.number, }), width: PropTypes.number, height: PropTypes.number, padding: PropTypes.number, showLegend: PropTypes.bool, legendPercentage: PropTypes.number, legendFont: PropTypes.shape({ family: PropTypes.string, size: PropTypes.number, color: PropTypes.string, weight: PropTypes.string, }), valueFont: PropTypes.shape({ family: PropTypes.string, size: PropTypes.number, color: PropTypes.string, weight: PropTypes.string, }), labelFont: PropTypes.shape({ family: PropTypes.string, size: PropTypes.number, color: PropTypes.string, weight: PropTypes.string, }), }; BubbleChart.defaultProps = { overflow: false, graph: { zoom: 1.1, offsetX: -0.05, offsetY: -0.01, }, width: 1000, height: 800, padding: 0, showLegend: true, legendPercentage: 20, legendFont: { family: "Arial", size: 12, color: "#000", weight: "bold", }, valueFont: { family: "Arial", size: 16, color: "#fff", weight: "bold", }, labelFont: { family: "Arial", size: 11, color: "#fff", weight: "normal", }, bubbleClickFun: (label) => { console.log(`Bubble ${label} is clicked ...`); }, legendClickFun: (label) => { console.log(`Legend ${label} is clicked ...`); }, };