0
votes

How can I make overlapping parts in a d3.js radar chart transparent.

The radar chart blobs below are created with d3.js. The space where red and green intersect should be made transparent (This will serve for a before <-> after comparison). The snippet below the chart contains the code for creating the radar chart. I didn't include the 5 axes for simplicity:

enter image description here

/* Original radar chart design created by Nadieh Bremer - VisualCinnamon.com */
			
	////////////////////////// Data ////////////////////////////// 

	var data = [
				[//Before - red
				{axis:"First",value:0.49},
				{axis:"Banana",value:0.79},
				{axis:"Scratch",value:0.70},
				{axis:"Wheelie",value:0.09},
				{axis:"Pantalon",value:0.37},
				],[//After - green
				{axis:"First",value:0.64},
				{axis:"Banana",value:0.44},
				{axis:"Scratch",value:0.37},
				{axis:"Wheelie",value:0.84},
				{axis:"Pantalon",value:0.81},
				]
			];
      
	//////////////////////// Setup //////////////////////////////// 

	var margin = {top: 50, right: 50, bottom: 50, left: 50},
      width = 300,
		  height = 300;
	var color = d3.scale.ordinal().range(["#CC333F","#53e87d"]);
	var radarChartOptions = {
		w: width,				//Width of the circle
		h: height,				//Height of the circle
		margin: margin, 		//The margins of the SVG
		opacityArea: 0.7, 	//The opacity of the area of the blob
		color: color	//Color function
	};

	//////////////////////// RadarChart ///////////////////////////
  
	RadarChart("#radarChart1", data, radarChartOptions);

/////////////////////////////////////////////////////////
/////////////// The Radar Chart Function ////////////////
/////////////// Written by Nadieh Bremer ////////////////
////////////////// VisualCinnamon.com ///////////////////
/////////// Inspired by the code of alangrafu ///////////
/////////////////////////////////////////////////////////

function RadarChart(id, data, options) {
	
	/////////////////////////////////////////////////////////
	///////////////     Prep                 ////////////////
  /////////////////////////////////////////////////////////

	var cfg = radarChartOptions;
	var allAxis = (data[0].map(function(i, j){return i.axis})),	//Names of each axis
		  total = allAxis.length,					            //The number of different axes
		  radius = Math.min(cfg.w/2, cfg.h/2), 	      //Radius of the outermost circle
		  Format = d3.format('%'),			              //Percentage formatting
		  angleSlice = Math.PI * 2 / total;	          //The width in radians of each "slice"
	var rScale = d3.scale.linear()                  //Scale for the radius
		           .range([0, radius]);
		

	//////////// Create the container SVG and g /////////////
  
	var svg = d3.select(id).append("svg") 	//Initiate the radar chart SVG
			.attr("width",  cfg.w + cfg.margin.left + cfg.margin.right)
			.attr("height", cfg.h + cfg.margin.top + cfg.margin.bottom)
			.attr("class", "radar"+id);
	var g = svg.append("g") 	//Append a g element	
			.attr("transform", "translate(" + (cfg.w/2 + cfg.margin.left) + "," + (cfg.h/2 + cfg.margin.top) + ")");
			


	/////////////////////////////////////////////////////////
	///////////// Draw the radar chart blobs ////////////////
	/////////////////////////////////////////////////////////
	
	//The radial line function
	var radarLine = d3.svg.line.radial()
		.interpolate("cardinal-closed")
		.radius(function(d) { return rScale(d.value); })
		.angle(function(d,i) {	return i*angleSlice; });


	//Create a wrapper for the blobs	
	var blobWrapper = g.selectAll(".radarWrapper")
		.data(data)
		.enter().append("g")
		.attr("class", "radarWrapper");
			
	//Append the backgrounds	
	blobWrapper
		.append("path")
		.attr("class", "radarArea")
		.attr("d", function(d,i) { return radarLine(d); })
		.style("fill", function(d,i) { return cfg.color(i);})
		.style("fill-opacity", cfg.opacityArea);
	
	
}//RadarChart function
<script src="https://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<!-- Radar Chart -->
<div class="radar-chart" id="radarChart1"></div>

I found that in HTML5-Canvas overlapping elements can be made transparent (1). It can also be used to bind d3 nodes to it (2) and they could made transparent this way. However, this seems very complicated for more complex objects like the above radar-chart blobs. Is there an easy solution to do this in d3? (There seems to be one in chart.js (3))

Addendum - What I already found:

  1. Overlapping (non-d3) elements can be made transparent using HTML5-Canvas by setting the globalCompositeOperation mode to 'xor':

For more about compositing visit https://www.sarasoueidan.com/blog/compositing-and-blending-in-css/

var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');

ctx.globalCompositeOperation = 'xor';

// WITHOUT overlap:
ctx.fillStyle = 'blue';
ctx.fillRect(10, 10, 100, 100);

ctx.fillStyle = 'red';
ctx.fillRect(110, 50, 100, 100);

// WITH (transparent) overlap:
ctx.fillStyle = 'blue';
ctx.fillRect(250, 10, 100, 100);

