I'm a newbie both in JSON and js. I'm trying to load json data based on this radarchart. If I add the data just after var data = MY DATA, it works but if I load json file, it doesn't. For loading data, I follow the last suggestion entitled “What does d3.json return?” (v5) of this post--in fact the others didn't worked too--; thus, I have added <script src="https://d3js.org/d3.v5.min.js"></script> in addition to <script src="https://d3js.org/d3.v4.min.js"></script> which I used for other barplots and piecharts in the same webpage.
It works
<head>
<!-- Load d3.js -->
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-path.v1.min.js" charset="utf-8"></script>
<!-- source: https://gist.github.com/mthh/7e17b680b35b83b49f1c22a3613bd89f -->
<script src="../other/radarChart.js" charset="utf-8"></script>
</head>
<body>
<!-- a few barplots and piecharts -->
<!-- afterwards the following radarChart -->
<table>
<tr>
<td style="radarChart">
<div class="radarChart" style="display: inline-flex;"></div>
<script>
//////////////////////////////////////////////////////////////
//////////////////////// Set-Up //////////////////////////////
//////////////////////////////////////////////////////////////
var marginRadar = { top: 150, right: 80, bottom: 50, left: 80 },
widthRadar = Math.min(700, window.innerWidth / 4) - marginRadar.left - marginRadar.right,
heightRadar = Math.min(width, window.innerHeight - marginRadar.top - marginRadar.bottom);
//////////////////////////////////////////////////////////////
////////////////////////// Data //////////////////////////////
//////////////////////////////////////////////////////////////
var data = [
{
"name": "Clan A",
"axes": [
{"axis": "cognition", "value": 3.12},
{"axis": "communication","value": 9.38},
{"axis": "competition","value": 18.75},
{"axis": "consumption","value": 6.25},
{"axis": "contact","value": 21.88},
{"axis": "emotion","value": 6.25},
{"axis": "motion","value": 18.75},
{"axis": "perception","value": 3.12},
{"axis": "possession","value": 3.12},
{"axis": "social","value": 3.12}
]
},
{"name": "clan B",
"axes": [
{"axis": "cognition","value": 3.12},
{"axis": "communication","value": 0.00},
{"axis": "competition","value": 0.00},
{"axis": "consumption","value": 0.00},
{"axis": "contact","value": 9.38},
{"axis": "emotion","value": 0.00},
{"axis": "motion","value": 0.00},
{"axis": "perception","value": 0.00},
{"axis": "possession","value": 0.00},
{"axis": "social","value": 0.00}
]
}
];
var radarChartOptions = {
w: 290,
h: 350,
margin: marginRadar,
levels: 6,
roundStrokes: false,
color: d3.scaleOrdinal().range(["#AFC52F", "#ff6600"]),
format: '.0f',
legend: { title: 'Legend', translateX: 150, translateY: 100 },
unit: '%'
};
// Draw the chart, get a reference the created svg element :
let svg_radar = RadarChart(".radarChart", data, radarChartOptions);
</script>
</td>
</tr>
</table>
</body>
It doesn't work: loading json file [note: everything is the same before and after the following suggestion, except that I have added <script src="https://d3js.org/d3.v5.min.js"></script> between <td> and <div class="radarChart"..> so that there is no conflict with barplots and piecharts which need v4 instead of v5]:
<table>
<tr>
<td style="radarChart">
<!-- add v5 here -->
<script src="https://d3js.org/d3.v5.min.js"></script>
<div class="radarChart" style="display: inline-flex;"></div>
<!-- [same data ] -->
[...]
var data = d3.json("../other/radarChart-data.json");
console.log(data)
<!-- [same data after] -->
[...]
Thus my question is: how to load JSON file for this radarchart? I prefer to load a file since "value: XX" are not fixed, but changed according to new values adding in related xml files.
In advance, thanks so much for your kind help.
The solution suggested here works:
https://www.quora.com/How-can-I-load-data-from-a-JSON-file-into-a-variable-in-JavaScript-without-using-Ajax?fbclid=IwAR1ilYFGKBaNsUupYOwIBSVMHBDP24o7j87WP5GzZSrlwIsyLK1riU5JCRQ
It bypasses the constraints of Ajax in a beautifully simple way!
move the declaration of your var data into a separate file
load this file as a javascript
use the variable as you need to!
Important: Both imports of d3 edit the global scope, so one will override the other. I recommend to just choose v4 for the radar chart.
My answer is very long, because I had to include the RadarChart code to make it work. Just scroll down to the very end to see what I did. d3.json needs a function as a second argument, and will call that function with the result:
/////////////////////////////////////////////////////////
/////////////// The Radar Chart Function ////////////////
/// mthh - 2017 /////////////////////////////////////////
// Inspired by the code of alangrafu and Nadieh Bremer //
// (VisualCinnamon.com) and modified for d3 v4 //////////
/////////////////////////////////////////////////////////
const max = Math.max;
const sin = Math.sin;
const cos = Math.cos;
const HALF_PI = Math.PI / 2;
const RadarChart = function RadarChart(parent_selector, data, options) {
//Wraps SVG text - Taken from http://bl.ocks.org/mbostock/7555321
const wrap = (text, width) => {
text.each(function() {
var text = d3.select(this),
words = text.text().split(/\s+/).reverse(),
word,
line = [],
lineNumber = 0,
lineHeight = 1.4, // ems
y = text.attr("y"),
x = text.attr("x"),
dy = parseFloat(text.attr("dy")),
tspan = text.text(null).append("tspan").attr("x", x).attr("y", y).attr("dy", dy + "em");
while (word = words.pop()) {
line.push(word);
tspan.text(line.join(" "));
if (tspan.node().getComputedTextLength() > width) {
line.pop();
tspan.text(line.join(" "));
line = [word];
tspan = text.append("tspan").attr("x", x).attr("y", y).attr("dy", ++lineNumber * lineHeight + dy + "em").text(word);
}
}
});
} //wrap
const cfg = {
w: 600, //Width of the circle
h: 600, //Height of the circle
margin: {
top: 20,
right: 20,
bottom: 20,
left: 20
}, //The margins of the SVG
levels: 3, //How many levels or inner circles should there be drawn
maxValue: 0, //What is the value that the biggest circle will represent
labelFactor: 1.25, //How much farther than the radius of the outer circle should the labels be placed
wrapWidth: 60, //The number of pixels after which a label needs to be given a new line
opacityArea: 0.35, //The opacity of the area of the blob
dotRadius: 4, //The size of the colored circles of each blog
opacityCircles: 0.1, //The opacity of the circles of each blob
strokeWidth: 2, //The width of the stroke around each blob
roundStrokes: false, //If true the area and stroke will follow a round path (cardinal-closed)
color: d3.scaleOrdinal(d3.schemeCategory10), //Color function,
format: '.2%',
unit: '',
legend: false
};
//Put all of the options into a variable called cfg
if ('undefined' !== typeof options) {
for (var i in options) {
if ('undefined' !== typeof options[i]) {
cfg[i] = options[i];
}
} //for i
} //if
//If the supplied maxValue is smaller than the actual one, replace by the max in the data
// var maxValue = max(cfg.maxValue, d3.max(data, function(i){return d3.max(i.map(function(o){return o.value;}))}));
let maxValue = 0;
for (let j = 0; j < data.length; j++) {
for (let i = 0; i < data[j].axes.length; i++) {
data[j].axes[i]['id'] = data[j].name;
if (data[j].axes[i]['value'] > maxValue) {
maxValue = data[j].axes[i]['value'];
}
}
}
maxValue = max(cfg.maxValue, maxValue);
const allAxis = data[0].axes.map((i, j) => 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(cfg.format), //Formatting
angleSlice = Math.PI * 2 / total; //The width in radians of each "slice"
//Scale for the radius
const rScale = d3.scaleLinear()
.range([0, radius])
.domain([0, maxValue]);
/////////////////////////////////////////////////////////
//////////// Create the container SVG and g /////////////
/////////////////////////////////////////////////////////
const parent = d3.select(parent_selector);
//Remove whatever chart with the same id/class was present before
parent.select("svg").remove();
//Initiate the radar chart SVG
let svg = parent.append("svg")
.attr("width", cfg.w + cfg.margin.left + cfg.margin.right)
.attr("height", cfg.h + cfg.margin.top + cfg.margin.bottom)
.attr("class", "radar");
//Append a g element
let g = svg.append("g")
.attr("transform", "translate(" + (cfg.w / 2 + cfg.margin.left) + "," + (cfg.h / 2 + cfg.margin.top) + ")");
/////////////////////////////////////////////////////////
////////// Glow filter for some extra pizzazz ///////////
/////////////////////////////////////////////////////////
//Filter for the outside glow
let filter = g.append('defs').append('filter').attr('id', 'glow'),
feGaussianBlur = filter.append('feGaussianBlur').attr('stdDeviation', '2.5').attr('result', 'coloredBlur'),
feMerge = filter.append('feMerge'),
feMergeNode_1 = feMerge.append('feMergeNode').attr('in', 'coloredBlur'),
feMergeNode_2 = feMerge.append('feMergeNode').attr('in', 'SourceGraphic');
/////////////////////////////////////////////////////////
/////////////// Draw the Circular grid //////////////////
/////////////////////////////////////////////////////////
//Wrapper for the grid & axes
let axisGrid = g.append("g").attr("class", "axisWrapper");
//Draw the background circles
axisGrid.selectAll(".levels")
.data(d3.range(1, (cfg.levels + 1)).reverse())
.enter()
.append("circle")
.attr("class", "gridCircle")
.attr("r", d => radius / cfg.levels * d)
.style("fill", "#CDCDCD")
.style("stroke", "#CDCDCD")
.style("fill-opacity", cfg.opacityCircles)
.style("filter", "url(#glow)");
//Text indicating at what % each level is
axisGrid.selectAll(".axisLabel")
.data(d3.range(1, (cfg.levels + 1)).reverse())
.enter().append("text")
.attr("class", "axisLabel")
.attr("x", 4)
.attr("y", d => -d * radius / cfg.levels)
.attr("dy", "0.4em")
.style("font-size", "10px")
.attr("fill", "#737373")
.text(d => Format(maxValue * d / cfg.levels) + cfg.unit);
/////////////////////////////////////////////////////////
//////////////////// Draw the axes //////////////////////
/////////////////////////////////////////////////////////
//Create the straight lines radiating outward from the center
var axis = axisGrid.selectAll(".axis")
.data(allAxis)
.enter()
.append("g")
.attr("class", "axis");
//Append the lines
axis.append("line")
.attr("x1", 0)
.attr("y1", 0)
.attr("x2", (d, i) => rScale(maxValue * 1.1) * cos(angleSlice * i - HALF_PI))
.attr("y2", (d, i) => rScale(maxValue * 1.1) * sin(angleSlice * i - HALF_PI))
.attr("class", "line")
.style("stroke", "white")
.style("stroke-width", "2px");
//Append the labels at each axis
axis.append("text")
.attr("class", "legend")
.style("font-size", "11px")
.attr("text-anchor", "middle")
.attr("dy", "0.35em")
.attr("x", (d, i) => rScale(maxValue * cfg.labelFactor) * cos(angleSlice * i - HALF_PI))
.attr("y", (d, i) => rScale(maxValue * cfg.labelFactor) * sin(angleSlice * i - HALF_PI))
.text(d => d)
.call(wrap, cfg.wrapWidth);
/////////////////////////////////////////////////////////
///////////// Draw the radar chart blobs ////////////////
/////////////////////////////////////////////////////////
//The radial line function
const radarLine = d3.radialLine()
.curve(d3.curveLinearClosed)
.radius(d => rScale(d.value))
.angle((d, i) => i * angleSlice);
if (cfg.roundStrokes) {
radarLine.curve(d3.curveCardinalClosed)
}
//Create a wrapper for the blobs
const blobWrapper = g.selectAll(".radarWrapper")
.data(data)
.enter().append("g")
.attr("class", "radarWrapper");
//Append the backgrounds
blobWrapper
.append("path")
.attr("class", "radarArea")
.attr("d", d => radarLine(d.axes))
.style("fill", (d, i) => cfg.color(i))
.style("fill-opacity", cfg.opacityArea)
.on('mouseover', function(d, i) {
//Dim all blobs
parent.selectAll(".radarArea")
.transition().duration(200)
.style("fill-opacity", 0.1);
//Bring back the hovered over blob
d3.select(this)
.transition().duration(200)
.style("fill-opacity", 0.7);
})
.on('mouseout', () => {
//Bring back all blobs
parent.selectAll(".radarArea")
.transition().duration(200)
.style("fill-opacity", cfg.opacityArea);
});
//Create the outlines
blobWrapper.append("path")
.attr("class", "radarStroke")
.attr("d", function(d, i) {
return radarLine(d.axes);
})
.style("stroke-width", cfg.strokeWidth + "px")
.style("stroke", (d, i) => cfg.color(i))
.style("fill", "none")
.style("filter", "url(#glow)");
//Append the circles
blobWrapper.selectAll(".radarCircle")
.data(d => d.axes)
.enter()
.append("circle")
.attr("class", "radarCircle")
.attr("r", cfg.dotRadius)
.attr("cx", (d, i) => rScale(d.value) * cos(angleSlice * i - HALF_PI))
.attr("cy", (d, i) => rScale(d.value) * sin(angleSlice * i - HALF_PI))
.style("fill", (d) => cfg.color(d.id))
.style("fill-opacity", 0.8);
/////////////////////////////////////////////////////////
//////// Append invisible circles for tooltip ///////////
/////////////////////////////////////////////////////////
//Wrapper for the invisible circles on top
const blobCircleWrapper = g.selectAll(".radarCircleWrapper")
.data(data)
.enter().append("g")
.attr("class", "radarCircleWrapper");
//Append a set of invisible circles on top for the mouseover pop-up
blobCircleWrapper.selectAll(".radarInvisibleCircle")
.data(d => d.axes)
.enter().append("circle")
.attr("class", "radarInvisibleCircle")
.attr("r", cfg.dotRadius * 1.5)
.attr("cx", (d, i) => rScale(d.value) * cos(angleSlice * i - HALF_PI))
.attr("cy", (d, i) => rScale(d.value) * sin(angleSlice * i - HALF_PI))
.style("fill", "none")
.style("pointer-events", "all")
.on("mouseover", function(d, i) {
tooltip
.attr('x', this.cx.baseVal.value - 10)
.attr('y', this.cy.baseVal.value - 10)
.transition()
.style('display', 'block')
.text(Format(d.value) + cfg.unit);
})
.on("mouseout", function() {
tooltip.transition()
.style('display', 'none').text('');
});
const tooltip = g.append("text")
.attr("class", "tooltip")
.attr('x', 0)
.attr('y', 0)
.style("font-size", "12px")
.style('display', 'none')
.attr("text-anchor", "middle")
.attr("dy", "0.35em");
if (cfg.legend !== false && typeof cfg.legend === "object") {
let legendZone = svg.append('g');
let names = data.map(el => el.name);
if (cfg.legend.title) {
let title = legendZone.append("text")
.attr("class", "title")
.attr('transform', `translate(${cfg.legend.translateX},${cfg.legend.translateY})`)
.attr("x", cfg.w - 70)
.attr("y", 10)
.attr("font-size", "12px")
.attr("fill", "#404040")
.text(cfg.legend.title);
}
let legend = legendZone.append("g")
.attr("class", "legend")
.attr("height", 100)
.attr("width", 200)
.attr('transform', `translate(${cfg.legend.translateX},${cfg.legend.translateY + 20})`);
// Create rectangles markers
legend.selectAll('rect')
.data(names)
.enter()
.append("rect")
.attr("x", cfg.w - 65)
.attr("y", (d, i) => i * 20)
.attr("width", 10)
.attr("height", 10)
.style("fill", (d, i) => cfg.color(i));
// Create labels
legend.selectAll('text')
.data(names)
.enter()
.append("text")
.attr("x", cfg.w - 52)
.attr("y", (d, i) => i * 20 + 9)
.attr("font-size", "11px")
.attr("fill", "#737373")
.text(d => d);
}
return svg;
}
//////////////////////////////////////////////////////////////
//////////////////////// Set-Up //////////////////////////////
//////////////////////////////////////////////////////////////
var marginRadar = {
top: 150,
right: 80,
bottom: 50,
left: 80
},
widthRadar = Math.min(700, window.innerWidth / 4) - marginRadar.left - marginRadar.right,
heightRadar = Math.min(widthRadar, window.innerHeight - marginRadar.top - marginRadar.bottom);
//////////////////////////////////////////////////////////////
////////////////////////// Data //////////////////////////////
//////////////////////////////////////////////////////////////
var radarChartOptions = {
w: 290,
h: 350,
margin: marginRadar,
levels: 6,
roundStrokes: false,
color: d3.scaleOrdinal().range(["#AFC52F", "#ff6600"]),
format: '.0f',
legend: {
title: 'Legend',
translateX: 150,
translateY: 100
},
unit: '%'
};
// Just some example JSON data I found at https://support.oneskyapp.com/hc/en-us/articles/208047697-JSON-sample-files
d3.json('https://support.oneskyapp.com/hc/en-us/article_attachments/202761727/example_2.json', function(fakeData) {
// Which I then map into your format - you don't need to do this
const data = Object.entries(fakeData['quiz']).map(([topic, questionObject]) => ({
name: topic,
axes: [
{ axis: 'difficulty', value: Object.keys(questionObject).length },
{ axis: 'depth', value: Object.keys(questionObject).length },
{ axis: 'duration', value: Object.keys(questionObject).length },
{ axis: 'delicacy', value: Object.keys(questionObject).length }
]
}));
// Draw the chart, get a reference the created svg element :
let svg_radar = RadarChart(".radarChart", data, radarChartOptions);
});
<head>
<!-- Load d3.js -->
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-path.v1.min.js" charset="utf-8"></script>
<!-- source: https://gist.github.com/mthh/7e17b680b35b83b49f1c22a3613bd89f -->
</head>
<body>
<table>
<tr>
<td style="radarChart">
<div class="radarChart" style="display: inline-flex;"></div>
</td>
</tr>
</table>
Related
I have a radar chart working in a regular js file, and a react app component that can render and return a simple bar chart. I'm trying to get the much more complex radar chart to render and return in react's component. I'm currently getting an error: ./src/App.js Attempted import error: 'scale' is not exported from 'd3' (imported as 'd3'). Any other feedback/suggestions in general would be appreciated. I'm new to D3. Thanks so much in advance.
import * as d3 from "d3";
class App extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
/////////////////////////////////////////////////////////
/////////////// The Radar Chart Function ////////////////
/////////////// Written by Nadieh Bremer ////////////////
////////////////// VisualCinnamon.com ///////////////////
/////////// Inspired by the code of alangrafu ///////////
/////////////////////////////////////////////////////////
/* Radar chart design created by Nadieh Bremer - VisualCinnamon.com */
componentDidMount() {
//////////////////////// Set-Up //////////////////////////////
var margin = { top: 100, right: 100, bottom: 100, left: 100 },
width = Math.min(700, window.innerWidth - 10) - margin.left - margin.right,
height = Math.min(
width,
window.innerHeight - margin.top - margin.bottom - 20
);
////////////////////////// Data //////////////////////////////
var data = [
[
//Tsircon
{ axis: "BALLISTIC", value: 65 },
{ axis: "Glide", value: 39 },
{ axis: "20G TURN", value: 43 },
{ axis: "FISH HOOK DIVE", value: 50 },
{ axis: "PHUGOID MOTION", value: 60 },
{ axis: "ENERGY SCRUB", value: 52 },
],
// [
// //Kinzhal
// { axis: "BALLISTIC", value: 0.27 },
// { axis: "Glide", value: 0.16 },
// { axis: "20G TURN", value: 0.35 },
// { axis: "FISH HOOK DIVE", value: 0.13 },
// { axis: "PHUGOID MOTION", value: 0.2 },
// { axis: "ENERGY SCRUB", value: 0.13 },
// ],
// [
// //Example Missile Overlay
// { axis: "BALLISTIC", value: 0.26 },
// { axis: "Glide", value: 0.1 },
// { axis: "20G TURN", value: 0.3 },
// { axis: "FISH HOOK DIVE", value: 0.14 },
// { axis: "PHUGOID MOTION", value: 0.22 },
// { axis: "ENERGY SCRUB", value: 0.04 },
// ],
];
//////////////////// Draw the Chart //////////////////////////
var color = d3.scale.ordinal().range(["#739CC4", "#CC333F", "#00A0B0"]);
var radarChartOptions = {
w: width,
h: height,
margin: margin,
maxValue: 0.5,
levels: 5,
roundStrokes: false,
color: color,
};
//Call function to draw the Radar chart
RadarChart(".radarChart", data, radarChartOptions);
function RadarChart(id, data, options) {
var cfg = {
w: 600, //Width of the circle
h: 600, //Height of the circle
margin: { top: 20, right: 20, bottom: 20, left: 20 }, //The margins of the SVG
levels: 3, //How many levels or inner circles should there be drawn
maxValue: 0, //What is the value that the biggest circle will represent
labelFactor: 1.25, //How much farther than the radius of the outer circle should the labels be placed
wrapWidth: 60, //The number of pixels after which a label needs to be given a new line
opacityArea: 0.35, //The opacity of the area of the blob
dotRadius: 4, //The size of the colored circles of each blog
opacityCircles: 0.1, //The opacity of the circles of each blob
strokeWidth: 2, //The width of the stroke around each blob
roundStrokes: false, //If true the area and stroke will follow a round path (cardinal-closed)
color: d3.scale.category10(), //Color function
};
//Put all of the options into a variable called cfg
if ("undefined" !== typeof options) {
for (var i in options) {
if ("undefined" !== typeof options[i]) {
cfg[i] = options[i];
}
} //for i
} //if
//If the supplied maxValue is smaller than the actual one, replace by the max in the data
var maxValue = Math.max(
cfg.maxValue,
d3.max(data, function (i) {
return d3.max(
i.map(function (o) {
return o.value;
})
);
})
);
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"
//Scale for the radius
var rScale = d3.scale.linear().range([0, radius]).domain([0, maxValue]);
//////////// Create the container SVG and g /////////////
//Remove whatever chart with the same id/class was present before
d3.select(id).select("svg").remove();
//Initiate the radar chart SVG
var svg = d3
.select(id)
.append("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);
//Append a g element
var g = svg
.append("g")
.attr(
"transform",
"translate(" +
(cfg.w / 2 + cfg.margin.left) +
"," +
(cfg.h / 2 + cfg.margin.top) +
")"
);
////////// Glow filter ///////////
//Filter for the outside glow
var filter = g.append("defs").append("filter").attr("id", "glow"),
feGaussianBlur = filter
.append("feGaussianBlur")
.attr("stdDeviation", "2.5")
.attr("result", "coloredBlur"),
feMerge = filter.append("feMerge"),
feMergeNode_1 = feMerge.append("feMergeNode").attr("in", "coloredBlur"),
feMergeNode_2 = feMerge.append("feMergeNode").attr("in", "SourceGraphic");
/////////////// Draw the Circular grid //////////////////
//Wrapper for the grid & axes
var axisGrid = g.append("g").attr("class", "axisWrapper");
//Draw the background circles
axisGrid
.selectAll(".levels")
.data(d3.range(1, cfg.levels + 1).reverse())
.enter()
.append("circle")
.attr("class", "gridCircle")
.attr("r", function (d, i) {
return (radius / cfg.levels) * d;
})
.style("fill", "#CDCDCD")
.style("stroke", "#CDCDCD")
.style("fill-opacity", cfg.opacityCircles)
.style("filter", "url(#glow)");
//Text indicating at what % each level is
axisGrid
.selectAll(".axisLabel")
.data(d3.range(1, cfg.levels + 1).reverse())
.enter()
.append("text")
.attr("class", "axisLabel")
.attr("x", 4)
.attr("y", function (d) {
return (-d * radius) / cfg.levels;
})
.attr("dy", "0.4em")
.style("font-size", "10px")
.attr("fill", "#737373")
.text(function (d, i) {
return Format((maxValue * d) / cfg.levels);
});
//////////////////// Draw the axes //////////////////////
//Create the straight lines radiating outward from the center
var axis = axisGrid
.selectAll(".axis")
.data(allAxis)
.enter()
.append("g")
.attr("class", "axis");
//Append the lines
axis
.append("line")
.attr("x1", 0)
.attr("y1", 0)
.attr("x2", function (d, i) {
return rScale(maxValue * 1.1) * Math.cos(angleSlice * i - Math.PI / 2);
})
.attr("y2", function (d, i) {
return rScale(maxValue * 1.1) * Math.sin(angleSlice * i - Math.PI / 2);
})
.attr("class", "line")
.style("stroke", "white")
.style("stroke-width", "2px");
//Append the labels at each axis
axis
.append("text")
.attr("class", "legend")
.style("font-size", "11px")
.attr("text-anchor", "middle")
.attr("dy", "0.35em")
.attr("x", function (d, i) {
return (
rScale(maxValue * cfg.labelFactor) *
Math.cos(angleSlice * i - Math.PI / 2)
);
})
.attr("y", function (d, i) {
return (
rScale(maxValue * cfg.labelFactor) *
Math.sin(angleSlice * i - Math.PI / 2)
);
})
.text(function (d) {
return d;
})
.call(wrap, cfg.wrapWidth);
svg.append("text")
.attr("x", (width / 4.8))
.attr("y", 75 - (margin.top / 2))
.attr("text-anchor", "middle")
.style("font-size", "24px")
.style("font-weight", "bold")
.style("fill", "white")
.text("MANEUVERS");
svg.append("text")
.attr("x", (width / 4.2))
.attr("y", 95 - (margin.top / 2))
.attr("text-anchor", "middle")
.style("font-size", "16px")
.style("fill", "white")
.text("initiate: % ground track");
svg.append("text")
.attr("x", (width / 3.6))
.attr("y", 115 - (margin.top / 2))
.attr("text-anchor", "middle")
.style("font-size", "16px")
.style("fill", "#8CD9FF")
.text("% variability");
svg.append("text")
.attr("x", (width / 8))
.attr("y", 135 - (margin.top / 2))
.attr("text-anchor", "middle")
.style("font-size", "16px")
.style("fill", "white")
.text("sample:");
svg.append("text")
.attr("x", (width / 3.4))
.attr("y", 135 - (margin.top / 2))
.attr("text-anchor", "middle")
.style("font-size", "16px")
.style("fill", "#76ADDB")
.text("% trajectories");
///////////// Draw the radar chart blobs ////////////////
//The radial line function
var radarLine = d3.svg.line
.radial()
.interpolate("linear-closed")
.radius(function (d) {
return rScale(d.value);
})
.angle(function (d, i) {
return i * angleSlice;
});
if (cfg.roundStrokes) {
radarLine.interpolate("cardinal-closed");
}
//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)
.on("mouseover", function (d, i) {
//Dim all blobs
d3.selectAll(".radarArea")
.transition()
.duration(200)
.style("fill-opacity", 0.1);
//Bring back the hovered over blob
d3.select(this).transition().duration(200).style("fill-opacity", 0.7);
})
.on("mouseout", function () {
//Bring back all blobs
d3.selectAll(".radarArea")
.transition()
.duration(200)
.style("fill-opacity", cfg.opacityArea);
});
//Create the outlines
blobWrapper
.append("path")
.attr("class", "radarStroke")
.attr("d", function (d, i) {
return radarLine(d);
})
.style("stroke-width", cfg.strokeWidth + "px")
.style("stroke", function (d, i) {
return cfg.color(i);
})
.style("fill", "none")
.style("filter", "url(#glow)");
//Append the circles
blobWrapper
.selectAll(".radarCircle")
.data(function (d, i) {
return d;
})
.enter()
.append("circle")
.attr("class", "radarCircle")
.attr("r", cfg.dotRadius)
.attr("cx", function (d, i) {
return rScale(d.value) * Math.cos(angleSlice * i - Math.PI / 2);
})
.attr("cy", function (d, i) {
return rScale(d.value) * Math.sin(angleSlice * i - Math.PI / 2);
})
.style("fill", function (d, i, j) {
return cfg.color(j);
})
.style("fill-opacity", 0.8);
//////// Append invisible circles for tooltip ///////////
//Wrapper for the invisible circles on top
var blobCircleWrapper = g
.selectAll(".radarCircleWrapper")
.data(data)
.enter()
.append("g")
.attr("class", "radarCircleWrapper");
//Append a set of invisible circles on top for the mouseover pop-up
blobCircleWrapper
.selectAll(".radarInvisibleCircle")
.data(function (d, i) {
return d;
})
.enter()
.append("circle")
.attr("class", "radarInvisibleCircle")
.attr("r", cfg.dotRadius * 1.5)
.attr("cx", function (d, i) {
return rScale(d.value) * Math.cos(angleSlice * i - Math.PI / 2);
})
.attr("cy", function (d, i) {
return rScale(d.value) * Math.sin(angleSlice * i - Math.PI / 2);
})
.style("fill", "none")
.style("pointer-events", "all")
.on("mouseover", function (d, i) {
newX = parseFloat(d3.select(this).attr("cx")) - 10;
newY = parseFloat(d3.select(this).attr("cy")) - 10;
tooltip
.attr("x", newX)
.attr("y", newY)
.text(Format(d.value))
.transition()
.duration(200)
.style("opacity", 1);
})
.on("mouseout", function () {
tooltip.transition().duration(200).style("opacity", 0);
});
//Set up the small tooltip for when you hover over a circle
var tooltip = g.append("text").attr("class", "tooltip").style("opacity", 0);
/////////////////// Helper Function /////////////////////
//Taken from http://bl.ocks.org/mbostock/7555321
//Wraps SVG text
function wrap(text, width) {
text.each(function () {
var text = d3.select(this),
words = text.text().split(/\s+/).reverse(),
word,
line = [],
lineNumber = 0,
lineHeight = 1.4, // ems
y = text.attr("y"),
x = text.attr("x"),
dy = parseFloat(text.attr("dy")),
tspan = text
.text(null)
.append("tspan")
.attr("x", x)
.attr("y", y)
.attr("dy", dy + "em");
while ((word = words.pop())) {
line.push(word);
tspan.text(line.join(" "));
if (tspan.node().getComputedTextLength() > width) {
line.pop();
tspan.text(line.join(" "));
line = [word];
tspan = text
.append("tspan")
.attr("x", x)
.attr("y", y)
.attr("dy", ++lineNumber * lineHeight + dy + "em")
.text(word);
}
}
});
} //wrap
} //RadarChart
} //componentDidMount
render() {
const styles = {
container: {
display: "grid",
justifyItems: "center"
}
};
return (
// <div ref="chart" style={styles.container}>
<div className=".radarChart" style={styles.container}>
<h1 style={{ textAlign: "center" }}>Hi, I'm the radar chart</h1>
</div>
);
}
}
export default App;
Edit: update: so I found some answers elsewhere, thank you #Michael Rovinsky for pointing me in the right direction. Some of this code was pre D3 version 4.
d3.scale.linear() vs d3.scaleLinear()
What is the d3.js v4.0 equivalent for d3.scale.category10()?
Current error: I am now getting
src\App.js
Line 406:8: 'newX' is not defined no-undef
Line 407:8: 'newY' is not defined no-undef
Line 410:21: 'newX' is not defined no-undef
Line 411:21: 'newY' is not defined no-undef
Which was fixed by defining them with "let" so the new code is
let newX = parseFloat(d3.select(this).attr("cx")) - 10; let newY = parseFloat(d3.select(this).attr("cy")) - 10;
Finally I was getting an error TypeError: Cannot read property 'radial' of undefined
which was solved by updating var radarLine = d3.svg.line.radial().interpolate("linear-closed").radius(...
to radarLine = d3.lineRadial().radius(...
The graph rendered once I fixed the typo className=".radarChart" to className="radarChart"
I'm currently working on trying to create a "facet plot" within d3.js, which I realize in D3 language isn't really a thing. However, I found this thread which outlined how to accomplish the task. From this post, I've converted the line chart from vertical to horizontal, and added a few extra padding elements where needed.
Now I would like to add a tooltip to each plot. From working with other pieces of code, this seems quite challenging. For some reason I can't figure out how to attach the tooltip to each individual plot. Any thoughts on how I might be able to accomplish this?
What it currently looks like:
// Data and manipluation:
const data = d3.range(25).map(i => ({
bib: Math.floor(i / 5) + 1,
ratio: -1 + Math.random() * 5,
run: [1, 2, 3, 4, 5][i % 5],
run: [1, 2, 3, 4, 5][i % 5],
name: ['metric1', 'metric2', 'metric3', 'metric4', 'metric5'][Math.floor(i / 5)]
}));
const grouped = d3.group(data,d=>d.bib);
// Dimensions:
const height = 800;
const width = 700;
const margin = {
top: 10,
left: 50,
right: 50,
bottom: 50
}
const padding = 30;
const doublePadding = padding * 2;
const plotHeight = (height-doublePadding)/grouped.size - padding;
const plotWidth = width-padding*2;
// const plotWidth = (width-padding)/grouped.size - padding;
// const plotHeight = height-padding*2;
const svg = d3.select("#chart1")
.append("svg")
.attr("width", margin.left+width+margin.right)
.attr("height", margin.top+height+margin.bottom+(padding*grouped.size));
const g = svg.append("g")
.attr("transform","translate("+[margin.left,margin.top]+")");
//Scales:
const xScale = d3.scaleLinear()
.domain(d3.extent(data, d => d.run))
.range([0, plotWidth]);
const yScale = d3.scaleLinear()
.domain(d3.extent(data, d => d.ratio))
.range([plotHeight,0]);
// Place plots:
const plots = g.selectAll(null)
.data(grouped)
.enter()
.append("g")
.attr("transform", function(d,i) {
return "translate("+[padding,i*(doublePadding+plotHeight)+padding]+")";
})
//Optional plot background:
plots.append("rect")
.attr("width",plotWidth)
.attr("height",plotHeight)
.attr("fill","#ddd");
// Plot actual data
plots.selectAll(null)
.data(d=>d[1])
.enter()
.append("circle")
.attr("r", 4)
.attr("cy", d=>yScale(d.ratio))
.attr("cx", d=>xScale(d.run))
// Plot line if needed:
plots.append("path")
.attr("d", function(d) {
return d3.line()
.x(d=>xScale(d.run))
.y(d=>yScale(d.ratio))
(d[1])
})
.attr("stroke", "#333")
.attr("stroke-width", 1)
.attr("fill","none")
// Plot names if needed:
plots.append("text")
.attr("x", plotWidth/2)
.attr("y", -10)
.text(function(d) {
return d[1][0].name;
})
.attr("text-anchor","middle");
// Plot axes
plots.append("g")
.attr("transform","translate("+[0,plotHeight]+")")
.call(d3.axisBottom(xScale).ticks(4));
plots.append("g")
.attr("transform","translate("+[-padding,0]+")")
.call(d3.axisLeft(yScale))
<head>
<!-- Load d3.js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.6.1/d3.min.js"></script>
</head>
<body>
<div class="graph-container">
<div id="chart1"></div>
</div>
</body>
See a solution in a fiddle:
Add a g element with background and text(s):
const tooltip = d3.select('svg')
.append('g')
.style('visibility', 'hidden');
tooltip.append('rect')
.attr('width', 100)
.attr('height', 50)
.style('fill', '#fff')
.style('stroke', '#000')
tooltip.append('text')
.attr('x', 20)
.attr('y', 20)
Update it on mouseenter, mousevove, mouseleave:
plots.append("rect")
.attr("width",plotWidth)
.attr("height",plotHeight)
.attr("fill","#ddd")
.on("mouseenter", (e, d) => {
tooltip.style('visibility', 'visible');
tooltip.select('text').text(d[0]);
})
.on("mouseleave", () => tooltip.style('visibility', 'hidden'))
.on("mousemove", e => tooltip.attr('transform', `translate(${e.clientX},${e.clientY})`))
I'm really having trouble with D3 and need some help changing my existing barchart to be a grouped barchart The barchart is being used within a tooltip and currently looks like:
Each colour represents a sector of industry (pink = retail, teal = groceries...etc).
I need to change the bar chart so that it compares the percentage change in each industry with the world average percentage change in this industry.
At the moment the bar chart is being created from an array of data. I also have an array with the world percentage values.
So imagine:
countryData = [10,-20,-30,-63,-23,20],
worldData = [23,-40,-23,-42,-23,40]
Where index 0 = retail sector, index 1 = grocery sector, etc.
I need to plot a grouped barchart comparing each sector to the world average (show the world average in red). This is a bit tricky to explain so I drew it for you (...excuse the shoddy drawing).
Please can someone help me change my existing tooltip?
Here's the current code. If you want to simulate the data values changing.
If you want to scrap my existing code that's fine.
.on('mouseover', ({ properties }) => {
// get county data
const mobilityData = covid.data[properties[key]] || {};
const {
retailAverage,
groceryAverage,
parksAverage,
transitAverage,
workplaceAverage,
residentialAverage,
} = getAverage(covid1);
let avgArray = [retailAverage, groceryAverage, parksAverage, transitAverage, workplaceAverage, retailAverage];
let categoriesNames = ["Retail", "Grocery", "Parks", "Transit", "Workplaces", "Residential"];
// create tooltip
div = d3.select('body')
.append('div')
.attr('class', 'tooltip')
.style('opacity', 0);
div.html(properties[key]);
div.transition()
.duration(200)
.style('opacity', 0.9);
// calculate bar graph data for tooltip
const barData = [];
Object.keys(mobilityData).forEach((industry) => {
const stringMinusPercentage = mobilityData[industry].slice(0, -1);
barData.push(+stringMinusPercentage); // changing it to an integer value, from string
});
//combine the two lists for the combined bar graph
var combinedList = [];
for(var i = 0; i < barData.length; i++) {
const stringMinusPercentage2 = +(avgArray[i].slice(0, -1));
const object = {category: categoriesNames[i], country: barData[i], world: stringMinusPercentage2}
combinedList.push(object); //Push object into list
}
console.log(combinedList);
// barData = barData.sort(function (a, b) { return a - b; });
// sort into ascending ^ keeping this in case we need it later
const height2 = 220;
const width2 = 250;
const margin = {
left: 50, right: 10, top: 20, bottom: 15,
};
// create bar chart svg
const svgA = div.append('svg')
.attr('height', height2)
.attr('width', width2)
.style('border', '1px solid')
.append('g')
// apply the margins:
.attr('transform', `translate(${[`${margin.left},${margin.top}`]})`);
const barWidth = 30; // Width of the bars
// plot area is height - vertical margins.
const chartHeight = height2 - margin.top - margin.left;
// set the scale:
const yScale = d3.scaleLinear()
.domain([-100, 100])
.range([chartHeight, 0]);
// draw some rectangles:
svgA
.selectAll('rect')
.data(barData)
.enter()
.append('rect')
.attr('x', (d, i) => i * barWidth)
.attr('y', (d) => {
if (d < 0) {
return yScale(0); // if the value is under zero, the top of the bar is at yScale(0);
}
return yScale(d); // otherwise the rectangle top is above yScale(0) at yScale(d);
})
.attr('height', (d) => Math.abs(yScale(0) - yScale(d))) // the height of the rectangle is the difference between the scale value and yScale(0);
.attr('width', barWidth)
.style('fill', (d, i) => colours[i % 6]) // colour the bars depending on index
.style('stroke', 'black')
.style('stroke-width', '1px');
// Labelling the Y axis
const yAxis = d3.axisLeft(yScale);
svgA.append('text')
.attr('class', 'y label')
.attr('text-anchor', 'end')
.attr('x', -15)
.attr('y', -25)
.attr('dy', '-.75em')
.attr('transform', 'rotate(-90)')
.text('Percentage Change (%)');
svgA.append('g')
.call(yAxis);
})
.on('mouseout', () => {
div.style('opacity', 0);
div.remove();
})
.on('mousemove', () => div
.style('top', `${d3.event.pageY - 140}px`)
.style('left', `${d3.event.pageX + 15}px`));
svg.append('g')
.attr('transform', 'translate(25,25)')
.call(colorLegend, {
colorScale,
circleRadius: 10,
spacing: 30,
textOffset: 20,
});
};
drawMap(svg1, geoJson1, geoPath1, covid1, key1, 'impact1');
drawMap(svg2, geoJson2, geoPath2, covid2, key2, 'impact2');
};
In short I would suggest you to use two Band Scales for x axis. I've attached a code snippet showing the solution.
Enjoy ;)
//Assuming the following data final format
var finalData = [
{
"groupKey": "Retail",
"sectorValue": 70,
"worldValue": 60
},
{
"groupKey": "Grocery",
"sectorValue": 90,
"worldValue": 90
},
{
"groupKey": "other",
"sectorValue": -20,
"worldValue": 30
}
];
var colorRange = d3.scaleOrdinal().range(["#00BCD4", "#FFC400", "#ECEFF1"]);
var subGroupKeys = ["sectorValue", "worldValue"];
var svg = d3.select("svg");
var margin = {top: 20, right: 20, bottom: 30, left: 40};
var width = +svg.attr("width") - margin.left - margin.right;
var height = +svg.attr("height") - margin.top - margin.bottom;
var container = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// The scale spacing the groups, your "sectors":
var x0 = d3.scaleBand()
.domain(finalData.map(d => d.groupKey))
.rangeRound([0, width])
.paddingInner(0.1);
// The scale for spacing each group's bar, your "sector bar":
var x1 = d3.scaleBand()
.domain(subGroupKeys)
.rangeRound([0, x0.bandwidth()])
.padding(0.05);
var yScale = d3.scaleLinear()
.domain([-100, 100])
.rangeRound([height, 0]);
//and then you will need to append both, groups and bars
var groups = container.append('g')
.selectAll('g')
.data(finalData, d => d.groupKey)
.join("g")
.attr("transform", (d) => "translate(" + x0(d.groupKey) + ",0)");
//define groups bars, one per sub group
var bars = groups
.selectAll("rect")
.data(d => subGroupKeys.map(key => ({ key, value: d[key], groupKey: d.groupKey })), (d) => "" + d.groupKey + "_" + d.key)
.join("rect")
.attr("fill", d => colorRange(d.key))
.attr("x", d => x1(d.key))
.attr("width", (d) => x1.bandwidth())
.attr('y', (d) => Math.min(yScale(0), yScale(d.value)))
.attr('height', (d) => Math.abs(yScale(0) - yScale(d.value)));
//append x axis
container.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x0));
//append y axis
container.append("g")
.attr("class", "y axis")
.call(d3.axisLeft(yScale))
.append("text")
.attr("x", 2)
.attr("y", yScale(yScale.ticks().pop()) + 0.5)
.attr("dy", "0.32em")
.attr("fill", "#000")
.attr("font-weight", "bold")
.attr("text-anchor", "start")
.text("Values");
<script src="https://d3js.org/d3.v7.min.js"></script>
<svg width="600" height="400"></svg>
I am working on a d3 applicaton - with a pie chart -- I would like to get animation onload and on a call to action. Like when the chart becomes visible during a scroll.
Where the pie segments grow around the central pivot. So tween or snap to the other segment like a relay race
http://jsfiddle.net/pg886/192/
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://d3js.org/d3.v3.min.js"></script>
<div class="piechart" data-role="piechart" data-width=400 data-height=400 data-radius=30 data-innerradius=20
data-data=x>
</div>
<style>
.piechart{
/*border: 1px solid black;*/
/*text-align: center;
font-size: 12px;*/
}
</style>
<script>
$( document ).ready(function() {
console.log("test")
var $this = $('.piechart');
var data = [{
"label": "Apples",
"value": 100
},
{
"label": "Pears",
"value": 120
},
{
"label": "Bananas",
"value": 20
}];
var w = $this.data("width");
var h = $this.data("height");
var ir = $this.data("innerradius");
var r = $this.data("radius");
function colores_google(n) {
var colores_g = ["#f7b363", "#448875", "#c12f39", "#2b2d39", "#f8dd2f"];
//var colores_g = ["#47abd5", "#005a70", "#f5a0a3", "#ff7276", "#a9a19c", "#d0743c", "#ff8c00"];
return colores_g[n % colores_g.length];
}
var radius = Math.min(w, h) / 4;
var arc = d3.svg.arc()
.outerRadius(radius - 10)
.innerRadius(0);
var labelArc = d3.svg.arc()
.outerRadius(radius - r)
.innerRadius(radius - ir);
var pie = d3.layout.pie()
.sort(null)
.value(function(d) { return d.value; });
var chart = d3.select('.piechart').append("svg")
.attr("class", "chart")
.attr("width", w)
.attr("height", h)
.attr("transform", "translate(0,0)");
var piechart = chart
.append("g")
.attr("class", "piechart")
.attr("width", (radius*2))
.attr("transform", "translate(0,"+h/4+")");
var path_group = piechart.append("g")
.attr("class", "path_group")
.attr("transform", "translate(90," + ((h / 4) - 20) + ")");
var padding = 45;
var legendPaddingTop = 30;
var legend = chart.append("g")
.attr("class", "legend")
.attr("width", w/2)
.attr("height", h)
.attr("transform", "translate(" + (w - 50) + "," + (h / 4) + ")");
var label_group = legend.append("svg:g")
.attr("class", "label_group")
.attr("transform", "translate(" + (-(w / 3) + 20) + "," + 0 + ")");
var legend_group = legend.append("svg:g")
.attr("class", "legend_group")
.attr("transform", "translate(" + (-(w / 3) - 100) + "," + 0 + ")");
var g = path_group.selectAll(".arc")
.data(pie(data))
.enter().append("g")
.attr("class", "arc");
g.append("path")
.attr("d", arc)
.style("fill", function(d, i) {
return colores_google(i);
});
var legendHeight = legendPaddingTop;
var ySpace = 18;
//draw labels
var labels = label_group.selectAll("text.labels")
.data(data);
labels.enter().append("svg:text")
.attr("class", "labels")
.attr("dy", function(d, i) {
legendHeight+=ySpace;
return (ySpace * i) + 4;
})
.attr("text-anchor", function(d) {
return "start";
})
.text(function(d) {
return d.label;
});
labels.exit().remove();
//draw labels
//draw legend
var legend = legend_group.selectAll("circle").data(data);
legend.enter().append("svg:circle")
.attr("cx", 100)
.attr("cy", function(d, i) {
return ySpace * i;
})
.attr("r", 7)
.attr("width", 18)
.attr("height", 18)
.style("fill", function(d, i) {
return colores_google(i);
});
legend.exit().remove();
//draw legend
//reset legend height
//console.log("optimum height for legend", legendHeight);
$this.find('.legend').attr("height", legendHeight);
function type(d) {
d.value = +d.value;
return d;
}
});
</script>
So you can achieve this pretty easily, and there are a couple of blocks that will help you.
Arc Tween Firstly this block gives you an example of how to tween an arc. Basically you can't get that automatically so you have to write your own attrTween function. Fortunately this is pretty simple and Mike Bostock gives a really good example in there.
Here's a code sample - but the link gives a really good verbose description of what's going on.
.attrTween("d", function(d) {
var interpolate = d3.interpolate(d.endAngle, newAngle);
return function(t) {
d.endAngle = interpolate(t);
return arc(d);
};
}
Next you want something like this Donut with transitions. This is actually the end-result for where you're trying to get to. This effect is really easy to achieve, all you need to do is set your angles correctly and the timings.
Angles: So here you want both the endAngle and the startAngle to be the same at the start (which should be the endAngle value of the previous segment or 0 for the first segment).
Timing: You want to allow 1 animation to complete before you start the next, simply by delaying them. You can see how that's done with this snippet:
.transition()
.delay(function(d,i) { return i * 500; })
.duration(500)
.attrTween(...)
const dataset = [
{ age: "<5", population: 2704659 },
{ age: "5-13", population: 4499890 },
{ age: "14-17", population: 2159981 },
{ age: "18-24", population: 3853788 },
{ age: "25-44", population: 14106543 },
{ age: "45-64", population: 8819342 },
{ age: "≥65", population: 612463 },
];
const TIME = 2000 / dataset.length;
const color = d3.scaleOrdinal(["#98abc5", "#8a89a6", "#7b6888", "#6b486b", "#a05d56", "#d0743c", "#ff8c00"]);
const pie = d3.pie()
.sort(null)
.value(function(d) { return d.population; });
const path = d3.arc()
.innerRadius(0)
.outerRadius(350);
d3.select("#container")
.selectAll(".arc")
.data(pie(dataset))
.enter()
.append("g")
.attr("class", "arc")
.append("path")
.attr("fill", function(d) { return color(d.data.age); })
.transition()
.duration(TIME)
.ease(d3.easeLinear)
.delay(function(d, i) { return i * TIME; })
.attrTween("d", function(d) {
// Note the 0.1 to prevent errors generating the path
const angleInterpolation = d3.interpolate(d.startAngle + 0.1, d.endAngle);
return function(t) {
d.endAngle = angleInterpolation(t);
return path(d);
}
});
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="800" height="800">
<g id="container" transform="translate(400, 400)"></g>
</svg>
I have a graph that uses d3.radialLine that can be seen in this fiddle. My data points on the line are visualized by plotting the circle on the proper axis value. I need to have a line down the middle of the svg element that will give me the reading of the plotted data point that is touching it. You can see a graphic image of what I'm trying to accomplish:
I would like to programmatically rotate, for example clockwise so that December will be inline with the red line, and be able to read that data point. I know that I'll have to get the x2 value of the red line and use 'x2' of the December value to find out how many degrees I would need to rotate the graph, but I can't seem to figure that part out. The code was taken from Radar Chart.
I know that formula for rotating..d3.select(...).attr('transform', 'rotate(degree, x, y)'
Code
//////////////////////////////////////////////////////////////
//////////////////////// Set-Up //////////////////////////////
//////////////////////////////////////////////////////////////
var margin = {top: 100, right: 100, bottom: 100, left: 100},
width = Math.min(700, window.innerWidth - 10) - margin.left - margin.right,
height = Math.min(width, window.innerHeight - margin.top - margin.bottom - 20);
//////////////////////////////////////////////////////////////
////////////////////////// Data //////////////////////////////
//////////////////////////////////////////////////////////////
var data = [
// Yearly
[{axis:"Jan",value: 700},
{axis:"Feb",value: 1453},
{axis:"March",value: 1300},
{axis:"April",value: 1534},
{axis:"May",value: 1534},
{axis:"June",value: 1547},
{axis:"July",value: 1100},
{axis:"August",value: 1800},
{axis:"September",value: 1700},
{axis:"October",value: 1500},
{axis:"November",value: 1000},
{axis:"December",value: 1200}
]
];
//////////////////////////////////////////////////////////////
//////////////////// Draw the Chart //////////////////////////
//////////////////////////////////////////////////////////////
var color = d3.scaleOrdinal()
.range(["#58D6C7","#CC333F","#00A0B0"]);
var radarChartOptions = {
w: width,
h: height,
margin: margin,
maxValue: 0.5,
levels: 5,
roundStrokes: true,
color: color,
opacityCircles: 0.1
};
//Call function to draw the Radar chart
RadarChart(".radarChart", data, radarChartOptions);
/////////////////////////////////////////////////////////
/////////////// The Radar Chart Function ////////////////
/////////////// Written by Nadieh Bremer ////////////////
////////////////// VisualCinnamon.com ///////////////////
/////////// Inspired by the code of alangrafu ///////////
/////////////////////////////////////////////////////////
function RadarChart(id, data, options) {
var cfg = {
w: 600, //Width of the circle
h: 600, //Height of the circle
margin: {top: 20, right: 20, bottom: 20, left: 20}, //The margins of the SVG
levels: 3, //How many levels or inner circles should there be drawn
maxValue: 0, //What is the value that the biggest circle will represent
labelFactor: 1.25, //How much farther than the radius of the outer circle should the labels be placed
wrapWidth: 60, //The number of pixels after which a label needs to be given a new line
opacityArea: 0.35, //The opacity of the area of the blob
dotRadius: 4, //The size of the colored circles of each blog
opacityCircles: 0.1, //The opacity of the circles of each blob
strokeWidth: 2, //The width of the stroke around each blob
roundStrokes: false, //If true the area and stroke will follow a round path (cardinal-closed)
color: d3.scaleOrdinal(d3.schemeCategory10) //Color function
};
//Put all of the options into a variable called cfg
if('undefined' !== typeof options){
for(var i in options){
if('undefined' !== typeof options[i]){ cfg[i] = options[i]; }
}//for i
}//if
//If the supplied maxValue is smaller than the actual one, replace by the max in the data
var maxValue = Math.max(cfg.maxValue, d3.max(data, function(i){return d3.max(i.map(function(o){return o.value;}))}));
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"
//Scale for the radius
var rScale = d3.scaleLinear()
.range([0, radius])
.domain([0, maxValue]);
/////////////////////////////////////////////////////////
//////////// Create the container SVG and g /////////////
/////////////////////////////////////////////////////////
//Remove whatever chart with the same id/class was present before
d3.select(id).select("svg").remove();
//Initiate the radar chart SVG
var svg = d3.select(id).append("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);
//Append a g element
var g = svg.append("g")
.attr("transform", "translate(" + (cfg.w/2 + cfg.margin.left) + "," + (cfg.h/2 + cfg.margin.top) + ")");
/////////////////////////////////////////////////////////
////////// Glow filter for some extra pizzazz ///////////
/////////////////////////////////////////////////////////
//Filter for the outside glow
var filter = g.append('defs').append('filter').attr('id','glow'),
feGaussianBlur = filter.append('feGaussianBlur').attr('stdDeviation','2.5').attr('result','coloredBlur'),
feMerge = filter.append('feMerge'),
feMergeNode_1 = feMerge.append('feMergeNode').attr('in','coloredBlur'),
feMergeNode_2 = feMerge.append('feMergeNode').attr('in','SourceGraphic');
/////////////////////////////////////////////////////////
/////////////// Draw the Circular grid //////////////////
/////////////////////////////////////////////////////////
//Wrapper for the grid & axes
var axisGrid = g.append("g").attr("class", "axisWrapper");
//Draw the background circles
axisGrid.selectAll(".levels")
.data(d3.range(1,(cfg.levels+1)).reverse())
.enter()
.append("circle")
.attr("class", "gridCircle")
.attr("r", function(d, i){return radius/cfg.levels*d;})
.style("fill", "#CDCDCD")
.style("stroke", "#CDCDCD")
.style("fill-opacity", cfg.opacityCircles)
.style("filter" , "url(#glow)");
//Text indicating at what % each level is
axisGrid.selectAll(".axisLabel")
.data(d3.range(1,(cfg.levels+1)).reverse())
.enter().append("text")
.attr("class", "axisLabel")
.attr("x", 4)
.attr("y", function(d){return -d*radius/cfg.levels;})
.attr("dy", "0.4em")
.style("font-size", "10px")
.attr("fill", "#737373")
.text(function(d,i) { return Format(maxValue * d/cfg.levels); });
/////////////////////////////////////////////////////////
//////////////////// Draw the axes //////////////////////
/////////////////////////////////////////////////////////
//Create the straight lines radiating outward from the center
var axis = axisGrid.selectAll(".axis")
.data(allAxis)
.enter()
.append("g")
.attr("class", "axis");
//Append the lines
axis.append("line")
.attr("x1", 0)
.attr("y1", 0)
.attr("x2", function(d, i){ return rScale(maxValue*1.1) * Math.cos(angleSlice*i - Math.PI/2); })
.attr("y2", function(d, i){ return rScale(maxValue*1.1) * Math.sin(angleSlice*i - Math.PI/2); })
.attr("class", "line")
.style("stroke", "white")
.style("stroke-width", "2px");
//Append the labels at each axis
axis.append("text")
.attr("class", "legend")
.style("font-size", "11px")
.attr("text-anchor", "middle")
.attr("dy", "0.35em")
.attr("x", function(d, i){ return rScale(maxValue * cfg.labelFactor) * Math.cos(angleSlice*i - Math.PI/2); })
.attr("y", function(d, i){ return rScale(maxValue * cfg.labelFactor) * Math.sin(angleSlice*i - Math.PI/2); })
.text(function(d){return d})
.call(wrap, cfg.wrapWidth);
/////////////////////////////////////////////////////////
///////////// Draw the radar chart blobs ////////////////
/////////////////////////////////////////////////////////
//The radial line function
var radarLine = d3.radialLine()
.curve(d3.curveLinearClosed)
.radius(function(d) { return rScale(d.value); })
.angle(function(d,i) { return i*angleSlice; });
if(cfg.roundStrokes) {
radarLine.curve(d3.curveCardinalClosed);
}
//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)
.on('mouseover', function (d,i){
//Dim all blobs
d3.selectAll(".radarArea")
.transition().duration(200)
.style("fill-opacity", 0.1);
//Bring back the hovered over blob
d3.select(this)
.transition().duration(200)
.style("fill-opacity", 0.7);
})
.on('mouseout', function(){
//Bring back all blobs
d3.selectAll(".radarArea")
.transition().duration(200)
.style("fill-opacity", cfg.opacityArea);
});
//Create the outlines
blobWrapper.append("path")
.attr("class", "radarStroke")
.attr("d", function(d,i) { return radarLine(d); })
.style("stroke-width", cfg.strokeWidth + "px")
.style("stroke", function(d,i) { return cfg.color(i); })
.style("fill", "none")
.style("filter" , "url(#glow)");
//Append the circles
blobWrapper.selectAll(".radarCircle")
.data(function(d,i) { return d; })
.enter().append("circle")
.attr("class", "radarCircle")
.attr("r", cfg.dotRadius)
.attr("cx", function(d,i){ return rScale(d.value) * Math.cos(angleSlice*i - Math.PI/2); })
.attr("cy", function(d,i){ return rScale(d.value) * Math.sin(angleSlice*i - Math.PI/2); })
.style("fill", function(d,i,j) { return cfg.color(j); })
.style("fill-opacity", 0.8);
/////////////////////////////////////////////////////////
//////// Append invisible circles for tooltip ///////////
/////////////////////////////////////////////////////////
//Wrapper for the invisible circles on top
var blobCircleWrapper = g.selectAll(".radarCircleWrapper")
.data(data)
.enter().append("g")
.attr("class", "radarCircleWrapper");
//Append a set of invisible circles on top for the mouseover pop-up
blobCircleWrapper.selectAll(".radarInvisibleCircle")
.data(function(d,i) { return d; })
.enter().append("circle")
.attr("class", "radarInvisibleCircle")
.attr("r", cfg.dotRadius*1.5)
.attr("cx", function(d,i){ return rScale(d.value) * Math.cos(angleSlice*i - Math.PI/2); })
.attr("cy", function(d,i){ return rScale(d.value) * Math.sin(angleSlice*i - Math.PI/2); })
.style("fill", "none")
.style("pointer-events", "all")
.on("mouseover", function(d,i) {
newX = parseFloat(d3.select(this).attr('cx')) - 10;
newY = parseFloat(d3.select(this).attr('cy')) - 10;
tooltip
.attr('x', newX)
.attr('y', newY)
.text(Format(d.value))
.transition().duration(200)
.style('opacity', 1);
})
.on("mouseout", function(){
tooltip.transition().duration(200)
.style("opacity", 0);
});
//Set up the small tooltip for when you hover over a circle
var tooltip = g.append("text")
.attr("class", "tooltip")
.style("opacity", 0);
/////////////////////////////////////////////////////////
/////////////////// Helper Function /////////////////////
/////////////////////////////////////////////////////////
//Taken from http://bl.ocks.org/mbostock/7555321
//Wraps SVG text
function wrap(text, width) {
text.each(function() {
var text = d3.select(this),
words = text.text().split(/\s+/).reverse(),
word,
line = [],
lineNumber = 0,
lineHeight = 1.4, // ems
y = text.attr("y"),
x = text.attr("x"),
dy = parseFloat(text.attr("dy")),
tspan = text.text(null).append("tspan").attr("x", x).attr("y", y).attr("dy", dy + "em");
while (word = words.pop()) {
line.push(word);
tspan.text(line.join(" "));
if (tspan.node().getComputedTextLength() > width) {
line.pop();
tspan.text(line.join(" "));
line = [word];
tspan = text.append("tspan").attr("x", x).attr("y", y).attr("dy", ++lineNumber * lineHeight + dy + "em").text(word);
}
}
});
}//wrap
}//RadarChart
For rotating you just need to increase your initial angle offset over time.
Here's an example:
https://bl.ocks.org/tezzutezzu/c9d8706587e8f5b5d72084b083b502f8
As we know the angle is dependent to the index of the datum, you can calculate the value of the red dots under the red line in this way.
var currentAngle = (offset - Math.PI/2) % (Math.PI*2);
var currentIndex = Math.floor( (currentAngle/(Math.PI*2)) * data[0].length);
var currentValue = data[0][currentIndex].value;