Related
demo() function is a pie chart i have a toggle button with check box toggle button whenever i toggle it demo() function is calling which is fine but it is creating copy of the pie chart whenever i am clicking on check box(toggle button) it changes the value around the chart which is working fine but it is calling the pie chart function again and again and multiple pie chart are creating with toggle button. what should i do get the new chart only once i mean it will not create a copy of pie chart and remove the old one?
async function renderTradesInfo() {
let tradeOrders = await TradesInformation();
totalTrades = tradeOrders.data.total_orders;
let ThirtydOrders = tradeOrders.data.orders_placed_30d;
let ThirtydOrder = `${ThirtydOrders}`;
let thirtydOrderTrade= $("#tradeCharts").find("#thirtydOrderTrade");
thirtydOrderTrade.html(ThirtydOrder);
let checkedValue = $('#checkbox1').is(':checked');
if(!checkedValue){
closedOrdersTrade(tradeOrders);
totalTrades = tradeOrders.data.total_orders;
demo(totalTrades);
}
$('#checkbox1').on('change', function () {
if ($(this).is(':checked')){
let openProfitOrders = tradeOrders.data.open_profit_orders;
let openProfitOrder = `${openProfitOrders} Profit`;
let tradeChartProfit= $("#tradeCharts").find(".tradeChartProfit");
tradeChartProfit.html(openProfitOrder);
let openLossOrders = tradeOrders.data.open_loss_orders;
let openLossOrder = `${openLossOrders} Loss`;
let tradeChartLoss= $("#tradeCharts").find(".tradeChartLoss");
tradeChartLoss.html(openLossOrder);
// totalTrades = tradeOrders.data.open_orders;
demo(totalTrades);
}
else {
closedOrdersTrade(tradeOrders);
// totalTrades = tradeOrders.data.closed_orders;
demo(totalTrades);
}
});
}
This is the piechart function which i want to update on toggle but it is creating new piechart
function demo(totalTrades){
const chartData = [
{ name: 'Loss', value: 10},
{ name: 'Profit', value: 90}
]
const colors = ['#EB5757', '#1EE8B7']
const margin = { top: 10, right: 0, bottom: 0, left: 45 },
width = 190 - margin.right - margin.left,
height = 210,
radius = width / 2
const colorScale = d3.scaleOrdinal().range(colors)
const arc = d3.arc()
.outerRadius(radius - 10)
.innerRadius(radius - 25)
const textArc = d3.arc()
.outerRadius(radius - 50)
.innerRadius(radius - 50)
const pie = d3.pie()
.sort(null)
.value(d => d.value)
const svg = d3.select('.wrapper')
.append('svg')
.attr('transform', `translate(${margin.left}, 0)`)
.attr('width', width)
.attr('height', height)
.append('g')
.attr('transform', `translate(${width / 2}, ${height / 2 - 30})`)
const g = svg.selectAll('.arc')
.data(pie(chartData))
.enter().append('g')
.attr('class', 'arc')
g.append('path')
.attr('d', arc)
.style('fill', d => colorScale(d.data.name))
.style('stroke', 'none')
.transition()
.ease(d3.easeLinear)
.duration(1000)
.attrTween('d', chartTween)
// g.append('text')
// .transition()
// .ease(d3.easeLinear)
// .duration(1000)
// .attr('transform', d => `translate(${textArc.centroid(d)})`)
// .attr('dx', '-3em')
// .attr('dy', '.75em')
// .text(d => d.data.value)
// .attr('font-size', '20px')
// .attr('fill', '#fff')
g.append('text')
.html('Total Trades')
.attr('text-anchor', 'middle')
.attr('class', 'center-text')
.attr('transform', d => 'translate(0, -10)')
.attr('fill', '#fff')
g.append('text')
.html(totalTrades)
.attr('text-anchor', 'middle')
.attr('class', 'center-text')
.attr('transform', d => 'translate(0, 10)')
.attr('fill', '#FFFFFF')
svg.append('g')
.attr('class', 'legendLinear')
.attr('transform', 'translate(70, 330)')
const colorLegend = d3.legendColor()
.orient('horizontal')
.shapeWidth(60)
.scale(colorScale)
svg.select('.legendLinear')
.call(colorLegend)
// helper functions
function chartTween(b) {
b.innerRadius = 0
const i = d3.interpolate({ startAngle: 0, endAngle: 0 }, b)
return function (t) { return arc(i(t)) }
}
}
Hi i figured out how to add log labels at Y axis axis but cant make chart line scale log, and i dont really know how to make X axis labels plain text instead of converting it to date
If i just change scaleLinear to scaleLog chart line dissapear
Also removing const parseTime = d3.timeParse('%Y/%m/%d'); and change parseTime(val.date) just to val.date, and changing scaleTime to scaleOrdinal does not work as i would
Also i don't understand why Y axis labels are so blured
Sorry for asking but cant findout how this pluggin works.
using D3 7.2.1 and JQ 3.5.1
(function (d3){
const lineChartData = [
{
currency: "data",
values: [
{
date: "2018/01/01",
close: 0
},
{
date: "2018/02/01",
close: 5
},
{
date: "2018/03/01",
close: 10
},
{
date: "2018/04/01",
close: 50
},
{
date: "2018/05/01",
close: 100
},
{
date: "2018/06/01",
close: 500
},
{
date: "2018/07/01",
close: 1000
},
{
date: "2018/08/01",
close: 5000
},
{
date: "2018/09/01",
close: 10000
},
]
}
];
const margin = {
top: 20,
bottom: 20,
left: 50,
right: 20
};
const width = 400 - margin.left - margin.right;
const height = 300 - margin.top - margin.bottom;
const createGradient = select => {
const gradient = select
.select('defs')
.append('linearGradient')
.attr('id', 'gradient')
.attr('x1', '0%')
.attr('y1', '100%')
.attr('x2', '0%')
.attr('y2', '0%');
gradient
.append('stop')
.attr('offset', '0%')
.attr('style', 'stop-color:#FF6500; stop-opacity:0');
gradient
.append('stop')
.attr('offset', '100%')
.attr('style', 'stop-color:#FF6500; stop-opacity: 1');
}
const createGlowFilter = select => {
const filter = select
.select('defs')
.append('filter')
.attr('id', 'glow')
//stdDeviation is px count for make blur around main chart line
filter
.append('feGaussianBlur')
.attr('stdDeviation', '0')
.attr('result', 'coloredBlur');
const femerge = filter
.append('feMerge');
femerge
.append('feMergeNode')
.attr('in', 'coloredBlur');
femerge
.append('feMergeNode')
.attr('in', 'SourceGraphic');
}
const svg = d3.select('#line-chart')
.append('svg')
.attr('width', 700 + margin.left + margin.right)
.attr('height', 300 + margin.top + margin.bottom)
.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`);
svg.append('defs');
svg.call(createGradient);
svg.call(createGlowFilter);
const parseTime = d3.timeParse('%Y/%m/%d');
const parsedData = lineChartData.map(company => ({
ticker: company.ticker,
values: company.values.map(val => ({
close: val.close,
date: parseTime(val.date)
}))
}));
const xScale = d3.scaleTime()
.domain([
d3.min(parsedData, d => d3.min(d.values, v => v.date)),
d3.max(parsedData, d => d3.max(d.values, v => v.date))
])
.range([0, width]);
const yScale = d3.scaleLinear()
.domain([
d3.min(parsedData, d => d3.min(d.values, v => v.close)),
d3.max(parsedData, d => d3.max(d.values, v => v.close))
])
.range([height, 0]);
const line = d3.line()
.x(d => xScale(d.date))
.y(d => yScale(d.close))
.curve(d3.curveCatmullRom.alpha(0.5));
svg.selectAll('.line')
.data(parsedData)
.enter()
.append('path')
.attr('d', d => {
const lineValues = line(d.values).slice(1);
const splitedValues = lineValues.split(',');
return `M0,${height},${lineValues},l0,${height - splitedValues[splitedValues.length - 1]}`
})
.style('fill', 'url(#gradient)')
svg.selectAll('.line')
.data(parsedData)
.enter()
.append('path')
.attr('d', d => line(d.values))
.attr('stroke-width', '2')
.style('fill', 'none')
.style('filter', 'url(#glow)')
.attr('stroke', '#FF6500');
const tick = svg.append('g')
.attr('transform', `translate(0, ${height})`)
.call(d3.axisBottom(xScale).ticks(9))
.selectAll('.tick')
.style('transition', '.2s');
//Y dashes
//stroke color of line in background
//stroke-dasharray
//first paramter is length
//second parameter is space between
tick
.selectAll('line')
.attr('stroke-dasharray', `4, 7`)
.attr('stroke', '#5E779B')
.attr('y2', `-${height}px`)
tick
.append('rect')
.attr('width', `${(width / 12) + 10}px`)
.attr('x', `-${width / 24 + 5}px`)
.attr('y', `-${height}px`)
.attr('height', `${height + 30}px`)
.style('fill', 'transparent');
svg.selectAll('.tick')
.append('circle')
.attr('r', '5px')
.style('fill', '#ffffff')
.style('stroke', '#FF6500')
.attr('cy', (x, i) => - height + yScale(parsedData[0].values[i].close));
svg.select('.domain')
.attr('stroke', '#5E779B')
.attr('stroke-dasharray', `4, 7`)
var yscale = d3.scaleLog()
.domain([1, 100000])
.nice()
.range([height - 10, -10]);
var y_axis = d3.axisLeft(yscale);
y_axis.ticks(5);
svg.append("g")
.call(d3.axisLeft(xScale).ticks(5))
.attr("transform", "translate(0, 10)")
.attr('stroke', '#5E779B')
.attr('stroke-dasharray', `4, 7`)
.call(y_axis)
})
(d3);
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<body>
<div id="line-chart"></div>
</body>
I'll start with the log scale issues. You have two y-scales, yScale and yscale. The first is a linear scale that is used in the line generator and to position the circles. The second is a log scale that is only used for the axis. You should only have one y-scale and use it both for positioning the elements and for the axis.
Also, log scales cannot handle the value 0:
As log(0) = -∞, a log scale domain must be strictly-positive or strictly-negative; the domain must not include or cross zero.
The data point that has a close value of 0 cannot be shown on a log scale.
Next, the y-axis labels look wrong because of the "stroke" and "stroke-dasharray" attributes that you set on the y-axis group.
Finally, for the x-scale you are correct to convert the strings to Date objects and use a d3.scaleTime. For the x-axis, you can then do something like this:
const xAxis = d3.axisBottom(xScale)
.ticks(d3.timeMonth.every(1), '%b');
The first argument to ticks says that you want to have one tick for each month. The second argument is a date format specifier that defines how the tick labels should be formatted. %b puts an abbreviated month name at each tick mark. If you want the tick marks to have the same format as the original string, then you can use %Y/%m/%d, but you might find that the labels are too long and overlap each other.
Here is an example that fixes the issues mentioned above.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="https://d3js.org/d3.v7.js"></script>
</head>
<body>
<div id="line-chart"></div>
<script>
// data
const lineChartData = [
{
ticker: "ABC",
values: [
{
date: "2018/01/01",
close: 1
},
{
date: "2018/02/01",
close: 5
},
{
date: "2018/03/01",
close: 10
},
{
date: "2018/04/01",
close: 50
},
{
date: "2018/05/01",
close: 100
},
{
date: "2018/06/01",
close: 500
},
{
date: "2018/07/01",
close: 1000
},
{
date: "2018/08/01",
close: 5000
},
{
date: "2018/09/01",
close: 10000
},
]
}
];
const parseTime = d3.timeParse('%Y/%m/%d');
const parsedData = lineChartData.map(company => ({
ticker: company.ticker,
values: company.values.map(val => ({
close: val.close,
date: parseTime(val.date)
}))
}));
// gradient
const createGradient = defs => {
const gradient = defs.append('linearGradient')
.attr('id', 'gradient')
.attr('x1', '0%')
.attr('y1', '100%')
.attr('x2', '0%')
.attr('y2', '0%');
gradient.append('stop')
.attr('offset', '0%')
.attr('style', 'stop-color:#FF6500; stop-opacity:0');
gradient.append('stop')
.attr('offset', '100%')
.attr('style', 'stop-color:#FF6500; stop-opacity: 1');
}
// filter
const createGlowFilter = defs => {
const filter = defs.append('filter')
.attr('id', 'glow')
//stdDeviation is px count for make blur around main chart line
filter.append('feGaussianBlur')
.attr('stdDeviation', '2')
.attr('result', 'coloredBlur');
const femerge = filter.append('feMerge');
femerge.append('feMergeNode')
.attr('in', 'coloredBlur');
femerge.append('feMergeNode')
.attr('in', 'SourceGraphic');
}
// set up
const margin = { top: 20, right: 20, bottom: 20, left: 50 };
const width = 400 - margin.left - margin.right;
const height = 300 - margin.top - margin.bottom;
const svg = d3.select('#line-chart')
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.call(svg => svg.append('defs')
.call(createGradient)
.call(createGlowFilter))
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
// scales
const xScale = d3.scaleTime()
.domain([
d3.min(parsedData, d => d3.min(d.values, v => v.date)),
d3.max(parsedData, d => d3.max(d.values, v => v.date))
])
.range([0, width]);
const yScale = d3.scaleLog()
.domain([
d3.min(parsedData, d => d3.min(d.values, v => v.close)),
d3.max(parsedData, d => d3.max(d.values, v => v.close))
])
.range([height, 0]);
// line and area generators
const area = d3.area()
.x(d => xScale(d.date))
.y1(d => yScale(d.close))
.y0(yScale(yScale.domain()[0]))
.curve(d3.curveCatmullRom.alpha(0.5));
const line = area.lineY1();
// axes
const xAxis = d3.axisBottom(xScale)
.ticks(d3.timeMonth.every(1), '%b');
svg.append('g')
.attr('transform', `translate(0,${height})`)
.call(xAxis)
// add vertical grid lines
.call(g =>
g.selectAll('.tick>line')
.clone()
.attr('stroke', '#5E779B')
.attr('stroke-dasharray', '4, 7')
.attr('y0', 0)
.attr('y1', -height)
)
const yAxis = d3.axisLeft(yScale)
.ticks(5);
svg.append('g')
.call(yAxis);
// draw area, line, and circles
// create one group for each company
const companies = svg.append('g')
.selectAll('g')
.data(parsedData)
.join('g');
// add the area
companies.append('path')
.attr('d', d => area(d.values))
.attr('fill', 'url(#gradient)');
// add the line
companies.append('path')
.attr('d', d => line(d.values))
.attr('stroke-width', 2)
.attr('fill', 'none')
.attr('stroke', '#FF6500')
.attr('filter', 'url(#glow)');
// add the circles
companies.selectAll('circle')
.data(d => d.values)
.join('circle')
.attr('fill', 'white')
.attr('stroke', '#FF6500')
.attr('r', '3')
.attr('cx', d => xScale(d.date))
.attr('cy', d => yScale(d.close));
</script>
</body>
</html>
I'm trying to append a line to an end of an area chart path. The most difficult part my area is animated. I have a clipPath which width being transformed from width: 0 to width: 960 and the line at the end goes along with it so shoud be synchronised. Also the text on top of that line needs to be updated while it goes along.
Desired output:
My initial idea was to build a chart area and add a clipPath and then add a bar chart inside of area chart so I can update my text based on the bar appended, however bars are not inside my area chart. What am I doing wrong to place bars inside area chart or is there a better solution to this?
// Area chart width and height
const width1 = 1000,
height1 = 100;
// Define x and y scale for area chart
const xScale1 = d3.scaleTime().range([0, width1]);
const yScale1 = d3.scaleLinear().range([height1, 0]);
// Define x and y range for bar chart
let xScale2 = d3.scaleBand().range([0, width1]);
let yScale2 = d3.scaleLinear().range([height1, 0]);
// Add SVG to #areachart
const svg1 = d3
.select('#areachart')
.append('svg')
.attr('viewBox', `0 0 ${width1} ${height1}`)
.attr('transform', 'translate(' + 0 + ',' + -50 + ')');
const g1 = svg1.append('g');
// Fetch data
d3.json(
'https://api.coronavirus.data.gov.uk/v1/data?filters=areaName=United%2520Kingdom;areaType=overview&structure=%7B%22areaType%22:%22areaType%22,%22areaName%22:%22areaName%22,%22areaCode%22:%22areaCode%22,%22date%22:%22date%22,%22newCasesByPublishDate%22:%22newCasesByPublishDate%22,%22cumCasesByPublishDate%22:%22cumCasesByPublishDate%22%7D&format=json'
)
.then(function(data) {
console.log('DATES SLICED ----->', data.data.slice(30, 281));
//Define xScale1 & yScale1 domain after data loaded
yScale1.domain([
0,
d3.max(data.data, function(d) {
return +d.cumCasesByPublishDate;
}),
]);
xScale1.domain(
d3.extent(data.data, function(d) {
return new Date(d.date);
})
);
// Area generator
const area = d3
.area()
.curve(d3.curveStepAfter)
.x((d) => xScale1(new Date(d.date)))
.y1((d) => yScale1(+d.cumCasesByPublishDate))
.y0(yScale1(0));
g1.append('path')
.datum(data.data.slice(30, 200))
.attr('d', area)
.classed('placeholder-layer', true)
.style('fill', '#dadada')
.style('opacity', '0.3');
// clipPath for areachart fill animation
const clip = g1.append('clipPath').attr('id', 'clip');
const clipRect = clip.append('rect').attr('width', 0).attr('height', 750);
g1.append('path')
.datum(data.data.slice(30, 200))
.attr('d', area)
.attr('clip-path', 'url(#clip)')
.classed('overlay-layer', true)
.style('fill', 'yellow')
.style('opacity', '0.3');
g1.append('line').attr('stroke-width', 960).style('stroke', 'yellow');
clipRect
.transition()
.duration(10000)
.ease(d3.easeLinear)
.attr('width', 960);
//x and y domain for bar chart
xScale2.domain(data.data.slice(30, 200).map((d) => new Date(d.date)));
yScale2.domain([
0,
d3.max(data.data, function(d) {
return +d.cumCasesByPublishDate;
}),
]);
g1.selectAll('rect')
.data(data.data.slice(30, 200))
.enter()
.append('rect')
.style('fill', 'red')
.attr('width', xScale2.bandwidth() * 10)
.attr('height', (d) => yScale2(+d.cumCasesByPublishDate))
.attr('x', 0)
.attr('y', function(d) {
return yScale2(+d.cumCasesByPublishDate);
})
.transition()
.delay(function(d, i) {
return i * 30;
})
.attr('x', function(d) {
return xScale2(new Date(d.date));
})
.duration(100);
})
// if there's an error, log it
.catch((error) => console.log(error));
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<section id="map">
<div id="visualisation-container">
<div id="visualisation"></div>
<div id="areachart"></div>
</div>
</section>
I'd just use a line, no bars, and use transition.tween with d3.interpolateDate to make the text change.
// Area chart width and height
const width1 = 800,
height1 = 250,
marginBottom = 50
// Define x and y scale for area chart
const xScale1 = d3.scaleTime().range([0, width1]);
const yScale1 = d3.scaleLinear().range([height1, marginBottom]);
// Define x and y range for bar chart
let xScale2 = d3.scaleBand().range([0, width1]);
let yScale2 = d3.scaleLinear().range([height1, marginBottom]);
// Add SVG to #areachart
const svg1 = d3
.select('#areachart')
.append('svg')
.attr('width', width1)
.attr('height', height1);
const g1 = svg1.append('g');
// Fetch data
d3.json(
'https://api.coronavirus.data.gov.uk/v1/data?filters=areaName=United%2520Kingdom;areaType=overview&structure=%7B%22areaType%22:%22areaType%22,%22areaName%22:%22areaName%22,%22areaCode%22:%22areaCode%22,%22date%22:%22date%22,%22newCasesByPublishDate%22:%22newCasesByPublishDate%22,%22cumCasesByPublishDate%22:%22cumCasesByPublishDate%22%7D&format=json'
)
.then(data => {
data.data.forEach(d => {
d.cumCasesByPublishDate = +d.cumCasesByPublishDate;
d.date = new Date(d.date);
});
return data.data.slice(30, 200);
})
.then(function(data) {
//Define xScale1 & yScale1 domain after data loaded
yScale1.domain([
0,
d3.max(data, d => d.cumCasesByPublishDate),
]);
xScale1.domain(
d3.extent(data, d => d.date)
);
// Area generator
const area = d3
.area()
.curve(d3.curveStepAfter)
.x((d) => xScale1(d.date))
.y1((d) => yScale1(d.cumCasesByPublishDate))
.y0(yScale1(0));
g1.append('path')
.datum(data)
.attr('d', area)
.classed('placeholder-layer', true)
.style('fill', '#dadada')
.style('opacity', '0.3');
// clipPath for areachart fill animation
const clip = g1.append('clipPath').attr('id', 'clip');
const clipRect = clip.append('rect').attr('width', 0).attr('height', 750);
g1.append('path')
.datum(data)
.attr('d', area)
.attr('clip-path', 'url(#clip)')
.classed('overlay-layer', true)
.style('fill', 'yellow')
.style('opacity', '0.3');
const format = d3.timeFormat("%B %d, %Y");
const duration = 10000;
g1.append('line')
.attr('stroke-width', 5)
.style('stroke', 'black')
.attr('x1', xScale1.range()[0])
.attr('x2', xScale1.range()[0])
.attr('y1', yScale1.range()[0])
.attr('y2', yScale1.range()[1])
.transition()
.duration(duration)
.ease(d3.easeLinear)
.attr('x1', xScale1.range()[1])
.attr('x2', xScale1.range()[1])
g1.append('text')
.attr('x', xScale1.range()[0])
.attr('y', marginBottom / 2)
.attr('text-anchor', 'middle')
.transition()
.duration(duration)
.ease(d3.easeLinear)
.attr('x', xScale1.range()[1])
.tween('text', function() {
const i = d3.interpolateDate(xScale1.domain()[0], xScale1.domain()[1]);
return (t) => d3.select(this).text(format(i(t)));
})
})
// if there's an error, log it
.catch((error) => console.log(error));
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<section id="map">
<div id="visualisation-container">
<div id="visualisation"></div>
<div id="areachart"></div>
</div>
</section>
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>
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>