0

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);
  }
}
itman312
  • 11
  • 1
  • 8
  • Is that d3 or is that billboard.js or some other library? – ksav Jan 23 '19 at 09:34
  • Welcome to stackoverflow. Please read https://stackoverflow.com/help/mcve and then edit your question. – ksav Jan 23 '19 at 09:36
  • I'm not really sure how to make the example both "minimum" and "complete", but I edited my post to provide some additional context. – itman312 Jan 23 '19 at 14:10
  • I know it's difficult to hit that sweet spot in between. But it needs to be complete so that someone else can actually run the code somewhere to replicate your problem. And it should be minimal enough so that whoever is taking the time to help you doesn't have to read through 100s of lines of irrelevant code. – ksav Jan 23 '19 at 14:17
  • At the moment, I would help you, but I don't even know how to run the code in your example. – ksav Jan 23 '19 at 14:19
  • Yeah, I can barely run it myself. Unfortunately, there really isn't a way to run this without building three node projects and linking them. I was kind of hoping that someone who is really familiar with D3 might have encountered this issue before. It seems like such a common thing that surely this problem has been encountered in the past, but I haven't yet found a solution on here. – itman312 Jan 23 '19 at 14:32
  • Maybe this can help you in the meantime: https://stackoverflow.com/questions/53421526/d3-js-legend-overlapping-chart-area/53429048 – ksav Jan 23 '19 at 14:41

0 Answers0