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.