Closing path for transition animation d3.js - javascript

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>

Related

D3.js graph shows up as text

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>

Colored intercection area

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;

D3 v4 re-rendering nested objects, having problems with `enter()` and `exit()`

I'm trying to display a number of circles wrapped in <g> elements. This wrapping thing is important for me as in real world I want other elements to be in my group as well. The problem is, circles in the below example wont get displayed by first render(myObjects) call, and if you hit render button they are shown.
Second problem is when I press update the previously drawn circles are not removed and the new ones will render on top of them.
var svg = d3.select('body').append('svg')
.attr('width', 250)
.attr('height', 250);
//render the data
function render(datum) {
//Bind
var groups = svg.selectAll("g").data(datum);
groups.enter().append('g');
var circles = groups.append("circle").attr('r', 10)
.attr('cx', function(d) {
return d.x;
})
.attr('cy', function(d) {
return d.y;
});
groups.exit().remove();
}
var myObjects = [{ x: 100, y: 100 }, { x: 130, y: 120 }, { x: 80, y: 180 }, { x: 180, y: 80 }, { x: 180, y: 40 }];
var newObjects = [{ x: 200, y: 120 }, { x: 150, y: 160 }, { x: 20, y: 80 }, { x: 80, y: 60 }, { x: 100, y: 20 }];
function removeEls() {
svg.selectAll('circle').remove();
}
render(myObjects);
svg {
width: 500px;
height: 500px
}
button {
float: left
}
text {
text-anchor: middle;
}
circle {
fill: orange;
stroke: orange;
fill-opacity: 0.5;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<button onclick="render(newObjects)">Update</button>
<button onclick="render(myObjects);">Render</button>
<button onclick="removeEls();">removeEls</button>
You have 2 little issues in your code :
You have added the circles in the groups instead of groups.enter(), hence the issue of not displaying the circles at the first draw
You forget to add an identifier when you add the data, so the script is unable to detect which data is new and which data is not. I added a d.x/d.y identifier, which works in your case, but I advice you to add a true unique identifier. Like add a field id in your list of objects.
Here is the snippet with the corrections :
var svg = d3.select('body').append('svg')
.attr('width', 250)
.attr('height', 250)
.append('g');
//render the data
function render(datum) {
//Bind
var groups = svg.selectAll("g").data(datum, d => (d.x)/(d.y));
var groupEnter = groups.enter().append('g');
var circles = groupEnter.append("circle").attr('r', 10)
.attr('cx', function(d) {
return d.x;
})
.attr('cy', function(d) {
return d.y;
});
groups.exit().remove();
}
var myObjects = [{ x: 100, y: 100 }, { x: 130, y: 120 }, { x: 80, y: 180},{ x: 180, y: 80 }, { x: 180, y: 40 }];
var newObjects = [{ x: 200, y: 120 }, { x: 150, y: 160 }, { x: 20, y: 80}, { x: 80, y: 60 }, { x: 100, y: 20 }];
function removeEls() {
svg.selectAll('circle').remove();
}
render(myObjects);
svg {
width: 500px;
height: 500px
}
button {
float: left
}
text {
text-anchor: middle;
}
circle {
fill: orange;
stroke: orange;
fill-opacity: 0.5;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<button onclick="render(newObjects)">Update</button>
<button onclick="render(myObjects);">Render</button>
<button onclick="removeEls();">removeEls</button>

d3 multiline update path and transition path does not work with nested data

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/

Transitioning a path more than once

I'm having trouble trying to create a transition of groups. I've created an object from a path based on x and y data. I want this object to transition horizontally 5 times. I have 5 groups that will use the same object and transition. I'm trying to use a for loop so I don't have to rewrite that transition 5 times. You'll see in the snippet I have the groups group1, group2, etc., that are meant to use the same object. Is it possible to create an array of those groups that can be used for the transition? Hopefully this isn't confusing, and I'm using d3.v4. Thank you for taking a look at this.
var width = 900
var height = 1200;
var canvas = d3.select('body')
.append('svg')
.attr('width', width)
.attr('height', height);
// x and y coordinates of the path
var objectArray = [{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 objectOutline = d3.line()
.x(function(d, i) { return d.x / 2.6 })
.y(function(d, i) { return d.y / 2.2 })
.curve(interpolate);
// The 5 groups that use that same shapeArray for transition
var group1 = canvas.append('g');
var group2 = canvas.append('g');
var group3 = canvas.append('g');
var group4 = canvas.append('g');
var group5 = canvas.append('g');
// The group coordinates for start and end of transition
var locationArray = [{x:1200, y:440, xTransition:250},
{x:1200, y:440, xTransition:278},
{x:1200, y:440, xTransition:306},
{x:1200, y:440, xTransition:334},
{x:1200, y:440, xTransition:362}];
var length = locationArray.length;
for (var i=0; i < length; i++){
// Right now the group1 group is being used for the path
group1.selectAll('path')
.data([objectArray])
.enter()
.append('path')
.attr('d', objectOutline)
group1.attr('transform','translate('+ locationArray[i].x + ','+ locationArray[i].y + ')')
.transition()
.duration((Math.random() * 3000) + 800)
.delay(Math.random() * 3000)
.attr('transform', 'translate(' + locationArray[i].xTransition + ',' + locationArray[i].y + ')');
}
The problem with a for loop is that it will run to the end in few milliseconds, that is, almost immediately. That's not how you create a series of transitions.
The idiomatic way for doing this is using .on("end"... at the end of each transition. For instance, in this snippet, I create a function named transitioning that is called when the transition ends. As you have just 5 elements in the array, I set a counter, and the transition is called only when the counter is < 5:
transitioning(1)
function transitioning(index) {
console.log(locationArray[index].y)
path.transition()
.duration((Math.random() * 3000) + 800)
.delay(Math.random() * 3000)
.attr('transform', 'translate(' + locationArray[index].xTransition + ',' + locationArray[index].y + ')')
.on("end", function() {
if (++index > 4) return;
transitioning(index)
})
}
I'm starting at 1 because the bottle is already at position 0. Also, it's funny that you're transitioning the group, not the path. I changed that. If that's not what you want, just use the same logic to the groups.
Here is your code with this new function:
var width = 600
var height = 500;
var canvas = d3.select('body')
.append('svg')
.attr('width', width)
.attr('height', height);
// x and y coordinates of the path
var objectArray = [{
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 objectOutline = d3.line()
.x(function(d, i) {
return d.x / 2.6
})
.y(function(d, i) {
return d.y / 2.2
})
.curve(interpolate);
// The 5 groups that use that same shapeArray for transition
var group1 = canvas.append('g');
// The group coordinates for start and end of transition
var locationArray = [{
x: 200,
y: 40,
xTransition: 20
}, {
x: 200,
y: 40,
xTransition: 80
}, {
x: 200,
y: 40,
xTransition: 140
}, {
x: 200,
y: 40,
xTransition: 220
}, {
x: 200,
y: 40,
xTransition: 280
}];
var length = locationArray.length;
// Right now the group1 group is being used for the path
var path = group1.selectAll('path')
.data([objectArray])
.enter()
.append('path')
.attr('d', objectOutline)
.attr('transform', 'translate(' + locationArray[0].xTransition + ',' + locationArray[0].y + ')')
transitioning(1)
function transitioning(index) {
path.transition()
.duration((Math.random() * 3000) + 800)
.delay(Math.random() * 1000)
.attr('transform', 'translate(' + locationArray[index].xTransition + ',' + locationArray[index].y + ')')
.on("end", function() {
if (++index > 4) return;
transitioning(index)
})
}
<script src="https://d3js.org/d3.v4.min.js"></script>

Categories

Resources