I am trying to set the quantity value inside of each individual bar in my bar graph like the image I have provided below:
Unfortunately, the code I have tried hovers the percentage in a really weird spot and I'm not sure what I can do to achieve the desired effect.
Here is my code:
import React, { useEffect, useRef, useState } from 'react';
import * as d3 from 'd3';
import './BarChart.css';
const dataSet = [
{ category: '1', quantity: 15 },
{ category: '2', quantity: 10 },
{ category: '3', quantity: 50 },
{ category: '4', quantity: 30 },
{ category: '4', quantity: 75 },
{ category: '5', quantity: 5 }
];
const BarChartTest = () => {
const d3Chart = useRef();
const [dimensions, setDimensions] = useState({
width: window.innerWidth,
height: window.innerHeight
});
const update = useRef(false);
useEffect(() => {
// Listen for any resize event update
window.addEventListener('resize', () => {
setDimensions({
width: window.innerWidth,
height: window.innerHeight
});
// if resize, remove the previous chart
if (update.current) {
d3.selectAll('g').remove();
} else {
update.current = true;
}
});
DrawChart(dataSet, dimensions);
}, [dimensions]);
const margin = { top: 50, right: 30, bottom: 30, left: 60 };
const DrawChart = (data, dimensions) => {
const chartWidth = parseInt(d3.select('#d3RenewalChart').style('width')) - margin.left - margin.right;
const chartHeight = parseInt(d3.select('#d3RenewalChart').style('height')) - margin.top - margin.bottom;
const colors = ['#7fc97f', '#beaed4', '#fdc086', '#ffff99', '#386cb0', '#f0027f', '#bf5b17', '#666666'];
const svg = d3
.select(d3Chart.current)
.attr('width', chartWidth + margin.left + margin.right)
.attr('height', chartHeight + margin.top + margin.bottom);
const x = d3
.scaleBand()
.domain(d3.range(data.length))
.range([margin.left, chartWidth + margin.right])
.padding(0.1);
svg.append('g')
.attr('transform', 'translate(0,' + chartHeight + ')')
.call(
d3
.axisBottom(x)
.tickFormat((i) => data[i].category)
.tickSizeOuter(0)
);
const max = d3.max(data, function (d) {
return d.quantity;
});
const y = d3.scaleLinear().domain([0, 100]).range([chartHeight, margin.top]);
svg.append('g')
.attr('transform', 'translate(' + margin.left + ',0)')
.call(d3.axisLeft(y).tickFormat((d) => d + '%'));
svg.append('g')
.attr('fill', function (d, i, j) {
return colors[i];
})
.selectAll('rect')
.data(data)
.join('rect')
.attr('x', (d, i) => x(i))
.attr('y', (d) => y(d.quantity))
.attr('height', (d) => y(0) - y(d.quantity))
.attr('width', x.bandwidth())
.attr('fill', function (d, i) {
return colors[i];
})
.append('text')
.text(function (d) {
return d.quantity;
})
.on('click', (d) => {
location.replace('https://www.google.com');
});
svg.selectAll('.text')
.data(data)
.enter()
.append('text')
// .attr('text-anchor', 'middle')
.attr('fill', 'green')
.attr('class', 'label')
.attr('x', function (d) {
return x(d.quantity);
})
.attr('y', function (d) {
return y(d.quantity) - 20;
})
.attr('dy', '0')
.text(function (d) {
return d.quantity + '%';
})
.attr('x', function (d, i) {
console.log(i * (chartWidth / data.length));
return i * (chartWidth / data.length);
})
.attr('y', function (d) {
console.log(chartHeight - d.quantity * 4);
return chartHeight - d.quantity * 4;
});
};
return (
<div id="d3RenewalChart">
<svg ref={d3Chart}></svg>
</div>
);
};
export default BarChartTest;
Here is a link to my codesandbox.
The Codesandbox provided didn't contain any React code.
Copy-pasting the above into the component code and calling it from App.js revealed that resizing the window would cause problems, because the line svg.selectAll(".text") was having a fresh copy appended with every render (re-size).
Here is the original code in a working Codesandbox.
A refactored version of that code is in this updated Codesandbox.
Solution:
In addition to the .append() call appending the .text element to the svg without being removed, the code above appears to set the x and y attributes with .attr() twice; removing the additional code and changing a few values made it possible to position the bar labels in what is presumably the correct position.
Here's a refactored version:
// create labels
svg
.append("g")
.attr("fill", "black")
.attr("text-anchor", "end")
.style("font", "24px sans-serif")
.selectAll("text")
.data(data)
.enter()
.append("text")
.attr("class", "label");
// position labels
svg
.selectAll(".label")
.data(data)
.attr("x", (d, index) => x(index) + x.bandwidth() / 2 + 24)
.text((d) => d.quantity + "%")
// to exclude the animation, remove these two lines
.transition()
.delay((d, i) => i * 20)
//
.attr("y", (d) => y(d.quantity) + 22);
On another note, a color with higher contrast is advised here. White text on lighter backgrounds may not be visible and won't provide an accessible experience for everyone. Here's the refactored Codesandbox again.
Hope this was helpful! ✌️
Related
I try to do a beeswarm plot with different radius; inspired by this code
The issue I have, is that my point are offset regarding my x axis:
The point on the left should be at 31.7%. I don't understand why, so I would appreciate if you could guide me. This could be improved by changing the domain of x scale, but this can't match the exact value; same issue if I remove the d3.forceCollide()
Thank you,
Data are available here.
Here is my code:
$(document).ready(function () {
function tp(d) {
return d.properties.tp60;
}
function pop_mun(d) {
return d.properties.pop_mun;
}
var margin = {top: 20, right: 20, bottom: 20, left: 40},
width = 1280 - margin.right - margin.left,
height = 300 - margin.top - margin.bottom;
var svg = d3.select("body")
.append("svg")
.attr("viewBox", `0 0 ${width} ${height}`)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var z = d3.scaleThreshold()
.domain([.2, .3, .4, .5, .6, .7])
.range(["#35ff00", "#f1a340", "#fee0b6",
"#ff0000", "#998ec3", "#542788"]);
var loading = svg.append("text")
.attr("x", (width) / 2)
.attr("y", (height) / 2)
// .attr("dy", ".35em")
.style("text-anchor", "middle")
.text("Simulating. One moment please…");
var formatPercent = d3.format(".0%"),
formatNumber = d3.format(".0f");
d3.json('static/data/qp_full.json').then(function (data) {
features = data.features
//1 create scales
var x = d3.scaleLinear()
.domain([0, d3.max(features, tp)/100])
.range([0, width - margin.right])
var y = d3.scaleLinear().domain([0, 0.1]).range([margin.left, width - margin.right])
var r = d3.scaleSqrt().domain([0, d3.max(features, pop_mun)])
.range([0, 25]);
//2 create axis
var xAxis = d3.axisBottom(x).ticks(20)
.tickFormat(formatPercent);
svg.append("g")
.attr("class", "x axis")
.call(xAxis);
var nodes = features.map(function (node, index) {
return {
radius: r(node.properties.pop_mun),
color: '#ff7f0e',
x: x(node.properties.tp60 / 100),
y: height + Math.random(),
pop_mun: node.properties.pop_mun,
tp60: node.properties.tp60
};
});
function tick() {
for (i = 0; i < nodes.length; i++) {
var node = nodes[i];
node.cx = node.x;
node.cy = node.y;
}
}
setTimeout(renderGraph, 10);
function renderGraph() {
// Run the layout a fixed number of times.
// The ideal number of times scales with graph complexity.
// Of course, don't run too long—you'll hang the page!
const NUM_ITERATIONS = 1000;
var force = d3.forceSimulation(nodes)
.force('charge', d3.forceManyBody().strength(-3))
.force('center', d3.forceCenter(width / 2, height/2))
.force('x', d3.forceX(d => d.x))
.force('y', d3.forceY(d => d.y))
.force('collide', d3.forceCollide().radius(d => d.radius))
.on("tick", tick)
.stop();
force.tick(NUM_ITERATIONS);
force.stop();
svg.selectAll("circle")
.data(nodes)
.enter()
.append("circle")
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("r", d => d.radius)
.style("fill", d => z(d.tp60/100))
.on("mouseover", function (d, i) {
d3.select(this).style('fill', "orange")
console.log(i.tp60,i)
svg.append("text")
.attr("id", "t")
.attr("x", function () {
return d.x - 50;
})
.attr("y", function () {
return d.y - 50;
})
.text(function () {
return [x.invert(i.x), i.tp60]; // Value of the text
})
})
.on("mouseout", function (d, i) {
d3.select("#t").remove(); // Remove text location
console.log(i)
d3.select(this).style('fill', z(i.tp60/100));
});
loading.remove();
}
})
})
I wanted to update my d3 graph when user select the site. But when I select the dropdown menu up top, I get new graph being generated and I cannot seem to find a way to remove it.
this is the code in <script>
<script>
import * as d3 from 'd3'
import { rgbaToHex } from '../utils/color.ts'
export default {
data () {
return {
selectedSite: '',
selectedWhale: '',
groupType: '',
heatmapShow: false
}
},
mounted () {
this.generateChart()
},
methods: {
generateChart () {
// set the dimensions and margins of the graph
const margin = { top: 20, right: 20, bottom: 30, left: 30 }
const width = 1850 - margin.left - margin.right
const height = 200 - margin.top - margin.bottom
// make the area for the graph to stay
const svg = d3.select('#heatmap')
.append('svg') // svg area can include headers and color scales
.attr('width', width + margin.left + margin.right) // set width
.attr('height', height + margin.top + margin.bottom) // set height
.append('g') // new g tag area for graph only
.attr('transform', `translate(${margin.left}, ${margin.bottom})`)
// stick g tag to the bottom
const xLabel = d3.range(259)
const yLabel = d3.range(23, -1, -1)
const x = d3.scaleBand()
.domain(xLabel)
.range([0, width])
.padding(0.05)
svg.append('g')
.attr('transform', `translate(0, ${height})`)
.call(d3.axisBottom(x).tickValues(x.domain().filter((_, i) => !(i % 6))))
const y = d3.scaleBand()
.domain(yLabel)
.range([height, 0])
.padding(0.05)
svg.append('g').call(d3.axisLeft(y).tickValues(y.domain().filter((_, i) => !(i % 5))))
d3.json('../predictions.json').then((data) => {
const u = svg.selectAll().data(data.heatmaps[this.selectedWhale][this.selectedSite])
u.exit().remove()
const uEnter = u.enter().append('rect')
uEnter
.merge(u)
.transition()
.duration(1000)
.attr('x', function (d) {
return x(d[1]) // return cell's position
})
.attr('y', function (d) {
return y(d[0])
})
.attr('cx', 1)
.attr('cy', 1)
.attr('width', x.bandwidth()) // return cell's width
.attr('height', y.bandwidth()) // return cell's height
.style('fill', function (d) {
return rgbaToHex(0, 128, 255, 100 * d[2])
})
.on('mouseover', function () { // box stroke when hover
d3.select(this)
.style('stroke', 'black')
.style('opacity', 1)
})
.on('mouseout', function () { // fade block stroke when mouse leave the cell
d3.select(this)
.style('stroke', 'none')
.style('opacity', 0.8)
})
.on('click', (d) => {
console.log(d)
this.heatmapShow = true
})
uEnter.exit().remove()
})
}
},
watch: {
selectedSite: function () {
this.generateChart()
},
selectedWhale: function () {
this.generateChart()
},
groupType: function (value) {
console.log(value)
}
}
}
</script>
It appears as if you're selecting the ID to paint your graph canvas into it but you append it instead of inserting a new one.
selection.append(type)
If the specified type is a string, appends a new element of this type (tag name) as the last child of each selected element, or before the next following sibling in the update selection if this is an enter selection.
More on he subject is written here.
Try removing the ID before repainting it with selection remove and then try to append/insert again
So what I've done to fix it was, as was suggested before, just add d3.select('svg').remove() before I start creating the svg const again.
const margin = { top: 20, right: 20, bottom: 30, left: 30 }
const width = 1850 - margin.left - margin.right
const height = 200 - margin.top - margin.bottom
d3.select('svg').remove()
// make the area for the graph to stay
const svg = d3.select('#heatmap')
.append('svg') // svg area can include headers and color scales
/* rest of the code */
I am creating a stacked bar graph which should update on changing data and I want to use d3v5 and selection.join as explained here https://observablehq.com/#d3/learn-d3-joins?collection=#d3/learn-d3.
When entering the data everything works as expected, however the update function is never called (that's the console.log() for debugging.).
So it looks like it is just entering new data all the time.
How can I get this to work as expected?
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body {
margin: 0;
}
.y.axis .domain {
display: none;
}
</style>
</head>
<body>
<div id="chart"></div>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script>
let xVar = "year";
let alphabet = "abcdef".split("");
let years = [1996, 1997, 1998, 1999, 2000, 2001, 2002, 2003];
let margin = { left:80, right:20, top:50, bottom:100 };
let width = 600 - margin.left - margin.right,
height = 600 - margin.top - margin.bottom;
let g = d3.select("#chart")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + ", " + margin.top + ")");
let color = d3.scaleOrdinal(["#66c2a5","#fc8d62","#8da0cb","#e78ac3","#a6d854","#ffd92f"])
let x = d3.scaleBand()
.rangeRound([0, width])
.domain(years)
.padding(.25);
let y = d3.scaleLinear()
.rangeRound([height, 0]);
let xAxis = d3.axisBottom(x);
let yAxis = d3.axisRight(y)
.tickSize(width)
g.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
g.append("g")
.attr("class", "y axis")
.call(yAxis);
let stack = d3.stack()
.keys(alphabet)
.order(d3.stackOrderNone)
.offset(d3.stackOffsetNone);
redraw(randomData());
d3.interval(function(){
redraw(randomData());
}, 1000);
function redraw(data){
// update the y scale
y.domain([0, d3.max(data.map(d => d.sum ))])
g.select(".y")
.transition().duration(1000)
.call(yAxis);
groups = g.append('g')
.selectAll('g')
.data(stack(data))
.join('g')
.style('fill', (d,i) => color(d.key));
groups.selectAll('.stack')
.data(d => d)
.attr('class', 'stack')
.join(
enter => enter.append('rect')
.data(d => d)
.attr('x', d => x(d.data.year))
.attr('y', y(0))
.attr('width', x.bandwidth())
.call(enter => enter.transition().duration(1000)
.attr('y', d => y(d[1]))
.attr('height', d => y(d[0]) - y(d[1]))
),
update => update
.attr('x', d => x(d.data.year))
.attr('y', y(0))
.attr('width', x.bandwidth())
.call(update => update.transition().duration(1000)
.attr('y', d => y(d[1]))
.attr('height', d => y(d[0]) - y(d[1]))
.attr(d => console.log('update stack'))
)
)
}
function randomData(data){
return years.map(function(d){
let obj = {};
obj.year = d;
let nums = [];
alphabet.forEach(function(e){
let num = Math.round(Math.random()*2);
obj[e] = num;
nums.push(num);
});
obj.sum = nums.reduce((a, b) => a + b, 0);
return obj;
});
}
</script>
</body>
</html>
Here is it in a working jsfiddle: https://jsfiddle.net/blabbath/yeq5d1tp/
EDIT: I provided a wrong link first, here is the right one.
My example is heavily based on this: https://bl.ocks.org/HarryStevens/7e3ec1a6722a153a5d102b6c42f4501d
I had the same issue a few days ago. The way I did it is as follows:
We have two .join, the parent one is for the stack and the child is for the rectangles.
In the enter of the parent join, we call the updateRects in order to draw the rectangles for the first time, this updateRects function will do the child .join, this second join function will draw the rectangles.
For the update we do the same, but instead of doing it in the enter function of the join, we do it in the update.
Also, my SVG is structured in a different way, I have a stacks groups, then I have the stack group, and a bars group, in this bars group I add the rectangles. In the fiddle below, you can see that I added the parent group with the class stack.
The two functions are below:
updateStack:
function updateStack(data) {
// update the y scale
y.domain([0, d3.max(data.map((d) => d.sum))]);
g.select(".y").transition().duration(1000).call(yAxis);
const stackData = stack(data);
stackData.forEach((stackedBar) => {
stackedBar.forEach((stack) => {
stack.id = `${stackedBar.key}-${stack.data.year}`;
});
});
let bars = g
.selectAll("g.stacks")
.selectAll(".stack")
.data(stackData, (d) => {
return d.key;
});
bars.join(
(enter) => {
const barsEnter = enter.append("g").attr("class", "stack");
barsEnter
.append("g")
.attr("class", "bars")
.attr("fill", (d) => {
return color(d.key);
});
updateRects(barsEnter.select(".bars"));
return enter;
},
(update) => {
const barsUpdate = update.select(".bars");
updateRects(barsUpdate);
},
(exit) => {
return exit.remove();
}
);
}
updateRects:
function updateRects(childRects) {
childRects
.selectAll("rect")
.data(
(d) => d,
(d) => d.id
)
.join(
(enter) =>
enter
.append("rect")
.attr("id", (d) => d.id)
.attr("class", "bar")
.attr("x", (d) => x(d.data.year))
.attr("y", y(0))
.attr("width", x.bandwidth())
.call((enter) =>
enter
.transition()
.duration(1000)
.attr("y", (d) => y(d[1]))
.attr("height", (d) => y(d[0]) - y(d[1]))
),
(update) =>
update.call((update) =>
update
.transition()
.duration(1000)
.attr("y", (d) => y(d[1]))
.attr("height", (d) => y(d[0]) - y(d[1]))
),
(exit) =>
exit.call((exit) =>
exit
.transition()
.duration(1000)
.attr("y", height)
.attr("height", height)
)
);
}
Here is an update jsfiddle https://jsfiddle.net/5oqwLxdj/1/
I hope it helps.
It is getting called, but you can't see it. Try changing .attr(d => console.log('update stack')) to .attr(console.log('update stack')).
I am wondering is it possible to achieve the combination of area and bar chart in the way shown in the screenshot below?
Along with making the area in between clickable for some other action.
It would be really helpful if you can guide me to some of the examples to get an idea how to achieve the same.
I posted a codepen here. That creates a bar chart, and then separate area charts between each bar chart.
const BarChart = () => {
// set data
const data = [
{
value: 48,
label: 'One Rect'
},
{
value: 32,
label: 'Two Rect'
},
{
value: 40,
label: 'Three Rect'
}
];
// set selector of container div
const selector = '#bar-chart';
// set margin
const margin = {top: 60, right: 0, bottom: 90, left: 30};
// width and height of chart
let width;
let height;
// skeleton of the chart
let svg;
// scales
let xScale;
let yScale;
// axes
let xAxis;
let yAxis;
// bars
let rect;
// area
let areas = [];
function init() {
// get size of container
width = parseInt(d3.select(selector).style('width')) - margin.left - margin.right;
height = parseInt(d3.select(selector).style('height')) - margin.top - margin.bottom;
// create the skeleton of the chart
svg = d3.select(selector)
.append('svg')
.attr('width', '100%')
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', 'translate(' + margin.left + ', ' + margin.top + ')');
xScale = d3.scaleBand().padding(0.15);
xAxis = d3.axisBottom(xScale);
yScale = d3.scaleLinear();
yAxis = d3.axisLeft(yScale);
svg.append('g')
.attr('class', 'x axis')
.attr('transform', `translate(0, ${height})`);
svg.append('g')
.attr('class', 'y axis');
svg.append('g')
.attr('class', 'x label')
.attr('transform', `translate(10, 20)`)
.append('text')
.text('Value');
xScale
.domain(data.map(d => d.label))
.range([0, width])
.padding(0.3);
yScale
.domain([0, 75])
.range([height, 0]);
xAxis
.scale(xScale);
yAxis
.scale(yScale);
rect = svg.selectAll('rect')
.data(data);
rect
.enter()
.append('rect')
.style('fill', d => '#00BCD4')
.attr('y', d => yScale(d.value))
.attr('height', d => height - yScale(d.value))
.attr('x', d => xScale(d.label))
.attr('width', xScale.bandwidth());
// call the axes
svg.select('.x.axis')
.call(xAxis);
svg.select('.y.axis')
.call(yAxis);
// rotate axis text
svg.select('.x.axis')
.selectAll('text')
.attr('transform', 'rotate(45)')
.style('text-anchor', 'start');
if (parseInt(width) >= 600) {
// level axis text
svg.select('.x.axis')
.selectAll('text')
.attr('transform', 'rotate(0)')
.style('text-anchor', 'middle');
}
data.forEach(
(d, i) => {
if (data[i + 1]) {
areas.push([
{
x: d.label,
y: d.value
},
{
x: data[i + 1].label,
y: data[i + 1].value
}
]);
}
}
);
areas = areas.filter(
d => Object.keys(d).length !== 0
);
areas.forEach(
a => {
const area = d3.area()
.x((d, i) => {
return i === 0 ?
xScale(d.x) + xScale.bandwidth() :
xScale(d.x);
})
.y0(height)
.y1(d => yScale(d.y));
svg.append('path')
.datum(a)
.attr('class', 'area')
.style('fill', d => '#B2EBF2')
.attr('d', area)
.on('click', d => {
console.log('hello click!');
});
}
)
}
return { init };
};
const myChart = BarChart();
myChart.init();
#bar-chart {
height: 500px;
width: 100%;
}
<script src="https://unpkg.com/d3#5.2.0/dist/d3.min.js"></script>
<div id="bar-chart"></div>
After creating the bar chart, I repackage the data to make it conducive to creating an area chart. I created an areas array where each item is going to be a separate area chart. I'm basically taking the values for the first bar and the next bar, and packaging them together.
data.forEach(
(d, i) => {
if (data[i + 1]) {
areas.push([
{
x: d.label,
y: d.value
},
{
x: data[i + 1].label,
y: data[i + 1].value
}
]);
}
}
);
areas = areas.filter(
d => Object.keys(d).length !== 0
);
I then iterate through each element on areas and create the area charts.
The only tricky thing here, I think, is getting the area chart to span from the end of the first bar to the start of the second bar, as opposed to from the end of the first bar to the end of the second bar. To accomplish this, I added a rectangle width from my x-scale to the expected x value of the area chart when the first data point is being dealt with, but not the second.
I thought of this as making two points on a line: one for the first bar and one for the next bar. D3's area function can shade all the area under a line. So, the first point on my line should be the top-right corner of the first bar. The second point should be the top-left corner of the next bar.
Attaching a click event at the end is pretty straightforward.
areas.forEach(
a => {
const area = d3.area()
.x((d, i) => {
return i === 0 ?
xScale(d.x) + xScale.bandwidth() :
xScale(d.x);
})
.y0(height)
.y1(d => yScale(d.y));
svg.append('path')
.datum(a)
.attr('class', 'area')
.style('fill', d => '#B2EBF2')
.attr('d', area)
.on('click', d => {
console.log('hello click!');
});
}
)
In the example below, I have combined a simple bar chart (like in this famous bl.lock) with some polygons in between. I guess it could also be achieved with a path.
const data = [
{ letter: "a", value: 9 },
{ letter: "b", value: 6 },
{ letter: "c", value: 3 },
{ letter: "d", value: 8 }
];
const svg = d3.select("#chart");
const margin = { top: 20, right: 20, bottom: 30, left: 40 };
const width = +svg.attr("width") - margin.left - margin.right;
const height = +svg.attr("height") - margin.top - margin.bottom;
const xScale = d3.scaleBand()
.rangeRound([0, width]).padding(0.5)
.domain(data.map(d => d.letter));
const yScale = d3.scaleLinear()
.rangeRound([height, 0])
.domain([0, 10]);
const g = svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
g.append("g")
.attr("class", "axis axis--x")
.attr("transform", `translate(0,${height})`)
.call(d3.axisBottom(xScale));
g.append("g")
.attr("class", "axis axis--y")
.call(d3.axisLeft(yScale));
g.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("x", d => xScale(d.letter))
.attr("y", d => yScale(d.value))
.attr("width", xScale.bandwidth())
.attr("height", d => height - yScale(d.value));
// Add polygons
g.selectAll(".area")
.data(data)
.enter().append("polygon")
.attr("class", "area")
.attr("points", (d,i,nodes) => {
if (i < nodes.length - 1) {
const dNext = d3.select(nodes[i + 1]).datum();
const x1 = xScale(d.letter) + xScale.bandwidth();
const y1 = height;
const x2 = x1;
const y2 = yScale(d.value);
const x3 = xScale(dNext.letter);
const y3 = yScale(dNext.value);
const x4 = x3;
const y4 = height;
return `${x1},${y1} ${x2},${y2} ${x3},${y3} ${x4},${y4} ${x1},${y1}`;
}
})
.on("click", (d,i,nodes) => {
const dNext = d3.select(nodes[i + 1]).datum();
const pc = Math.round((dNext.value - d.value) / d.value * 100.0);
alert(`${d.letter} to ${dNext.letter}: ${pc > 0 ? '+' : ''}${pc} %`);
});
.bar {
fill: steelblue;
}
.area {
fill: lightblue;
}
.area:hover {
fill: sandybrown;
cursor: pointer;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<svg width="400" height="300" id="chart"></svg>
import React, { Component } from 'react';
import { json } from 'd3-request';
import { rgb } from 'd3-color';
import { interpolateHcl } from 'd3-interpolate';
import { scaleLinear, scaleOrdinal } from 'd3-scale';
import { arc, line, pie, curveMonotoneX } from 'd3-shape';
import { format } from 'd3-format';
import { min, max } from 'd3-array';
import { select } from 'd3-selection';
import { sankey as sankeyGraph, sankeyLinkHorizontal } from 'd3-sankey';
class Graph extends React.Component {
constructor(props) {
super(props);
this.createLineGraph = this.createLineGraph.bind(this);
this.createBarChart = this.createBarChart.bind(this);
this.createPieChart = this.createPieChart.bind(this);
this.createSankeyGraph = this.createSankeyGraph.bind(this);
// this.createRadialChart = this.createRadialChart.bind(this);
this.createTheGraphs = this.createTheGraphs.bind(this);
this.state = {
loading: false
};
}
getDimensions() {
const margin = {top: 20, right: 20, bottom: 20, left: 20},
padding = {top: 40, right: 40, bottom: 40, left: 40},
outerWidth = parseInt(this.props.size[0]),
outerHeight = parseInt(this.props.size[1]),
innerWidth = outerWidth - margin.left - margin.right,
innerHeight = outerHeight - margin.top - margin.bottom,
width = innerWidth - padding.left - padding.right,
height = innerHeight - padding.top - padding.botto,
radius = parseInt(min([innerWidth, innerHeight]) / 2),
donutHole = this.props.type === "DONUT" ? radius / 2 : 0,
color = scaleLinear()
.domain([1, this.props.data.length])
.interpolate(interpolateHcl)
.range([rgb("#AF2192"), rgb("#98D719")]);
// DON'T DO DATA MAPPING ON SANKEY GRAPH SINCE DATA STRUCTURE IS DIFFERENT
if (this.props.type !== "SANKEY") {
// HIGHEST VALUE OF ITEMS IN DATA ARRAY
const dataMax = max(this.props.data.map(item => item.value)),
dataSpread = (innerWidth / this.props.data.length),
// DEPEND SCALE OF ITEMS ON THE Y AXIS BASED ON HIGHEST VALUE
yScale = scaleLinear()
.domain([0, dataMax])
.range([0, innerHeight]),
// GENERATE THE LINE USING THE TOTAL SPACE AVAILABLE FROM THE SIZE PROP DIVIDED BY THE LENGTH OF THE DATA ARRAY
lineGen = line()
.x((d, i) => i * dataSpread)
.y(d => innerHeight - yScale(d))
// CURVEMONOTONEX GAVE THE BEST RESULTS
.curve(curveMonotoneX);
dimensions = {margin, padding, outerWidth, outerHeight, innerWidth, innerHeight, radius, donutHole, color, dataMax, dataSpread, yScale, lineGen};
} else {
dimensions = {margin, padding, outerWidth, outerHeight, innerWidth, innerHeight, radius, donutHole, color};
}
}
createSankeyGraph(data) {
const sankeyNode = this.node;
let graphData = this.props.data;
// console.log(graphData);
// console.log(graphData.links);
// console.log(graphData.nodes);
// console.log(dimensions.outerWidth, dimensions.outerHeight);
// GET DIMENSIONS IN A GLOBAL-VAR-LIKE WAY
this.getDimensions();
const formatNumber = format('.1f');
const formatted = function(d) {return formatNumber(d) + " Potential Guests"};
const color = scaleLinear()
.domain([1, 3])
.interpolate(interpolateHcl)
.range([rgb('#126382'), rgb('#417685')]);
var sankey = sankeyGraph()
.nodeWidth(15)
.nodePadding(10)
.extent([1, 1], [parseInt(dimensions.outerWidth) - 1, parseInt(dimensions.outerHeight) - 6]);
var SVG = select(sankeyNode)
.append('g')
.attr('transform', 'translate(' + dimensions.margin.left + ',' + dimensions.margin.top +')');
var link = SVG.append('g')
.attr('class', 'links')
.attr("fill", "none")
.attr("stroke", "#000")
.attr("stroke-opacity", 0.2)
.selectAll('path')
var node = SVG.append('g')
.attr('class', 'nodes')
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.selectAll('g')
// json('https://api.myjson.com/bins/15xgsd', function(error, graphData){
sankey(graphData);
// console.log(graphData.nodes, graphData.links);
link = link
.data(graphData.links)
.enter()
.append('path')
.attr('d', sankeyLinkHorizontal())
.attr('stroke-width', function(d) { return Math.max(1, d.width); });
link.append('title')
.text(function(d) { return d.source.name + " → " + d.target.name + "\n" + formatted(d.value); });
node = node
.data(graphData.nodes)
.enter()
.append('g')
node.append('rect')
.attr('x', function(d) { return d.x0; })
.attr('y', function(d) { return d.y0; })
.attr('height', function(d) { return d.y1 - d.y0})
.attr('width', function(d) { return d.x1 - d.x0})
.attr("fill", function(d, i) { return color(i); })
.attr('stroke', 'black');
node.append('text')
.attr('x', function(d) {return d.x0 - 6})
.attr('y', function(d) {return (d.y1 + d.y0) / 2})
.attr('dy', '.35em')
.attr('text-anchor', 'end')
.text(function(d) { return d.name; })
.filter(function(d) { return d.x0 < dimensions.innerWidth / 2; })
.attr('x', function(d) { return d.x1 + 6; })
.attr('text-anchor', 'start');
node.append('title')
.text(d => d.name + "\n" + formatted(d.value));
// });
}
createTheGraphs() {
(this.props.type === "LINE") ? this.createLineGraph() : "";
(this.props.type === "BAR") ? this.createBarChart() : "";
(this.props.type === "PIE" || this.props.type === "DONUT") ? this.createPieChart() : "";
(this.props.type === "SANKEY") ? this.createSankeyGraph() : "";
(this.props.type === "RADIAL") ? this.createRadialChart() : "";
}
componentWillMount() {
this.setState({ loading: true });
}
componentDidMount() {
this.createTheGraphs();
}
componentDidUpdate() {
this.createTheGraphs();
}
render() {
return(
<div className="Graph">
<svg className='Graph_Container' ref={node => this.node = node}></svg>
<h2>{this.props.type} Placeholder</h2>
</div>
);
}
}
Graph.propTypes = {
};
export default Graph;
What's happening? Well, basically the console is outputting
Error: missing: Peter modules.js:54276:20
find http://localhost:3000/packages/modules.js:54276:20
computeNodeLinks/< http://localhost:3000/packages/modules.js:54353:62
forEach self-hosted:267:13
computeNodeLinks http://localhost:3000/packages/modules.js:54350:5
sankey http://localhost:3000/packages/modules.js:54292:5
createSankeyGraph http://localhost:3000/app/app.js:554:13
createSankeyGraph self-hosted:941:17
createTheGraphs http://localhost:3000/app/app.js:598:44
createTheGraphs self-hosted:941:17
componentDidMount http://localhost:3000/app/app.js:617:13
mountComponent/</< http://localhost:3000/packages/modules.js:17838:20
measureLifeCyclePerf http://localhost:3000/packages/modules.js:17649:12
mountComponent/< http://localhost:3000/packages/modules.js:17837:11
notifyAll http://localhost:3000/packages/modules.js:10464:9
close http://localhost:3000/packages/modules.js:20865:5
closeAll http://localhost:3000/packages/modules.js:11623:11
perform http://localhost:3000/packages/modules.js:11570:11
batchedMountComponentIntoNode http://localhost:3000/packages/modules.js:22897:3
perform http://localhost:3000/packages/modules.js:11557:13
batchedUpdates http://localhost:3000/packages/modules.js:20563:14
batchedUpdates http://localhost:3000/packages/modules.js:10225:10
_renderNewRootComponent http://localhost:3000/packages/modules.js:23090:5
_renderSubtreeIntoContainer http://localhost:3000/packages/modules.js:23172:21
render http://localhost:3000/packages/modules.js:23193:12
routes.js/< http://localhost:3000/app/app.js:1504:3
maybeReady http://localhost:3000/packages/meteor.js:821:6
loadingCompleted http://localhost:3000/packages/meteor.js:833:5
Which results in the graph not rendering the nodes it needs to base the lines (paths) on. The only HTML I get back is:
<svg class="Graph_Container">
<g transform="translate(20,20)">
<g class="links" fill="none" stroke="#000" stroke-opacity="0.2"></g>
<g class="nodes" font-family="sans-serif" font-size="10"></g>
</g>
</svg>
No nodes in the 'g.nodes' thus no links in the 'g.links'. The data structure this graph should be processing looks like:
<Graph type="SANKEY"
data={{
nodes: [
{name: "Peter"},
{name: "Test.com"},
{name: "Thing.com"},
{name: "AnotherName"}
], links: [
{source: "Peter", target: "Test.com", value: 50},
{source: "Peter", target: "Thing.com", value: 50},
{source: "Test.com", target: "AnotherName", value: 50},
{source: "Thing.com", target: "AnotherName", value: 50}
]
}}
size={[500, 500]} />
I don't know where to go from here. With this package I jumped from issue to issue altering the code line by line. The original issue looked like this.
In case links are specified using a source and target name (i.e. a string) instead of node indices, the solution is to specify a nodeId mapping function:
d3.sankey()
.nodeId(d => d.name) // Needed to avoid "Error: Missing: myNode"
Of course one may have to adjust the function d => d.name to correspond to the actual data.