Skip to main content Skip to footer

Changing the Position of the Text in the FlexChart Legend | Angular

Changing Text Position Before and After

Background:

It’s rare, but sometimes you need to change where the legend text sits in a Wijmo FlexChart. The approach to take is to hook the rendered event, grab the legend SVG, then lay out your own legend items. This article shows how to do this in Angular.

Steps to Complete:

  1. Create your chart and wire up the (rendered) event

  2. Get the legend rect attributes

  3. Create legend items

  4. Position the legend items

  5. Set the legend sizing

Getting Started:

Use the chart’s rendered event to reposition the rect and text elements inside the legend. Rects are SVG rectangles.

Create your chart and wire up the (rendered) event

<!-- app.component.html -->
<wj-flex-chart
  #chart
  [itemsSource]="data"
  bindingName="name"
  binding="value"
  chartType="Pie"
  (rendered)="onRendered(chart, $event)">
  <wj-flex-chart-legend position="Right"></wj-flex-chart-legend>
</wj-flex-chart>

 

Get the legend rect attributes

const legendRectAttrs = ['x', 'y', 'width', 'height'] as const;
legendRectAttrs.forEach(attr => rects[0]?.setAttribute(attr, legend.getAttribute(attr) || '0'));

 

Create legend items

   const itemsSourceMap = (s.itemsSource || []).reduce((acc: Record<string, any>, item: any) => {
      acc[item[s.bindingName]] = item;
      return acc;
    }, {});

    const legendItems = texts
      .map((legendTextLabel, i) => {
        const item = itemsSourceMap[legendTextLabel.textContent || ''];
        if (!item) return null;

        const val = `${wjcCore.Globalize.format((+item[s.binding] || 0) / (sum || 1), 'p1')}`;
        const valueTextLabel = legendTextLabel.cloneNode() as SVGTextElement;
        valueTextLabel.classList.add('wj-value-label');
        valueTextLabel.textContent = val;

        const legendItem = document.createElementNS('http://www.w3.org/2000/svg', 'g');
        legendItem.classList.add('wj-legendItem');
        legendItem.setAttribute('fill', 'transparent');

        // IMPORTANT: keep order: label, color rect, value label
        legendItem.appendChild(legendTextLabel);
        if (rects[i + 1]) legendItem.appendChild(rects[i + 1]);
        legendItem.appendChild(valueTextLabel);

        legend.appendChild(legendItem);
        return legendItem;
      })
      .filter(Boolean) as SVGGElement[];

 

Position the legend items

    const legendX = parseFloat(legend.getAttribute('x') || '0');
    const legendY = parseFloat(legend.getAttribute('y') || '0');
    const legendWidth = parseFloat(legend.getAttribute('width') || '0');
    const legendHeight = parseFloat(legend.getAttribute('height') || '0');

    const columnWidth =
      (legendWidth - this.padding.paddingLeft - this.padding.paddingRight) / this.legendColumnCount;

    const rows = Math.ceil((legendItems.length || 1) / this.legendColumnCount);
    const rowHeight = rows ? legendHeight / rows : legendHeight;

    legendItems.forEach((item, i) => {
      const col = i % this.legendColumnCount;
      const row = Math.floor(i / this.legendColumnCount);

      const x = legendX + this.padding.paddingLeft + col * columnWidth;
      const y = legendY + row * rowHeight - this.padding.paddingTop - this.padding.paddingBottom;

      const textLabel = item.querySelector('text.wj-label') as SVGTextElement || item.querySelector('text') as SVGTextElement;
      const valueLabel = item.querySelector('text.wj-value-label') as SVGTextElement;
      const colorRect = item.querySelector('rect') as SVGRectElement;

      item.setAttribute('x', String(x));
      item.setAttribute('y', String(y));
      if (colorRect) {
        colorRect.setAttribute('x', String(x));
        colorRect.setAttribute('y', String(y));
      }

      if (textLabel && colorRect) {
        const cw = parseFloat(colorRect.getAttribute('width') || '0');
        const ch = parseFloat(colorRect.getAttribute('height') || '0');
        textLabel.setAttribute('x', String(x + 10 + cw)); // 10px spacing
        textLabel.setAttribute('y', String(y + ch));
      }

      if (valueLabel) {
        // valueText right-justified to the column end
        const valueLabelWidth = valueLabel.getBoundingClientRect().width || 0;
        valueLabel.setAttribute('x', String(legendX + (col + 1) * columnWidth - valueLabelWidth));
        if (colorRect) {
          const ch = parseFloat(colorRect.getAttribute('height') || '0');
          valueLabel.setAttribute('y', String(y + ch));
        }
      }
    });
  }

 

