Recently I have started learning D3.js v5 and I have to make different sorts of graph. One of those graphs is a stacked horizontal bar chart. I followed a tutorial in Udemy but I am not able to make this chart to work.
Can anybody help me out?
So, I know d3 uses a stack generator to generate stacked bar chart. I used it and checking the values in console it seems that the data is formatted pretty well and and can start generating stacked bar chart but i can't seem to understand why its not working.
<html>
<script src="https://d3js.org/d3.v5.min.js"></script>
<body></body>
<script type="text/javascript">
var Margin = {top: 30, right: 20, bottom: 40, left: 110};
var InnerWidth = 432 - Margin.left - Margin.right;
var InnerHeight = 432 - Margin.top - Margin.bottom;
var diff_variant=[{Missense_Mutation: 5, Silent: 4, Splice_Site: 0},
{Missense_Mutation: 8, Splice_Site: 0, Silent: 0},
{Splice_Site: 4, Missense_Mutation: 4, Silent: 0},
{Missense_Mutation: 4, Silent: 1, Splice_Site: 0},
{Missense_Mutation: 5, Splice_Site: 0, Silent: 0},
{Missense_Mutation: 5, Splice_Site: 0, Silent: 0},
{Missense_Mutation: 4, Splice_Site: 0, Silent: 0},
{Missense_Mutation: 4, Splice_Site: 0, Silent: 0},
{Silent: 1, Missense_Mutation: 1, Splice_Site: 0},
{Missense_Mutation: 1, Splice_Site: 0, Silent: 0}];
var data1=[{name: "Missense_Mutation", count: 41},
{name: "Silent", count: 6},
{name: "Splice_Site", count: 4}];
var variant_name=["Missense_Mutation", "Splice_Site", "Silent"];
var stack = d3.stack().keys(variant_name);
var x = d3.scaleLinear()
.domain([0, d3.max(data1, function(d){return d.count;})])
.range([0,InnerWidth]);
var x_axis = d3.scaleLinear()
.domain([0, d3.max(data1, function(d){return d.count;})])
.range([0,InnerWidth]);
var y_axis = d3.scaleLinear()
.domain([0, data1.length])
.range([0,InnerWidth]);
var y = d3.scaleBand()
.domain(data1.map(function(d){return d.name;}))
.rangeRound([0,InnerHeight])
.padding(0.1);
var svg5 = d3.select("body").append("svg")
.attr("preserveAspectRatio", "xMinYMin meet")
.attr("viewBox", `0 0 ${InnerWidth + Margin.left + Margin.right} ${InnerHeight + Margin.top + Margin.bottom}`)
.style("background","lightblue")
.append("g")
.attr("transform", "translate(" + Margin.left + "," + Margin.top + ")");
svg5.append("g").style("font", "12px sans").call(d3.axisLeft(y));
svg5.append("g").attr('transform', `translate(0, ${InnerHeight})`).style("font", "12px times").call(d3.axisBottom(x_axis));
var series = svg5.selectAll("g").data(stack(diff_variant))
.enter().append("g")
.style("fill", (d,i)=>d3.schemeSet3[i]);
series.selectAll("rect").data(function(d){return d;})
.enter().append("rect")
.attr("height", 25)
.attr("width", function(d){return x(d[1]) - x(d[0]);})
.attr("x", function(d){return x(d[0]);})
.attr("y", function(d,i){return i*40;});
</script>
</html>
When you do this...
var series = svg5.selectAll("g")//etc...
... you are selecting <g> elements that already exist in the SVG (the two axes) and binding data to them. That's why your enter selection is empty.
You can select null (read more about it here):
var series = svg5.selectAll(null)
Or you can select by a class, that you later set in the enter selection:
var series = svg5.selectAll(".foo")
However, the best solution is just moving the axes' group to the bottom: that not only will fix your issue, but it will also paint the axis above the rectangles.
Here is your code with that change:
<html>
<script src="https://d3js.org/d3.v5.min.js"></script>
<body></body>
<script type="text/javascript">
var Margin = {
top: 30,
right: 20,
bottom: 40,
left: 110
};
var InnerWidth = 432 - Margin.left - Margin.right;
var InnerHeight = 432 - Margin.top - Margin.bottom;
var diff_variant = [{
Missense_Mutation: 5,
Silent: 4,
Splice_Site: 0
},
{
Missense_Mutation: 8,
Splice_Site: 0,
Silent: 0
},
{
Splice_Site: 4,
Missense_Mutation: 4,
Silent: 0
},
{
Missense_Mutation: 4,
Silent: 1,
Splice_Site: 0
},
{
Missense_Mutation: 5,
Splice_Site: 0,
Silent: 0
},
{
Missense_Mutation: 5,
Splice_Site: 0,
Silent: 0
},
{
Missense_Mutation: 4,
Splice_Site: 0,
Silent: 0
},
{
Missense_Mutation: 4,
Splice_Site: 0,
Silent: 0
},
{
Silent: 1,
Missense_Mutation: 1,
Splice_Site: 0
},
{
Missense_Mutation: 1,
Splice_Site: 0,
Silent: 0
}
];
var data1 = [{
name: "Missense_Mutation",
count: 41
},
{
name: "Silent",
count: 6
},
{
name: "Splice_Site",
count: 4
}
];
var variant_name = ["Missense_Mutation", "Splice_Site", "Silent"];
var stack = d3.stack().keys(variant_name);
var x = d3.scaleLinear()
.domain([0, d3.max(data1, function(d) {
return d.count;
})])
.range([0, InnerWidth]);
var x_axis = d3.scaleLinear()
.domain([0, d3.max(data1, function(d) {
return d.count;
})])
.range([0, InnerWidth]);
var y_axis = d3.scaleLinear()
.domain([0, data1.length])
.range([0, InnerWidth]);
var y = d3.scaleBand()
.domain(data1.map(function(d) {
return d.name;
}))
.rangeRound([0, InnerHeight])
.padding(0.1);
var svg5 = d3.select("body").append("svg")
.attr("preserveAspectRatio", "xMinYMin meet")
.attr("viewBox", `0 0 ${InnerWidth + Margin.left + Margin.right} ${InnerHeight + Margin.top + Margin.bottom}`)
.style("background", "lightblue")
.append("g")
.attr("transform", "translate(" + Margin.left + "," + Margin.top + ")");
var series = svg5.selectAll("g").data(stack(diff_variant))
.enter().append("g")
.style("fill", (d, i) => d3.schemeSet3[i]);
series.selectAll("rect").data(function(d) {
return d;
})
.enter().append("rect")
.attr("height", 25)
.attr("width", function(d) {
return x(d[1]) - x(d[0]);
})
.attr("x", function(d) {
return x(d[0]);
})
.attr("y", function(d, i) {
return i * 40;
});
svg5.append("g").style("font", "12px sans").call(d3.axisLeft(y));
svg5.append("g").attr('transform', `translate(0, ${InnerHeight})`).style("font", "12px times").call(d3.axisBottom(x_axis));
</script>
</html>
Related
We are trying to create 3D network using D3.js, we tried to give the co-ordinates on the basis of all three axis (x,y,z) but on display it 2D network instead of 3D.
Is there any way to find the issue out...
Thanks in advance
<html>
<head>
<script src="https://d3js.org/d3.v6.min.js"></script>
<style>
#myDiv {
width: 70%;
float: left;
}
#lineGraph {
width: 30%;
height: 300px;
float: right;
background-color: lightgray;
}
</style>
</head>
<body>
<div id="myDiv"></div>
<div id="lineGraph"></div>
<script>
const nodes = [
{ x: 0, y: 0, z: 0, value: [1, 2, 3] },
{ x: 1, y: 1, z: 1, value: [4, 5, 6] },
{ x: 2, y: 0, z: 2, value: [7, 8, 9] },
{ x: 3, y: 1, z: 3, value: [10, 11, 12] },
{ x: 4, y: 0, z: 4, value: [13, 14, 15] }
];
const edges = [
{ source: 0, target: 1 },
{ source: 1, target: 2 },
{ source: 2, target: 3 },
{ source: 3, target: 4 }
];
const svg = d3.select("#myDiv")
.append("svg")
.attr("width", 500)
.attr("height", 500);
const node = svg.selectAll(".node")
.data(nodes)
.enter().append("circle")
.attr("cx", function(d) { return d.x * 50 + 100; })
.attr("cy", function(d) { return d.y * 50 + 100; })
.attr("cz", function(d) { return d.z * 50 + 100; })
.attr("r", 10)
.style("fill", "red")
.on("click", function(d) {
const lineData = [{
x: [0, 1, 2],
y: d.value,
type: "scatter"
}];
Plotly.newPlot("lineGraph", lineData);
});
const line = svg.selectAll(".line")
.data(edges)
.enter().append("line")
.attr("x1", function(d) { return nodes[d.source].x * 50 + 100; })
.attr("y1", function(d) { return nodes[d.source].y * 50 + 100; })
.attr("z1", function(d) { return nodes[d.source].z * 50 + 100; })
.attr("x2", function(d) { return nodes[d.target].x * 50 + 100; })
.attr("y2", function(d) { return nodes[d.target].y * 50 + 100; })
.attr("z2", function(d) { return nodes[d.target].z * 50 + 100; })
.style("stroke", "red")
.style("stroke-width", 2);
</script>
</body>
</html>
We are expecting a 3D graph, since we have assinged all the co-ordinates in terms of x,y,z but when it is displayed in web browser, it doesn't show a 3D network.
I want to open a graph as a modal after clicking another graph. However, what shows up in the modal is plain text, like such:
After inspecting the element, it seems like the text is the <g> tag that was in the svg, but I'm not sure what that means.
^ The corresponding HTML code is <text fill="currentColor" x="-9" dy="0.32em">−1.5</text>
I'm not sure why this is happening and I can't find any similar problems online for this.
Here's my graph code:
function Wavelet() {
const gWidth = window.innerWidth / 5
const gWidth_modal = window.innerWidth/3
var margin = {top: 20, right: 30, bottom: 30, left: 20},
width = gWidth - margin.left - margin.right,
height = 200 - margin.top - margin.bottom;
var margin_modal = {top: 20, right: 30, bottom: 30, left: 20},
width_modal = gWidth_modal - margin_modal.left - margin_modal.right,
height_modal = 400 - margin_modal.top - margin_modal.bottom;
// append the svg object to the body of the page
var svg = d3.select("#contour").selectAll("*").remove();
svg = d3.select("#contour").append("svg")
.attr("width", gWidth + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
var svg_modal = d3.select("#contour-modal").selectAll("*").remove();
svg_modal = d3.select("#contour-modal").append("svg_modal")
.attr("width", width_modal + margin_modal.left + margin_modal.right)
.attr("height", height_modal + margin_modal.top + margin_modal.bottom)
.append("g")
.attr("transform", "translate(" + margin_modal.left + "," + margin_modal.top + ")")
var color = d3.scaleLinear()
.domain([0, 0.01]) // Points per square pixel.
.range(["#c3d7e8", "#23527a"])
const data = [
{ x: 3.4, y: 4.2 },
{ x: -1, y: 4.2 },
{ x: -1, y: 2.8 },
{ x: 3.6, y: 4.3 },
{ x: -0.1, y: 3.7 },
{ x: 4.7, y: 2.5 },
{ x: 0.8, y: 3.6 },
{ x: 4.7, y: 3.7 },
{ x: -0.4, y: 4.2 },
{ x: 0.1, y: 2.2 },
{ x: 0.5, y: 3 },
{ x: 4.3, y: 4.5 },
{ x: 3.4, y: 2.7 },
{ x: 4.4, y: 3.6 },
{ x: 3.3, y: 0.6 },
{ x: 3, y: 3.4 },
{ x: 4.7, y: 0 },
{ x: -0.7, y: 2.7 },
{ x: 2.6, y: 2 },
{ x: 0, y: -1 },
{ x: 3.4, y: 4.5 },
{ x: 3.9, y: 4.6 },
{ x: 0.7, y: 3.9 },
{ x: 3, y: 0.2 }
];
// Add X axis
var x = d3.scaleLinear()
.domain([-2, 6])
.range([ 0, width ]);
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
var x_modal = d3.scaleLinear()
.domain([-2, 6])
.range([ 0, width_modal ]);
svg_modal.append("g")
.attr("transform", "translate(0," + height_modal + ")")
.call(d3.axisBottom(x_modal));
// Add Y axis
var y = d3.scaleLinear()
.domain([-2, 5])
.range([ height, 0 ]);
svg.append("g")
.call(d3.axisLeft(y));
var y_modal = d3.scaleLinear()
.domain([-2, 5])
.range([ height_modal, 0 ]);
svg_modal.append("g")
.call(d3.axisLeft(y_modal));
// compute the density data
var densityData = d3.contourDensity()
.x(function(d) { return x(d.x); }) // x and y = column name in .csv input data
.y(function(d) { return y(d.y); })
.size([width, height])
.bandwidth(20) // smaller = more precision in lines = more lines
(data)
// Add the contour: several "path"
svg
.selectAll("path")
.data(densityData)
.enter()
.append("path")
.attr("d", d3.geoPath())
.attr("fill", function(d) { return color(d.value); })
.attr("stroke", "#4285f4")
svg_modal
.selectAll("path")
.data(densityData)
.enter()
.append("path")
.attr("d", d3.geoPath())
.attr("fill", function(d) { return color(d.value); })
.attr("stroke", "#4285f4")
}
export default Wavelet
I have 2 svg as the one in the modal doesn't get rendered if I use the same one (for some reason). If that's what's causing me problems, please let me know how to fix it.
useEffect(() => {
(async () => {
await Wavelet()
})();
});
// const showModal = () => {setIsopen((prev) => !prev); console.log("aaaaaaaaaa");}
function showModal (id) {
setIsopen((prev) => !prev);
setModalID(id)
}
function Graph() {
const modal = <div>
<div id="contour" onClick={() => showModal(4)}>
</div>
{isOpen && (modalID == 4) && (
<ModalContent onClose={() => setIsopen(false)}>
<div className="modal_c">
<h3>{modalID}</h3>
<div id="contour-modal" >
</div>
<p> </p>
</div>
</ModalContent>
)}
</div>
return modal;
}
}
My modal class if anybody wants to reference it:
const modal = {
position: "fixed",
zIndex: 1,
left: 0,
top: 0,
width: "100vw",
height: "100vh",
overflow: "auto",
backgroundColor: "rgba(192,192,192,0.5)",
display: "flex",
alignItems: "center"
};
const close = {
position: "absolute",
top: "11vh",
right: "27vw",
color: "#000000",
fontSize: 40,
fontWeight: "bold",
cursor: "pointer"
};
const modalContent = {
display: "flex",
alignItems: "center",
justifyContent: "center",
width: "50%",
height: "80%",
margin: "auto",
backgroundColor: "white"
// border: "2px solid #000000"
};
export const Modal = ({ onOpen, children }) => {
console.log(children)
return <div onClick={onOpen}> {children}</div>;
};
export const ModalContent = ({ onClose, children }) => {
return (
<div style={modal}>
<div style={modalContent}>
<span style={close} onClick={onClose}>
×
</span>
{children}</div>
</div>
);
};
Problem is with your this statement : d3.select("#contour-modal").append("svg_modal").In order to render the elements correctly, you need to append them to svg. d3.select("#contour-modal").append("svg") . svg_modal is an invalid tag for your requirement. Kindly read more about svg rendering. Done a basic test of your code:
const gWidth = window.innerWidth / 5
const gWidth_modal = window.innerWidth / 3
var margin = {
top: 20,
right: 30,
bottom: 30,
left: 20
},
width = gWidth - margin.left - margin.right,
height = 200 - margin.top - margin.bottom;
var margin_modal = {
top: 20,
right: 30,
bottom: 30,
left: 20
},
width_modal = gWidth_modal - margin_modal.left - margin_modal.right,
height_modal = 200 - margin_modal.top - margin_modal.bottom;
// append the svg object to the body of the page
var svg = d3.select("#contour").selectAll("*").remove();
svg = d3.select("#contour").append("svg")
.attr("width", gWidth + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
var svg_modal = d3.select("#contour-modal").selectAll("*").remove();
svg_modal = d3.select("#contour-modal").append("svg")
.attr("width", width_modal + margin_modal.left + margin_modal.right)
.attr("height", height_modal + margin_modal.top + margin_modal.bottom)
.append("g")
.attr("transform", "translate(" + margin_modal.left + "," + margin_modal.top + ")")
var color = d3.scaleLinear()
.domain([0, 0.01]) // Points per square pixel.
.range(["#c3d7e8", "#23527a"])
const data = [{
x: 3.4,
y: 4.2
},
{
x: -1,
y: 4.2
},
{
x: -1,
y: 2.8
},
{
x: 3.6,
y: 4.3
},
{
x: -0.1,
y: 3.7
},
{
x: 4.7,
y: 2.5
},
{
x: 0.8,
y: 3.6
},
{
x: 4.7,
y: 3.7
},
{
x: -0.4,
y: 4.2
},
{
x: 0.1,
y: 2.2
},
{
x: 0.5,
y: 3
},
{
x: 4.3,
y: 4.5
},
{
x: 3.4,
y: 2.7
},
{
x: 4.4,
y: 3.6
},
{
x: 3.3,
y: 0.6
},
{
x: 3,
y: 3.4
},
{
x: 4.7,
y: 0
},
{
x: -0.7,
y: 2.7
},
{
x: 2.6,
y: 2
},
{
x: 0,
y: -1
},
{
x: 3.4,
y: 4.5
},
{
x: 3.9,
y: 4.6
},
{
x: 0.7,
y: 3.9
},
{
x: 3,
y: 0.2
}
];
// Add X axis
var x = d3.scaleLinear()
.domain([-2, 6])
.range([0, width]);
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
var x_modal = d3.scaleLinear()
.domain([-2, 6])
.range([0, width_modal]);
svg_modal.append("g")
.attr("transform", "translate(0," + height_modal + ")")
.call(d3.axisBottom(x_modal));
// Add Y axis
var y = d3.scaleLinear()
.domain([-2, 5])
.range([height, 0]);
svg.append("g")
.call(d3.axisLeft(y));
var y_modal = d3.scaleLinear()
.domain([-2, 5])
.range([height_modal, 0]);
svg_modal.append("g")
.call(d3.axisLeft(y_modal));
// compute the density data
var densityData = d3.contourDensity()
.x(function(d) {
return x(d.x);
}) // x and y = column name in .csv input data
.y(function(d) {
return y(d.y);
})
.size([width, height])
.bandwidth(20) // smaller = more precision in lines = more lines
(data)
// Add the contour: several "path"
svg
.selectAll("path")
.data(densityData)
.enter()
.append("path")
.attr("d", d3.geoPath())
.attr("fill", function(d) {
return color(d.value);
})
.attr("stroke", "#4285f4")
svg_modal
.selectAll("path")
.data(densityData)
.enter()
.append("path")
.attr("d", d3.geoPath())
.attr("fill", function(d) {
return color(d.value);
})
.attr("stroke", "#4285f4")
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.5.0/d3.min.js" integrity="sha512-+rnC6CO1Ofm09H411e0Ux7O9kqwM5/FlEHul4OsPk4QIHIYAiM77uZnQyIqcyWaZ4ddHFCvZGwUVGwuo0DPOnQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<div id="contour">
</div>
<div id="contour-modal">
</div>
How to create this chart with D3? Any help will help full tried with High charts but not help full much
click event is not working on this drill down event is added but it won't work on click on bars or y-axis labels.
const data = [
{ "name": 'IT', "value": 20, "negativeValue": -80 },
{ "name": 'Capital Invest', "value": 30, "negativeValue": -70 },
{ "name": 'Infrastructure', "value": 40, "negativeValue": -60 }
];
Highcharts.setOptions({
lang: {
drillUpText: `◁ Back to {series.description}`,
},
});
Highcharts.chart({
chart: {
type: 'bar',
renderTo: 'alignmentChart',
height: 530,
marginRight: 20,
backgroundColor: 'transparent',
events: {
drilldown(e: any) {
if (e.seriesOptions.fits) {
linesPositive = e.seriesOptions.line;
} else {
lineNegative = e.seriesOptions.line;
}
labels = !!e.seriesOptions && e.seriesOptions.data.map(a => a.name);
},
drillup(e: any) {
if (e.seriesOptions.fits) {
linesPositive = e.seriesOptions.line;
} else {
lineNegative = e.seriesOptions.line;
}
labels = !!e.seriesOptions && e.seriesOptions.data.map(a => a.name);
},
},
},
title: {
text: '',
},
colors: ['#f7a704', '#458dde'],
// tooltip: this.getTooltip(this),
xAxis: {
reversed: false,
tickPositions: Array.from(Array(this.multi.positive.length).keys()),
labels: {
useHTML: true,
formatter() {
return `<span title="${labels[this.value]}">${labels[this.value]}</span>`;
},
style: {
color: '#000000',
},
step: 1,
},
lineWidth: 0,
tickWidth: 0,
},
yAxis: {
title: {
text: null,
},
max: 100,
min: -100,
plotLines: [{
color: '#e5e5e5',
value: 0,
width: 1,
zIndex: 20,
}],
lineWidth: 1,
gridLineWidth: 0,
tickWidth: 1,
// offset: 100,
labels: {
y: 30,
align: 'center',
},
},
plotOptions: {
bar: {
pointWidth: 12,
},
series: {
stacking: 'normal',
dataLabels: {
enabled: true,
color: '#6b6b6b',
style: {
fontSize: '12px',
fontFamily: 'Proxima Nova'
},
formatter() {
return '';
},
inside: false,
},
},
},
series: [{
name: 'Fits Role',
description: 'Subfunctions',
data: this.multi.positive,
type: undefined
}, {
name: 'Not Fit Role',
description: 'Subfunctions',
data: this.multi.negative,
type: undefined
}],
drilldown: {
allowPointDrilldown: false,
activeAxisLabelStyle: {
fontSize: '12px',
fontWeight: 'bold',
color: '#007bc7',
textDecoration: 'none',
},
series: this.multi.drilldowns,
},
credits: {
enabled: false,
},
legend: {
enabled: false,
},
exporting: {
enabled: false,
},
});
I had to make only very few changes compared to the answer I shared. As I said in my comment, I create one g node per item, and draw two rects for every one.
Then I update the rects to have the same datum shape ({ name: string, value: number }), regardless of whether it's positive or negative. That allows me to do exactly the same things for both types.
// Now, the data can also be negative
const data = [{
"name": 'IT',
"value": 20,
"negativeValue": -80
}, {
"name": 'Capital Invest',
"value": 30,
"negativeValue": -70
}, {
"name": 'Infrastructure',
"value": 40,
"negativeValue": -60
}];
const width = 600,
height = 300,
margin = {
top: 20,
left: 100,
right: 40,
bottom: 40
};
// Now, we don't use 0 as a minimum, but get it from the data using d3.extent
const x = d3.scaleLinear()
.domain([-100, 100])
.range([0, width]);
const y = d3.scaleBand()
.domain(data.map(d => d.name))
.range([height, 0])
.padding(0.1);
const svg = d3.select('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom);
const g = svg
.append('g')
.attr('transform', `translate(${margin.left} ${margin.right})`);
// One group per data entry, each holding two bars
const barGroups = g
.selectAll('.barGroup')
.data(data);
barGroups.exit().remove();
const newBarGroups = barGroups.enter()
.append('g')
.attr('class', 'barGroup');
// Append one bar for the positive value, and one for the negative one
newBarGroups
.append('rect')
.attr('class', 'positive')
.attr('fill', 'darkgreen');
newBarGroups
.append('rect')
.attr('class', 'negative')
.attr('fill', 'darkred');
const positiveBars = newBarGroups
.merge(barGroups)
.select('.positive')
.datum(d => ({
name: d.name,
value: d.value
}));
const negativeBars = newBarGroups
.merge(barGroups)
.select('.negative')
.datum(d => ({
name: d.name,
value: d.negativeValue
}));
newBarGroups.selectAll('rect')
// If a bar is positive it starts at x = 0, and has positive width
// If a bar is negative it starts at x < 0 and ends at x = 0
.attr('x', d => d.value > 0 ? x(0) : x(d.value))
.attr('y', d => y(d.name))
// If the bar is positive it ends at x = v, but that means it's x(v) - x(0) wide
// If the bar is negative it ends at x = 0, but that means it's x(0) - x(v) wide
.attr('width', d => d.value > 0 ? x(d.value) - x(0) : x(0) - x(d.value))
.attr('height', y.bandwidth())
// Let's color the bar based on whether the value is positive or negative
g.append('g')
.classed('x-axis', true)
.attr('transform', `translate(0, ${height})`)
.call(d3.axisBottom(x))
g.append('g')
.classed('y-axis', true)
.attr('transform', `translate(${x(0)}, 0)`)
.call(d3.axisLeft(y))
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<svg></svg>
Alternatively, you can do it without merging the selections like so:
// Now, the data can also be negative
const data = [{
"name": 'IT',
"value": 20,
"negativeValue": -80
}, {
"name": 'Capital Invest',
"value": 30,
"negativeValue": -70
}, {
"name": 'Infrastructure',
"value": 40,
"negativeValue": -60
}];
const width = 600,
height = 300,
margin = {
top: 20,
left: 100,
right: 40,
bottom: 40
};
// Now, we don't use 0 as a minimum, but get it from the data using d3.extent
const x = d3.scaleLinear()
.domain([-100, 100])
.range([0, width]);
const y = d3.scaleBand()
.domain(data.map(d => d.name))
.range([height, 0])
.padding(0.1);
const svg = d3.select('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom);
const g = svg
.append('g')
.attr('transform', `translate(${margin.left} ${margin.right})`);
// One group per data entry, each holding two bars
const positiveBars = g
.selectAll('.positive')
.data(data);
positiveBars.exit().remove();
positiveBars.enter()
.append('rect')
.attr('class', 'positive')
.attr('fill', 'darkgreen')
.merge(positiveBars)
.attr('x', x(0))
.attr('y', d => y(d.name))
// The bar is positive. It ends at x = v, but that means it's x(v) - x(0) wide
.attr('width', d => x(d.value) - x(0))
.attr('height', y.bandwidth());
const negativeBars = g
.selectAll('.negative')
.data(data);
negativeBars.exit().remove();
negativeBars.enter()
.append('rect')
.attr('class', 'negative')
.attr('fill', 'darkred')
.merge(negativeBars)
.attr('x', d => x(d.negativeValue))
.attr('y', d => y(d.name))
// The bar is negative. It ends at x = 0, but that means it's x(0) - x(v) wide
.attr('width', d => x(0) - x(d.negativeValue))
.attr('height', y.bandwidth());
g.append('g')
.classed('x-axis', true)
.attr('transform', `translate(0, ${height})`)
.call(d3.axisBottom(x))
g.append('g')
.classed('y-axis', true)
.attr('transform', `translate(${x(0)}, 0)`)
.call(d3.axisLeft(y))
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<svg></svg>
I have data in the following format which I would like to plot using d3:
data = [
{ x: 0.2, y: [ 1, 2, 4 ] },
{ x: 0.3, y: [ 2 ] },
{ x: 0.5, y: [ 4, 7, 8, 12, 19 ] }
{ x: 1.4, y: [ 1, 3 ] }
]
Normally the y-axis values are integers, but here they are arrays, so the following code doesn't work as intended:
svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx", function(d){ return x(d.x) })
.attr("cy", function(d){ return y(d.y) })
.attr("r", 2)
Instead of getting multiple circles plotted for each of the values in the array, I only get one.
Other similar questions on this website deal only with data that has a fixed number of y-axis values, so I haven't found a way to modify those solutions for this problem.
The traditional D3 answer here would be appending a group for each object and then appending a circle for each y value for each group.
However, since you seem to be a D3 beginner (correct me if I'm wrong), I'd suggest to just create a single array of objects, that you can pass to data.
There are several ways for doing this, such as:
const newData = data.reduce(function(a, c) {
return a.concat(c.y.map(function(d) {
return {
x: c.x,
y: d
}
}));
}, []);
Here is your code with that change:
const data = [{
x: 0.2,
y: [1, 2, 4]
},
{
x: 0.3,
y: [2]
},
{
x: 0.5,
y: [4, 7, 8, 12, 19]
}, {
x: 1.4,
y: [1, 3]
}
];
const newData = data.reduce(function(a, c) {
return a.concat(c.y.map(function(d) {
return {
x: c.x,
y: d
}
}));
}, []);
const x = d3.scaleLinear()
.domain([0, 2])
.range([0, 300]);
const y = d3.scaleLinear()
.domain([0, 20])
.range([0, 150]);
const svg = d3.select("svg");
svg.selectAll("circle")
.data(newData)
.enter()
.append("circle")
.attr("cx", function(d) {
return x(d.x)
})
.attr("cy", function(d) {
return y(d.y)
})
.attr("r", 4)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>
Why not try unwrapping your data array before? That should be easy to process.
data = [{
x: 0.2,
y: [1, 2, 4]
},
{
x: 0.3,
y: [2]
},
{
x: 0.5,
y: [4, 7, 8, 12, 19],
},
{
x: 1.4,
y: [1, 3]
}
];
unwrappedData = [];
for (b in data) {
var temp = data[b].y.map((foo) => {
return {
x: data[b].x,
y: foo
}
})
unwrappedData = unwrappedData.concat(temp);
}
console.log(unwrappedData);
var svg = d3.select("body").append("svg")
.attr("width", 100)
.attr("height", 100)
.style("margin-top", "58px");
svg.selectAll("circle")
.data(unwrappedData)
.enter()
.append("circle")
.attr("cx", function(d) {
return d.x
})
.attr("cy", function(d) {
return d.y
})
.attr("r", 2)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
Also, shouldn't the cx and cy attributes return d.x and d.y instead?
I'm having a lot of trouble getting a circle to delete. In this example I want to delete the circle labeled "2" but it just deletes the last circle in the array. If you uncomment the commented part and comment the graphNodes.splice(2, 1), it works as it should adding a new circle. But the delete won't work.
What am I missing here?
var width = 640,
height = 480;
var graphNodes = [
{ id: 0, x: 39, y: 343, r: 15 },
{ id: 1, x: 425, y: 38, r: 15 },
{ id: 2, x: 183, y: 417, r: 15 },
{ id: 3, x: 564, y: 31, r: 15 },
{ id: 4, x: 553, y: 351, r: 15 },
{ id: 5, x: 454, y: 298, r: 15 },
{ id: 6, x: 493, y: 123, r: 15 },
{ id: 7, x: 471, y: 427, r: 15 },
{ id: 8, x: 142, y: 154, r: 15 }
];
var svg = d3.select('body').append('svg')
.attr('width', width)
.attr('height', height);
svg.append('rect')
.attr('class', 'graph')
.attr('width', width)
.attr('height', height)
.attr('fill', 'lightblue')
.attr('opacity', 0.3)
.on('click', function(){
/*graphNodes.push({
x: d3.mouse(this)[0],
y: d3.mouse(this)[1],
id: graphNodes.length,
r: 15
});*/
graphNodes.splice(2, 1);
update();
});
var nodeGroup = svg.selectAll('.nodes')
.data(graphNodes, function(d){ return d.id; })
.enter().append('g')
.attr('class', 'node');
nodeGroup.append('circle')
.attr('cx', function(d) { return d.x })
.attr('cy', function(d) { return d.y })
.attr("r", function(d){ return d.r; })
.attr("fill", "gray");
nodeGroup.append('text')
.attr("dx", function(d){ return d.x + 20; })
.attr("dy", function(d){ return d.y + 5; })
.text(function(d) { return d.id });
function update() {
if(nodeGroup){
// Update nodes
var node = nodeGroup.data(graphNodes),
nodeEnter = node.enter().append('g')
.attr('class', 'node');
nodeEnter.append('circle')
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; })
.attr('r', function(d){ return d.r; })
.attr('fill', 'gray');
nodeEnter.append('text')
.attr("dx", function(d){ return d.x + 20; })
.attr("dy", function(d){ return d.y + 5; })
.text(function(d) { return d.id });
nodeGroup = nodeEnter.merge(node);
node.exit().remove();
}
}
Fiddle
When you re-bind your data in update function, you forgot your key function:
var node = nodeGroup.data(graphNodes, function(d){ return d.id; }),
Update fiddle.