ctx.fillStyle = 'red';
ctx.fillRect(300, 50, 100, 100);
<!-- Original from https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation -->

<style>
  body {
    background-image: url("http://www.scri8e.com/stars/PNG_Clouds/zc06.png?filename=./zc06.png");
}
</style>

<canvas id="canvas" width="400" height="200" ></canvas>
  1. d3.js nodes can also be visualized by HTML5-Canvas by using getContext("2d"). Overlaps can be implemented the same way as above (1). However, to my understanding this requires to recreate each element in Javascript and then bind the d3 data on it. For example for circles, arc elements have to be recreated in the HTML5-Canvas context as in this example by Mike Bostock:

    context = canvas.getContext("2d"); ...
    
    context.arc(element.getAttribute("x"), element.getAttribute("y"), element.getAttribute("radius"), 0, 2 * Math.PI);
    
  2. Here is a possible implementation in chartjs (see picture below)

enter image description here

1

1 Answers

1
votes

Your description implies you will always have two charts ("before -> after").

This solution uses SVG masks. While it would be doable with more charts, it gets more complicated. You would have to construct the <mask> elements dynamically.

/* Original radar chart design created by Nadieh Bremer - VisualCinnamon.com */
        
////////////////////////// Data ////////////////////////////// 

var data = [
    [//Before - red
    {axis:"First",value:0.49},
    {axis:"Banana",value:0.79},
    {axis:"Scratch",value:0.70},
    {axis:"Wheelie",value:0.09},
    {axis:"Pantalon",value:0.37},
    ],[//After - green
    {axis:"First",value:0.64},
    {axis:"Banana",value:0.44},
    {axis:"Scratch",value:0.37},
    {axis:"Wheelie",value:0.84},
    {axis:"Pantalon",value:0.81},
    ]
];
      
//////////////////////// Setup //////////////////////////////// 

var margin = {top: 50, right: 50, bottom: 50, left: 50},
    width = 300,
    height = 300;
var color = d3.scale.ordinal().range(["#CC333F","#53e87d"]);
var radarChartOptions = {
    w: width,                //Width of the circle
    h: height,                //Height of the circle
    margin: margin,         //The margins of the SVG
    opacityArea: 0.7,     //The opacity of the area of the blob
    color: color    //Color function
};

//////////////////////// RadarChart ///////////////////////////
  
RadarChart("radarChart", data, radarChartOptions);

function RadarChart(id, data, options) {

    var cfg = radarChartOptions;
    var allAxis = (data[0].map(function(i, j){return i.axis})),    //Names of each axis
        total = allAxis.length,                                //The number of different axes
        radius = Math.min(cfg.w/2, cfg.h/2),           //Radius of the outermost circle
        Format = d3.format('%'),                          //Percentage formatting
        angleSlice = Math.PI * 2 / total;              //The width in radians of each "slice"
    var rScale = d3.scale.linear()                  //Scale for the radius
       .range([0, radius]);
  
    var svg = d3.select('#' + id)     //Initiate the radar chart SVG
        .attr("width",  cfg.w + cfg.margin.left + cfg.margin.right)
        .attr("height", cfg.h + cfg.margin.top + cfg.margin.bottom)
        .attr("class", "radar"+id);

    svg.select('#bg').attr('r', radius);

    var dx = cfg.w/2 + cfg.margin.left,
        dy = cfg.h/2 + cfg.margin.top;

    svg.selectAll('mask')
        .attr('x', -dx)
        .attr('y', -dy);

    var source = svg.select('#radarSource')
    var wrapper = svg.select('#radarWrapper')
        .attr("transform", "translate(" + dx + "," + dy + ")");

    //The radial line function
    var radarLine = d3.svg.line.radial()
        .interpolate("cardinal-closed")
        .radius(function(d) { return rScale(d.value); })
        .angle(function(d,i) {    return i*angleSlice; });


    //Draw the blobs    
    source.selectAll("path")
        .data(data)
        .enter().append("path")
        .attr("id", function(d,i) {return 'area' + i;})
        .attr("d", function(d,i) { return radarLine(d); });

    //render and apply masks
    wrapper.selectAll("use")
        .data(data)
        .enter().append("use")
        .attr("xlink:href", function(d,i) {return '#area' + i;})
        .attr("mask", function(d,i) {return 'url(#mask' + i + ')';})
        .style("fill", function(d,i) { return cfg.color(i);})
        .style("fill-opacity", cfg.opacityArea);


}//RadarChart function
<script src="https://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<!-- Radar Chart -->
<svg id="radarChart" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
  <defs>
    <circle id="bg" />
    <g id="radarSource" />
    <mask id="mask0" maskUnits="userSpaceOnUse">
      <use xlink:href="#bg" fill="white" />
      <use xlink:href="#area1" fill="black" />
    </mask>
    <mask id="mask1" maskUnits="userSpaceOnUse">
      <use xlink:href="#bg" fill="white" />
      <use xlink:href="#area0" fill="black" />
    </mask>
  </defs>
  <g id="radarWrapper" />
</svg>