Set the legend sizing

 private setLegendMaximumSize(host: HTMLElement, padding: { paddingTop: number; paddingRight: number; paddingBottom: number; paddingLeft: number; }) {
    const hostEleRect = host.getBoundingClientRect();
    const hostRect = (host.querySelector('svg') as SVGElement)?.getBoundingClientRect();
    const footerRect = (host.querySelector('.wj-footer') as HTMLElement)?.getBoundingClientRect();
    // For pie/donut rendering group:
    const plotRect = (host.querySelector('.wj-slice-group') as SVGGElement)?.getBoundingClientRect();

    if (!hostRect || !plotRect) return;

    const availableWidth = hostRect.width - padding.paddingLeft - padding.paddingRight;
    const availableHeight = Math.max(
      0,
      ((footerRect?.top ?? hostRect.bottom) - plotRect.bottom) - padding.paddingTop - padding.paddingBottom
    );

    const legend = host.querySelector('.wj-legend') as SVGGElement;
    if (!legend) return;

    legend.setAttribute('x', String(padding.paddingLeft));
    legend.setAttribute('y', String(plotRect.bottom - hostEleRect.top));
    legend.setAttribute('width', String(availableWidth));
    legend.setAttribute('height', String(availableHeight));
  }

  private getMaxLabelWidth(chart: any, engine: any, labelElements: Element[], sum: number) {
    if (!engine || !labelElements?.length) return 0;
    return Math.max(
      ...Array.from(labelElements).map((label, i) => {
        const name = (chart.itemsSource?.[i]?.[chart.bindingName]) ?? '';
        const frac = (chart.itemsSource?.[i]?.[chart.binding] ?? 0) / (sum || 1);
        const val = `${name} ${wjcCore.Globalize.format(frac, 'p1')}`;
        return engine.measureString(val).width;
      })
    );
  }
}

 

If followed correctly, your FlexChart legend should look something like this:

FlexChart Legend's Text Repositioned

 

Adjustments will be needed depending on your applications’ needs, but this example should be a good starting point. I hope you find this article helpful.

Happy coding!

 

Combined file for reference:

// app.component.ts
import { Component } from '@angular/core';
import * as wjcCore from '@mescius/wijmo';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html'
})
export class AppComponent {
  // demo data
  data = [
    { name: 'A', value: 10 },
    { name: 'B', value: 25 },
    { name: 'C', value: 15 },
    { name: 'D', value: 50 }
  ];

  // tweak as needed
  legendColumnCount = 2;
  padding = { paddingTop: 5, paddingRight: 5, paddingBottom: 5, paddingLeft: 5 };

