How can I set the color blue for the area of the graph intercepted between the two lines in d3.js.
I'm using d3.area for the orange area and d3.line for the lines. Is there a d3 function for this?
const svg = select(svgRef.current);
const { width, height } =
dimensions || wrapperRef.current.getBoundingClientRect();
const xScale = scaleLinear()
.domain([min(data, (d) => d.x), max(data, (d) => d.x)])
.range([0, width]);
const yScale = scaleLinear()
.domain([0, max(data, (d) => d.high)])
.range([height, 0]);
const xAxis = axisBottom(xScale);
svg.append("g").attr("transform", `translate(0, ${height})`).call(xAxis);
const yAxis = axisLeft(yScale);
svg.append("g").call(yAxis);
const areaGenerator = area()
.defined((d) => true)
.x((d) => xScale(d.x))
.y0((d) => yScale(d.low))
.y1((d) => yScale(d.high))
.curve(curveMonotoneX);
const areaData1 = areaGenerator(data);
svg.append("path")
.attr("class", "layer")
.style("fill", "orange")
.attr("d", areaData1);
const line = d3.line().x(p => xScale(p.x)).y(p => yScale(p.y));
svg.append("path").attr("class", "line1").attr("stroke", "black").attr("d", line(points[0]));
svg.append("path").attr("class", "line2").attr("stroke", "black").attr("d", line(points[1]));
It is solved!
Thank you so much Mehdi!
Now it works!
Bellow the complete source code using React JS:
import React, { useEffect, useRef, useState } from "react";
import {
select,
axisBottom,
min,
max,
scaleLinear,
axisLeft,
area,
line,
curveMonotoneX,
} from "d3";
import useResizeObserver from "./useResizeObserver";
const data = [
{ x: 0, low: 100, high: 245 },
{ x: 3, low: 97, high: 244 },
{ x: 5, low: 94, high: 243 },
{ x: 8, low: 92, high: 242 },
{ x: 10, low: 90, high: 240 },
{ x: 13, low: 88, high: 237 },
{ x: 16, low: 85, high: 234 },
{ x: 18, low: 83, high: 232 },
{ x: 20, low: 80, high: 230 },
{ x: 22, low: 79, high: 225 },
{ x: 25, low: 69, high: 220 },
{ x: 28, low: 49, high: 215 },
{ x: 30, low: 0, high: 210 },
{ x: 40, low: 0, high: 120 },
{ x: 49, low: 0, high: 0 },
];
const points = [
[
{ x: 0, y: 0 },
{ x: 10, y: 70 },
{ x: 20, y: 150 },
{ x: 25, y: 180 },
{ x: 30, y: 245 },
],
[
{ x: 14, y: 0 },
{ x: 27, y: 100 },
{ x: 30, y: 150 },
{ x: 38, y: 245 },
],
];
function StackedBarChart() {
const svgRef = useRef();
const wrapperRef = useRef();
const dimensions = useResizeObserver(wrapperRef);
const [init, setInit] = useState(false);
useEffect(() => {
if (data.length && points.length && !init) {
setInit(true);
const svg = select(svgRef.current);
const { width, height } =
dimensions || wrapperRef.current.getBoundingClientRect();
const xScale = scaleLinear()
.domain([min(data, (d) => d.x), max(data, (d) => d.x)])
.range([0, width]);
const yScale = scaleLinear()
.domain([0, max(data, (d) => d.high)])
.range([height, 0]);
const xAxis = axisBottom(xScale);
svg.append("g").attr("transform", `translate(0, ${height})`).call(xAxis);
const yAxis = axisLeft(yScale);
svg.append("g").call(yAxis);
const areaGenerator = area()
.defined((d) => true)
.x((d) => xScale(d.x))
.y0((d) => yScale(d.low))
.y1((d) => yScale(d.high))
.curve(curveMonotoneX);
const areaData1 = areaGenerator(data);
const lineGenerator = line()
.x((p) => xScale(p.x))
.y((p) => yScale(p.y));
const polygon1 = points.map((point) =>
point.map((item) => [item.x, item.y])
);
const arrPolygons = [...polygon1[0].reverse(), ...polygon1[1]];
const clip = svg.append("g").append("clipPath").attr("id", "clip");
clip
.selectAll("#pol1")
.data([arrPolygons])
.enter()
.append("polygon")
.attr("id", "pol1")
.attr("points", (d) => {
return d.map((i) => [xScale(i[0]), yScale(i[1])]).join(" ");
});
const areaChart1 = svg
.append("path")
.attr("id", "area1")
.attr("class", "layer1")
.style("fill", "orange")
.attr("d", areaData1);
const areaChart = svg
.append("path")
.attr("id", "area")
.attr("class", "layer")
.attr("clip-path", "url(#clip)")
.style("fill", "blue")
.attr("d", areaData1);
svg
.append("path")
.attr("class", "line1")
.attr("d", lineGenerator(points[0]));
svg
.append("path")
.attr("class", "line2")
.attr("d", lineGenerator(points[1]));
}
}, [data, dimensions, init, points]);
return (
<>
<div ref={wrapperRef} style={{ marginBottom: "2rem" }}>
<svg ref={svgRef} />
</div>
</>
);
}
export default StackedBarChart;
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 want to update the circles and the paths in this graph with a transition. However it does not work.
I am not very experienced with D3. How can I make my code better? Changing data structure is no problem. I want bind the data to graph with exit() remove() and enter() without deleting whole graph and add the data every time I update again. I do not know how to use enter() exit() and remove() for nested data. One time for the whole group and the other side updating the circle and paths. The ID should be fixed.
Here I have a little single line example from d3noob.
here is a JS Fiddle
var data = [{
id: 'p1',
points: [{
point: {
x: 10,
y: 10
}
}, {
point: {
x: 100,
y: 30
}
}]
},
{
id: 'p2',
points: [{
point: {
x: 30,
y: 100
}
}, {
point: {
x: 230,
y: 30
}
},
{
point: {
x: 50,
y: 200
}
},
{
point: {
x: 50,
y: 300
}
},
]
}
];
var svg = d3.select("svg");
var line = d3.line()
.x((d) => d.point.x)
.y((d) => d.point.y);
function updateGraph() {
console.log('dataset contains', data.length, 'item(s)')
var allGroup = svg.selectAll(".pathGroup").data(data, function(d) {
return d.id
});
var g = allGroup.enter()
.append("g")
.attr("class", "pathGroup")
allGroup.exit().remove()
g.append("path")
.attr("class", "line")
.attr("stroke", "red")
.attr("stroke-width", "1px")
.transition()
.duration(500)
.attr("d", function(d) {
return line(d.points)
});
g.selectAll("path")
.transition()
.duration(500)
.attr("d", function(d) {
return line(d.points)
});
g.selectAll("circle")
.data(d => d.points)
.enter()
.append("circle")
.attr("r", 4)
.attr("fill", "teal")
.attr("cx", function(d) {
return d.point.x
})
.attr("cy", function(d) {
return d.point.y
})
.exit().remove()
}
updateGraph()
document.getElementById('update').onclick = function(e) {
data = [{
id: 'p1',
points: [{
point: {
x: 10,
y: 10
}
}, {
point: {
x: 100,
y: 30
}
}]
},
{
id: 'p2',
points: [{
point: {
x: 30,
y: 100
}
}, {
point: {
x: 230,
y: 30
}
},
{
point: {
x: 50,
y: 300
}
},
]
}
];
updateGraph()
}
$('#cb1').click(function() {
if ($(this).is(':checked')) {
data = [{
id: 'p1',
points: [{
point: {
x: 10,
y: 10
}
}, {
point: {
x: 100,
y: 30
}
}]
},
{
id: 'p2',
points: [{
point: {
x: 30,
y: 100
}
}, {
point: {
x: 230,
y: 30
}
},
{
point: {
x: 50,
y: 200
}
},
{
point: {
x: 50,
y: 300
}
},
]
}
];
} else {
data = [{
id: 'p1',
points: [{
point: {
x: 10,
y: 10
}
}, {
point: {
x: 100,
y: 30
}
}]
}];
}
updateGraph()
});
Edited to change the data join to be be appropriate.
I think the issue has to do with variable data. So, I added data as an argument to the function and specified separate names for different data sets. When I specify different data names, I'm able to update the chart:
var first_data = [{
id: 'p1',
points: [{
point: {
x: 100,
y: 10
}
}, {
point: {
x: 100,
y: 30
}
}]
},
{
id: 'p2',
points: [{
point: {
x: 30,
y: 100
}
}, {
point: {
x: 230,
y: 30
}
},
{
point: {
x: 50,
y: 200
}
},
{
point: {
x: 50,
y: 300
}
},
]
}
];
var svg = d3.select("svg");
var line = d3.line()
.x((d) => d.point.x)
.y((d) => d.point.y);
function updateGraph(data) {
console.log('dataset contains', data.length, 'item(s)')
var allGroup = svg.selectAll(".pathGroup")
.data(data, function(d) { return d; });
var g = allGroup.enter()
.append("g")
.attr("class", "pathGroup")
allGroup.exit().remove()
g.append("path")
.attr("class", "line")
.attr("stroke", "red")
.attr("stroke-width", "1px")
.transition()
.duration(500)
.attr("d", function(d) {
console.log("update path");
return line(d.points)
});
g.selectAll("path")
.transition()
.duration(500)
.attr("d", function(d) {
return line(d.points)
});
g.selectAll("circle")
.data(d => d.points)
.enter()
.append("circle")
.attr("r", 4)
.attr("fill", "teal")
.attr("cx", function(d) {
return d.point.x
})
.attr("cy", function(d) {
return d.point.y
})
.exit().remove()
}
updateGraph(first_data)
document.getElementById('update').onclick = function(e) {
var update_data = [{
id: 'p1',
points: [{
point: {
x: 20,
y: 10
}
}, {
point: {
x: 100,
y: 30
}
}]
},
{
id: 'p2',
points: [{
point: {
x: 30,
y: 100
}
}, {
point: {
x: 230,
y: 30
}
},
{
point: {
x: 50,
y: 300
}
},
]
}
];
updateGraph(update_data)
}
$('#cb1').click(function() {
if ($(this).is(':checked')) {
var click_data = [{
id: 'p1',
points: [{
point: {
x: 10,
y: 10
}
}, {
point: {
x: 100,
y: 30
}
}]
},
{
id: 'p2',
points: [{
point: {
x: 30,
y: 100
}
}, {
point: {
x: 230,
y: 30
}
},
{
point: {
x: 50,
y: 200
}
},
{
point: {
x: 50,
y: 300
}
},
]
}
];
} else {
var click_data = [{
id: 'p1',
points: [{
point: {
x: 10,
y: 10
}
}, {
point: {
x: 100,
y: 30
}
}]
}];
}
updateGraph(click_data)
});
Fiddle: https://jsfiddle.net/wj266kmx/32/
I've made a line path for creating a shape of a bottle. I'm trying to figure out if I can close this path in order to transition it. Ideally I would like to have the bottle animate across the canvas, the same way a circle or rectangle would. Is this possible? I'm still learning d3.
var width = 900,
height = 800;
var canvas = d3.select('body')
.append('svg')
.attr('width', width)
.attr('height', height);
var dataArray = [
{x:51,y:44},
{x:51,y:49},
{x:53,y:50},
{x:53,y:53},
{x:52,y:53},
{x:52,y:60},
{x:70,y:85},
{x:71,y:160},
{x:64,y:181},
{x:54,y:181},
{x:47,y:170},
{x:43,y:170},
{x:36,y:181},
{x:26,y:181},
{x:19,y:160},
{x:19,y:85},
{x:39,y:60},
{x:39,y:53},
{x:38,y:53},
{x:38,y:50},
{x:40,y:49},
{x:40,y:44},
{x:51,y:44}
];
var interpolate = d3.curveCardinal.tension(0.35);
var line = d3.line()
.x(function(d,i){ return d.x/1.05 })
.y(function(d,i){ return d.y })
.curve(interpolate);
var group = canvas.append('g')
.attr('transform','translate(0,0)');
var bottle = group.selectAll('path')
.data([dataArray])
.enter()
.append('path')
.attr('fill','#ED5545')
.attr('stroke','#AA2731')
.attr('stroke-width','2')
.attr('id','bottleImage')
.attr('d',line);
bottle.transition()
.attr('x',300);
A SVG path has no x attribute. The simplest solution, therefore, is applying the transition to the group:
group.transition()
.delay(500)
.duration(2000)
.attr('transform', 'translate(300,0)');
Here is the demo:
var width = 400,
height = 300;
var canvas = d3.select('body')
.append('svg')
.attr('width', width)
.attr('height', height);
var dataArray = [{
x: 51,
y: 44
}, {
x: 51,
y: 49
}, {
x: 53,
y: 50
}, {
x: 53,
y: 53
}, {
x: 52,
y: 53
}, {
x: 52,
y: 60
}, {
x: 70,
y: 85
}, {
x: 71,
y: 160
}, {
x: 64,
y: 181
}, {
x: 54,
y: 181
}, {
x: 47,
y: 170
}, {
x: 43,
y: 170
}, {
x: 36,
y: 181
}, {
x: 26,
y: 181
}, {
x: 19,
y: 160
}, {
x: 19,
y: 85
}, {
x: 39,
y: 60
}, {
x: 39,
y: 53
}, {
x: 38,
y: 53
}, {
x: 38,
y: 50
}, {
x: 40,
y: 49
}, {
x: 40,
y: 44
}, {
x: 51,
y: 44
}];
var interpolate = d3.curveCardinal.tension(0.35);
var line = d3.line()
.x(function(d, i) {
return d.x / 1.05
})
.y(function(d, i) {
return d.y
})
.curve(interpolate);
var group = canvas.append('g')
.attr('transform', 'translate(0,0)');
var bottle = group.selectAll('path')
.data([dataArray])
.enter()
.append('path')
.attr('fill', '#ED5545')
.attr('stroke', '#AA2731')
.attr('stroke-width', '2')
.attr('id', 'bottleImage')
.attr('d', line);
group.transition()
.delay(500)
.duration(2000)
.attr('transform', 'translate(300,0)');
<script src="https://d3js.org/d3.v4.min.js"></script>