I am trying to fix an issue in which there is a D3 donut chart with a legend located just to the right. The text of the legend keeps being cutoff. It's either visible outside of the container, or it's not displayed outside. Either way, it doesn't fit within the container, even though I can see that both the legend and the donut chart are part of the same SVG. You can see what I'm referring to in this image:
https://i.stack.imgur.com/bkw1W.jpg
I am very new to working with D3, but I've been stuck on this issue for a while now. This isn't my code that I'm trying to fix, but here is where the options for generating the SVG are being passed in:
const donutOptions: DonutChartOptions = {
showPercentageInDonut: false,
width: 500,
height: 260,
title: {text: '', textStyle: {fontSize: '14px'}},
margin: {top: 120, right: 10, bottom: 65, left: 100},
colors: [Theme.Emerald.hex, Theme.Lime.hex, Theme.Teal.hex, Theme.SkyBlue.hex,
Theme.Marigold.hex, Theme.Azure.hex, Theme.Red.hex, Theme.Orange.hex]
};
const legendTextWrapWidthEdge = 1440;
const donutEthnicityOptions: DonutChartOptionsExtended = {
showPercentageInDonut: false,
width: 470,
height: 260,
title: {text: '', textStyle: {fontSize: '14px'}},
margin: {top: 120, right: 10, bottom: 65, left: 85},
colors: [Theme.Emerald.hex, Theme.Lime.hex, Theme.Teal.hex, Theme.SkyBlue.hex,
Theme.Marigold.hex, Theme.Azure.hex, Theme.Red.hex, Theme.Orange.hex],
legend: {textStyle: {fontSize: '14px'}},
legendOptions: {
legendRectVSpace: 10,
legendPositionX: 110,
legendPositionY: 85,
legendPercentagePositionX: 46,
legendPercentagePositionY: 15,
legendTextPositionX: 20,
legendTextWidth: (!!this.browserScreenWidth && this.browserScreenWidth < legendTextWrapWidthEdge) ? 100 : 200
}
};
I have tried experimenting with viewBox and preserveAspectRatio attributes, but I am apparently not doing something correctly.
This is the code that actually creates the chart using the aforementioned options. Messing with this code is kind of a last resort option if it can be avoided though. I think of it as a black box that I am providing merely for context:
import { DataSet } from '../data-set';
import { DonutChartOptionsExtended, ILegendOptions } from '../interfaces/DonutChartOptionsExtended';
import { XYChartSettings } from '../interfaces/XYChartSettings';
import { DEFAULTS } from '../interfaces/DEFAULTS';
import { TextStyle } from '../interfaces/TextStyle';
import Defaults from 'lodash-es/defaults';
import { getColorScale, initializeSvg, wrapText } from '../d3-fns';
export class DonutChart {
options: any;
dataset: any;
draw(dataSet?: DataSet, options?: DonutChartOptionsExtended) {
Promise.all([
import(/* webpackChunkName: "d3" */ 'd3-shape'),
import(/* webpackChunkName: "d3" */ 'd3-interpolate'),
import(/* webpackChunkName: "d3" */ 'd3-selection'),
import(/* webpackChunkName: "d3" */ 'd3-scale'),
import(/* webpackChunkName: "d3" */ 'd3-transition')
]).then(([d3Shape, d3Interpolate, d3Select, d3Scale, trans]) => {
if (dataSet) {
this.dataset = dataSet;
}
if (options) {
this.options = options;
}
const pie = d3Shape.pie()
.value((d: any) => d.value)
.sort(null)
.padAngle(.03);
const width = this.options.width;
const outerRadius = (width - 300) / 2;
const innerRadius = outerRadius / 2;
const arc = d3Shape.arc()
.outerRadius(outerRadius)
.innerRadius(innerRadius);
const settings: any = new XYChartSettings(this.options);
const svg = initializeSvg(d3Select, settings);
const color = getColorScale(d3Scale, settings.colors);
const dataRows = this.dataset.dataRows;
const path = svg.selectAll('path')
.data(pie(dataRows as any))
.enter()
.append('path')
.attr('d', (d: any, i: number, groups: any[]) => arc(d))
.attr('fill', (d: any, i: number, groups: any[]) => String(color(String(d.data.label))));
if (options && options.showPieAnimation) {
path.transition()
.duration(1000)
.attrTween('d', function (d: any) {
const interpolate = d3Interpolate.interpolate({startAngle: 0, endAngle: 0}, d);
return function (t: any) {
return arc(interpolate(t));
};
});
}
const restOfTheData = (mydata: any) => {
try {
const legendOptions: ILegendOptions = this.options.legendOptions;
const legendRectSize = !!legendOptions && legendOptions.legendRectHeight ? legendOptions.legendRectHeight : 20;
const legendSpacing = !!legendOptions && legendOptions.legendRectVSpace ? legendOptions.legendRectVSpace : 7;
const legendHeight = legendRectSize + legendSpacing;
const positionx = !!legendOptions && legendOptions.legendPositionX ? legendOptions.legendPositionX : 115;
const positiony = !!legendOptions && legendOptions.legendPositionY ? legendOptions.legendPositionY : 65;
if (options && options.showPercentageInDonut) {
this.displayPercentageOnThePie(mydata, svg, pie, arc);
}
const defaultColor = getColorScale(d3Scale, settings.colors);
if (this.options.colors) {
this.displayPercentageNextToLegend(
mydata, svg, defaultColor, positionx,
positiony, legendHeight,
settings.legend.textStyle.fontSize || '14px'
);
this.displayLengend(
d3Select, mydata, svg, defaultColor, legendHeight,
positionx, positiony, legendRectSize,
settings.legend.textStyle.fontSize || '14px'
);
} else {
this.displayPercentageNextToLegendDefault(
mydata, svg, positionx, positiony, legendHeight,
settings.legend.textStyle.fontSize || '14px'
);
this.displayLengendDefault(
svg, defaultColor, legendHeight,
positionx, positiony, legendRectSize,
settings.legend.textStyle.fontSize || '14px'
);
}
this.displayTitle(svg, settings);
} catch (ex) {
console.log(ex);
}
};
setTimeout(restOfTheData(dataRows), 1000);
})
}
private displayPercentageOnThePie(mydata: any, svg: any, pie: any, arc: any) {
svg.selectAll('text')
.data(pie(mydata))
.enter()
.append('text')
.transition()
.duration(200)
.attr('transform', (d: any, i: number, groups: any[]) => 'translate(' + arc.centroid(d) + ')')
.attr('dy', '.4em')
.attr('text-anchor', 'middle')
.text((d: any) => d.data.value + '%')
.style('fill', '#fff')
.style('font-size', '10px');
}
private displayPercentageNextToLegend(
mydata: any, svg: any, defaultColor: any, positionX: any,
positionY: any, legendHeight: any, fontSize: any) {
svg.selectAll('.percentage')
.data(mydata)
.enter().append('g')
.attr('class', 'percentage')
.attr('transform', (d: any, i: number, groups: any[]) => 'translate(' + (positionX + 40) +
',' + ((i * legendHeight) - positionY) + ')')
.append('text')
.style('fill', (d: any, i: number, groups: any[]) => defaultColor(i))
.style('text-anchor', 'end')
.style('font-size', fontSize)
.text((d: any) => d.value + '%');
}
private displayPercentageNextToLegendDefault(mydata: any, svg: any, positionX: any, positionY: any, legendHeight: any, fontSize: any) {
svg.selectAll('.percentage')
.data(mydata)
.enter().append('g')
.attr('class', 'percentage')
.attr('transform', (d: any, i: number, groups: any[]) => 'translate(' + (positionX + 40) +
',' + ((i * legendHeight) - positionY) + ')')
.append('text')
.style('fill', '#000')
.style('text-anchor', 'end')
.style('font-size', fontSize)
.text((d: any) => d.value + '%');
}
private displayLengend(d3Select: any, mydata: any, svg: any, defaultColor: any, legendHeight: any,
positionX: any, positionY: any, legendRectSize: any, fontSize: any) {
const legendOptions: ILegendOptions = this.options.legendOptions;
const legendRectWidth = !!legendOptions && legendOptions.legendRectWidth ? legendOptions.legendRectWidth : 10;
const percentageOffsetX = !!legendOptions && legendOptions.legendPercentagePositionX ? legendOptions.legendPercentagePositionX : 56;
const percentageOffsetY = !!legendOptions && legendOptions.legendPercentagePositionY ? legendOptions.legendPercentagePositionY : 15;
const textOffsetX = !!legendOptions && legendOptions.legendTextPositionX ? legendOptions.legendTextPositionX : 30;
const textOffsetY = !!legendOptions && legendOptions.legendTextPositionY ? legendOptions.legendTextPositionY : 15;
const textWidth = !!legendOptions && legendOptions.legendTextWidth ? legendOptions.legendTextWidth : 200;
const legend = svg.selectAll('.legend')
.data(mydata)
.enter()
.append('g')
.attr('class', 'legend')
// Just a calculation for x & y position
.attr('transform',
(d: any, i: number, groups: any[]) => `translate(${positionX
+ percentageOffsetX},${(i * legendHeight) - (positionY + percentageOffsetY)})`);
legend.append('rect')
.attr('width', legendRectWidth)
.attr('height', legendRectSize)
.attr('rx', 1)
.attr('ry', 1)
.style('fill', (d: any, i: number, groups: any[]) => defaultColor(i))
.style('stroke', (d: any, i: number, groups: any[]) => defaultColor(i));
legend.append('text')
.attr('x', textOffsetX)
.attr('y', textOffsetY)
.text((d: any) => d.label)
.style('fill', '#000')
.style('font-size', fontSize)
.call(wrapText, d3Select, textWidth);
}
private displayLengendDefault(svg: any, defaultColor: any, legendHeight: any,
positionX: any, positionY: any, legendRectSize: any, fontSize: any) {
const legendRectWidth = 10;
const legend = svg.selectAll('.legend')
.data(defaultColor.domain())
.enter()
.append('g')
.attr('class', 'legend')
// Just a calculation for x & y position
.attr('transform', (d: any, i: number, groups: any[]) => 'translate(' + (positionX + 50) +
',' + ((i * legendHeight) - (positionY + 15)) + ')');
legend.append('rect')
.attr('width', legendRectWidth)
.attr('height', legendRectSize)
.attr('rx', 1)
.attr('ry', 1)
.style('fill', defaultColor)
.style('stroke', defaultColor);
legend.append('text')
.attr('x', 30)
.attr('y', 15)
.text((d: any) => d)
.style('fill', '#929DAF')
.style('font-size', fontSize);
}
private displayTitle(svg: any, settings: any) {
const textStyle = <TextStyle>Defaults(settings.title.textStyle || {}, DEFAULTS.textStyleTitle);
svg.append('text')
.attr('x', settings.widthInner / 2)
.attr('y', 0 - (settings.margin.top / 1.15 ))
.attr('text-anchor', 'middle')
.style('font-size', textStyle.fontSize)
.style('text-decoration', textStyle.textDecoration)
.text(settings.title.text);
}
}