  onRendered(s: any, e: any) {
    const host: HTMLElement = s.hostElement as HTMLElement;
    const legend: SVGGElement | null = host.querySelector('.wj-legend');
    if (!legend) return;

    const texts = Array.from(legend.querySelectorAll('text'));
    const rects = Array.from(legend.querySelectorAll('rect'));

    // compute total once
    const sum = (s.itemsSource || []).reduce((acc: number, it: any) => acc + (+it[s.binding] || 0), 0);

    // Get max width (optional but useful for sizing)
    const maxWidth = this.getMaxLabelWidth(s, e?.engine, texts, sum);

    // Set legend max size based on plot/footer space
    this.setLegendMaximumSize(host, this.padding);

    // 2) Get the legend rect attributes (sync outer rect with legend group)
    const legendRectAttrs = ['x', 'y', 'width', 'height'] as const;
    legendRectAttrs.forEach(attr => rects[0]?.setAttribute(attr, legend.getAttribute(attr) || '0'));

    // 3) Create legend items
    const itemsSourceMap = (s.itemsSource || []).reduce((acc: Record<string, any>, item: any) => {
      acc[item[s.bindingName]] = item;
      return acc;
    }, {});

    const legendItems = texts
      .map((legendTextLabel, i) => {
        const item = itemsSourceMap[legendTextLabel.textContent || ''];
        if (!item) return null;

        const val = `${wjcCore.Globalize.format((+item[s.binding] || 0) / (sum || 1), 'p1')}`;
        const valueTextLabel = legendTextLabel.cloneNode() as SVGTextElement;
        valueTextLabel.classList.add('wj-value-label');
        valueTextLabel.textContent = val;

        const legendItem = document.createElementNS('http://www.w3.org/2000/svg', 'g');
        legendItem.classList.add('wj-legendItem');
        legendItem.setAttribute('fill', 'transparent');

        // IMPORTANT: keep order: label, color rect, value label
        legendItem.appendChild(legendTextLabel);
        if (rects[i + 1]) legendItem.appendChild(rects[i + 1]);
        legendItem.appendChild(valueTextLabel);

        legend.appendChild(legendItem);
        return legendItem;
      })
      .filter(Boolean) as SVGGElement[];

    // 4) Position legend items
    const legendX = parseFloat(legend.getAttribute('x') || '0');
    const legendY = parseFloat(legend.getAttribute('y') || '0');
    const legendWidth = parseFloat(legend.getAttribute('width') || '0');
    const legendHeight = parseFloat(legend.getAttribute('height') || '0');

    const columnWidth =
      (legendWidth - this.padding.paddingLeft - this.padding.paddingRight) / this.legendColumnCount;

    const rows = Math.ceil((legendItems.length || 1) / this.legendColumnCount);
    const rowHeight = rows ? legendHeight / rows : legendHeight;

    legendItems.forEach((item, i) => {
      const col = i % this.legendColumnCount;
      const row = Math.floor(i / this.legendColumnCount);

      const x = legendX + this.padding.paddingLeft + col * columnWidth;
      const y = legendY + row * rowHeight - this.padding.paddingTop - this.padding.paddingBottom;

      const textLabel = item.querySelector('text.wj-label') as SVGTextElement || item.querySelector('text') as SVGTextElement;
      const valueLabel = item.querySelector('text.wj-value-label') as SVGTextElement;
      const colorRect = item.querySelector('rect') as SVGRectElement;

      item.setAttribute('x', String(x));
      item.setAttribute('y', String(y));
      if (colorRect) {
        colorRect.setAttribute('x', String(x));
        colorRect.setAttribute('y', String(y));
      }

      if (textLabel && colorRect) {
        const cw = parseFloat(colorRect.getAttribute('width') || '0');
        const ch = parseFloat(colorRect.getAttribute('height') || '0');
        textLabel.setAttribute('x', String(x + 10 + cw)); // 10px spacing
        textLabel.setAttribute('y', String(y + ch));
      }

      if (valueLabel) {
        // valueText right-justified to the column end
        const valueLabelWidth = valueLabel.getBoundingClientRect().width || 0;
        valueLabel.setAttribute('x', String(legendX + (col + 1) * columnWidth - valueLabelWidth));
        if (colorRect) {
          const ch = parseFloat(colorRect.getAttribute('height') || '0');
          valueLabel.setAttribute('y', String(y + ch));
        }
      }
    });
  }

  // 5) Set the legend sizing
  private setLegendMaximumSize(host: HTMLElement, padding: { paddingTop: number; paddingRight: number; paddingBottom: number; paddingLeft: number; }) {
    const hostEleRect = host.getBoundingClientRect();
    const hostRect = (host.querySelector('svg') as SVGElement)?.getBoundingClientRect();
    const footerRect = (host.querySelector('.wj-footer') as HTMLElement)?.getBoundingClientRect();
    // For pie/donut rendering group:
    const plotRect = (host.querySelector('.wj-slice-group') as SVGGElement)?.getBoundingClientRect();

    if (!hostRect || !plotRect) return;

    const availableWidth = hostRect.width - padding.paddingLeft - padding.paddingRight;
    const availableHeight = Math.max(
      0,
      ((footerRect?.top ?? hostRect.bottom) - plotRect.bottom) - padding.paddingTop - padding.paddingBottom
    );

    const legend = host.querySelector('.wj-legend') as SVGGElement;
    if (!legend) return;

    legend.setAttribute('x', String(padding.paddingLeft));
    legend.setAttribute('y', String(plotRect.bottom - hostEleRect.top));
    legend.setAttribute('width', String(availableWidth));
    legend.setAttribute('height', String(availableHeight));
  }

  private getMaxLabelWidth(chart: any, engine: any, labelElements: Element[], sum: number) {
    if (!engine || !labelElements?.length) return 0;
    return Math.max(
      ...Array.from(labelElements).map((label, i) => {
        const name = (chart.itemsSource?.[i]?.[chart.bindingName]) ?? '';
        const frac = (chart.itemsSource?.[i]?.[chart.binding] ?? 0) / (sum || 1);
        const val = `${name} ${wjcCore.Globalize.format(frac, 'p1')}`;
        return engine.measureString(val).width;
      })
    );
  }
}

Andrew Peterson

Technical Engagement Engineer