2

I realize this is somewhat of a repeat question. However, I think it bears asking again since it appears that D3 has changed a bit since most of the previous questions and answers and also because I can't seem to get any of the existing solutions I found to work for my purposes.

I'm also willing to concede that I'm new to D3 and there's probably some level of user error involved.

I've got a radial chart with what will be a variable number of items in the chart anywhere from 10-20 on average.

The nature of the data means that the items will often overlap a bit or at the very least come into close contact with each other.

The items are further complicated by the fact that they're not exactly uniform dimensions since they consist of points and arrows of 3 different lengths.

I've got a JSFiddle of a rough example of the chart here:

https://jsfiddle.net/methnen/0do73xf6/

Here's same code as the JSFiddle:

<div id="m-chart-container-1175-1" class="m-chart-container">
    <div id="m-chart-1175-1" class="m-chart">
        <?xml version="1.0" encoding="utf-8"?>
        <svg viewBox="0 0 1229.36 1108" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
        </svg>
    </div>
</div>
<script>
data = {
    chart_args: {
        "max": 93.69369369369369,
        "min": 65.46546546546547,
        "data": [
            ["Jamie", 94, 0, "1", "10", "top"],
            ["Geoffrey", 82, 45, "3", "5", "top"],
            ["Sally", 80, 207, "2", "3", "bottom"],
            ["Joseph", 78, 54, "2", "45", "top"],
            ["Xinmin", 76, 63, "1", "-3", "top"],
            ["Nianqing", 79, -27, "2", "2", "top"],
            ["George", 66, 234, "2", "4", "bottom"],
            ["Samuel", 83, 126, "3", "7", "bottom"],
            ["Rachel", 79, 162, "3", "5", "bottom"],
            ["Jennifer", 89, 153, "2", "-4", "bottom"],
            ["Patricia", 76, 45, "2", "4", "top"]
        ]
    },
    post_id: 1175,
    instance: 1
};

var m_chart_d3_radar = {};

(function( $ ) {
    'use strict';

    // Start things up
    m_chart_d3_radar.init = function( chart_args, post_id, instance ) {
        this.post_id    = post_id;
        this.instance   = instance;
        this.chart_args = chart_args;

        // The actual dimensions of the SVG
        this.width  = 1229;
        this.height = 1108;

        // If the SVG doesn't have a centered chart we need to center d3's chart area in relation to it
        this.vertical   = 0;
        this.horizontal = -13;

        // The radius needs to fit the radius of the chart in the SVG so we adjust that here
        this.radius_modifier = -90;

        // The SVG design doesn't account for items in the actual center or right on the edge so we handle that here
        this.center = 85;
        this.outer  = 15;

        // Size of the chart points
        this.point_size = 13;

        // The colors used for the three point/arrow types
        this.point_colors = [
            'red',
            'blue',
            'green'
        ];

        // The lengths for the three arrow types
        this.arrow_lengths = [
            '21',
            '38',
            '53'
        ];

        this.render_chart();
    };

    m_chart_d3_radar.render_chart = function( cx, cy, ex, ey ) {
        // Calculate the radius of the chart area
        let radius = ( Math.min(this.width, this.height) / 2 ) + this.radius_modifier;

        // Sale the weighted score values against the chart area
        let r = d3.scaleLinear()
            .domain([this.chart_args.max, this.chart_args.min])
            .range([0 + this.center, radius - this.outer]);

        // Plot the point positions
        let line = d3.lineRadial()
            .radius(function(d) {
                return r(d[1]);
            })
            .angle(function(d) {
                return d[2] * ( Math.PI / 180 );
            });

        // Calculate the position of the chart area
        let horizontal = ( this.width / 2 ) + this.horizontal;
        let vertical   = ( this.height / 2 ) + this.vertical;

        this.svg = d3.select( '#m-chart-' + this.post_id + '-' + this.instance + ' svg' )
            .append('g')
            .attr( 'transform', 'translate(' + horizontal + ',' + vertical + ')');

        this.draw_grid_lines( r, radius );

        // Assign the colors and lengths for use later
        let colors  = this.point_colors;
        let lengths = this.arrow_lengths;

        // Build an array of labels and where they are anchored to
        let label_array = [];

        // Plot and draw all of the companies to the chart area
        let items = this.svg.selectAll( 'item' )
            .data( this.chart_args.data )
            .enter()
            .append( 'g' )
            .attr( 'class', 'item' );

        items
            // Add our square point
            .append( 'rect' )
                .attr( 'height', this.point_size )
                .attr( 'width', this.point_size )
                .attr( 'fill', function(d, i) {
                    return colors[ [d][0][3] - 1 ];
                })
                // Revert back to the item and draw the line
            .select(function() { return this.parentNode; })
            .append( 'line' )
                .attr( 'stroke-width', 4 )
                .attr( 'stroke', function(d) {
                    return colors[ [d][0][3] - 1 ];
                })
                .attr( 'y1', this.point_size / 2 )
                .attr( 'y2', this.point_size / 2 )
                .attr( 'x1', this.point_size - 1 )
                .attr( 'x2', function( d ) {
                    return lengths[[d][0][3] - 1];
                })
            // Revert back to the item and draw the arrow
            .select(function() { return this.parentNode; })
            .append( 'polygon' )
                .attr( 'points', '0,0 0,15 21,7.5' )
                .attr('fill',function(d, i){
                    return colors[ [d][0][3] - 1 ];
                })
                .attr( 'transform', function(d) {
                    return 'translate(' + ( parseInt( lengths[ [d][0][3] - 1 ] ) - 1 ) + ', -1)';
                })
            // Revert back to the item and position and point (rotate) the item as needed
            .select(function() { return this.parentNode; })
            .attr( 'transform', function(d) {
                // Position
                let translate = 'translate(' + line([d]).slice(1).slice(0, -1) + ')';

                // Rotation
                var coordinates = line([d]).slice(1).slice(0, -1).split(',');
                var angle = m_chart_d3_radar.get_angle(coordinates[0], coordinates[1], 0, 0);
                    angle = angle + parseInt( [d][0][4] );

                let rotate = 'rotate(' + angle + ', ' + ( m_chart_d3_radar.point_size / 2 ) + ', ' + ( m_chart_d3_radar.point_size / 2 ) + ')';

                return translate + ' ' + rotate;
            })
            .attr( 'id', function(d) {
                var item_name = [d][0][0];
                var id = "item-" + m_chart_d3_radar.sanitize_title( item_name );
                var coordinates = line([d]).slice(1).slice(0, -1);
                var x_and_y     = line([d]).slice(1).slice(0, -1).split(',');

                let vertical_modifier = -10;

                if ( 'bottom' == [d][0][5] ) {
                    vertical_modifier = 40;
                }

                label_array.push({
                    coordinates: coordinates,
                    x: x_and_y[0],
                    y: x_and_y[1],
                    name: item_name,
                    color: colors[ [d][0][3] - 1 ],
                    placement: [d][0][5]
                });

                return id;
            });

            // Draw labels for each item
            console.log(label_array)
            
            let labels = this.svg.selectAll( '.label' )
                .data( label_array )
                .enter()
                .append( 'g' )
                .attr( 'class', 'label' )
                .append( 'text' )
                .text( function(d) {
                    return d.name;
                })
                .style( 'font-weight', 'bold' )
                .style( 'font-size', '23' )
                .style('font-family', 'proxima-nova' )
                .style( 'fill', function(d) {
                    return d.color;
                })
                // Revert back to the label and position it
                .select(function() { return this.parentNode; })
                .attr( 'transform', function(d) {
                    let vertical_modifier = -10;
            
                    if ( 'bottom' == d.placement ) {
                        vertical_modifier = 40;
                    }
            
                    let vertical   = parseInt( d.y ) + vertical_modifier;
                    let horizontal = parseInt( d.x ) - ( d3.select(this).node().getBoundingClientRect().width / 2 );
            
                    let translate = 'translate(' + horizontal + ', ' + vertical + ')';
            
                    return translate;
                });
    }

    m_chart_d3_radar.draw_grid_lines = function( r, radius ) {
        var gr = this.svg.append('g')
            .attr('class', 'r axis')
            .selectAll('g')
            .data(r.ticks())
            .enter().append('g');

        gr.append("circle")
            .attr("r", r);

        var ga = this.svg.append('g')
            .attr('class', 'a axis')
            .selectAll('g')
            .data(d3.range(0, 360, 30))
            .enter().append('g')
            .attr('transform', function(d) {
                return 'rotate(' + -d + ')';
            });

        ga.append('line')
            .attr('x2', radius);

    }

    m_chart_d3_radar.get_angle = function( cx, cy, ex, ey ) {
      var dy = ey - cy;
      var dx = ex - cx;
      var theta = Math.atan2(dy, dx); // range (-PI, PI]
      theta *= 180 / Math.PI; // rads to degs, range (-180, 180]
      //if (theta < 0) theta = 360 + theta; // range [0, 360)
      return theta;
    }

    m_chart_d3_radar.sanitize_title = function( string ) {
        string = string.toLowerCase();
        string = string.replace( /[^a-zA-Z0-9]+/g,'-' );

        return string;
    };

    $( function() {
        m_chart_d3_radar.init( data.chart_args, data.post_id, data.instance );
    } );
})( jQuery );
</script>
<style>
  .frame {
    fill: none;
    stroke: #000;
  }
  .axis text {
    font: 10px sans-serif;
  }
  .axis line,
  .axis circle {
    fill: none;
    stroke: steelblue;
    stroke-dasharray: 4;
  }
  .axis:last-of-type circle {
    stroke: steelblue;
    stroke-dasharray: none;
  }
  .line {
    fill: none;
    stroke: orange;
    stroke-width: 3px;
  }
</style>

You can see a cluster of names in the top right quarter of the chart.

The goal would be to move those labels somehow to avoid them colliding but ideally do it in a way that adapts to different data sets and different collisions.

I found lots of examples of people working this problem with different charts and a few existing D3 plugins that are supposed to deal with it, but those solutions either threw errors because of changes in D3 that occurred since or didn't seem to do anything at all that I could tell.

It seems like a forceSimulation of some kind is probably the best route to go but I'm having trouble wrapping my head around how I'd set one up given what I'm looking to do and given my chart.

Any help would be greatly appreciated.

Thanks a ton.

Jamie Poitra
  • 431
  • 4
  • 15
  • Can you share links to existing solutions implemented in older versions of D3? It would be easier to port those to the new D3 version than to reinvent the wheel. – Shreshth Mar 15 '22 at 00:45
  • This was the top result I found and I tried piecing together something from the various responses and tried both plugins mentioned and neither worked: https://stackoverflow.com/questions/17425268/d3js-automatic-labels-placement-to-avoid-overlaps-force-repulsion the examples all seem to use an older version of the "force" stuff that isn't compatible with the current version of D3. – Jamie Poitra Mar 15 '22 at 04:37

0 Answers0