I'm going to create a real-time data chart with D3. Frame work is React, but the data value that keeps changing comes only once. What should I do to stack up the data?
Is it because of the useEffect? But when I do it, I have to use d3 in this hook so that there is no error. And if I write xValue in the deps of useEffect, I keep getting only one value.
const StreamingGraphWithD3 = ({contentsData, chartSetting}) => {
const ref = useRef();
const xValue = contentsData.tem
useEffect(() => {
D3Chart(ref, xValue)
}, [])
return (
<div ref={ref} id={"svg_data1"}>
</div>
)
}
Below is the information about d3 svg.
const D3Chart = (ref, xValue) => {
ref.current.innerHTML = '';
const currentElement = ref.current;
let n = 150,
duration = 200,
now = new Date(Date.now() - duration)
const data = d3.range(n).map(function () {
return 0;
});
const margin = {top: 100, right: 20, bottom: 30, left: 30}
const width = 600 - margin.left - margin.right
const height = 500 - margin.top - margin.bottom
const svg = d3.select(currentElement)
.append('svg')
.attr('class', 'svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`);
const x = d3.scaleTime()
.domain([now - (n - 2) * duration, now - duration])
.range([0, width]);
const y = d3.scaleLinear()
.domain([-100, 100])
.range([height, 0]);
const yGrid = () => {
return d3.axisLeft(y)
.ticks(10)
}
svg.append("g")
.attr("class", "Ygrid")
.attr('stroke-opacity', '0.2')
.call(yGrid()
.tickSize(-width)
.tickFormat("")
)
const line =
d3.line()
.x((d, i) => x(now - (n - 1 - i) * duration))
.y((d, i) => y(d))
.curve(d3.curveBasis);
const path = svg.append("g")
.attr("clip-path", "url(#clip)")
.append("path")
.attr('fill', 'none')
.attr('stroke', '#FF7F7FFF')
.attr('stroke-width', 1.5)
.data([data])
.attr("class", "line");
svg.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height);
const axis = svg.append("g")
.attr("class", "Xaxis")
.attr("transform", `translate(0,${height})`)
.call(x.axis = d3.axisBottom(x));
svg.append('g')
.attr('class','Yaxis')
.call(d3.axisLeft(y));
tick();
function tick() {
now = new Date();
x.domain([now - (n - 2) * duration, now - duration]);
y.domain([-100, 100]);
data.push(xValue);
svg.select(".line")
.attr("d", line)
.attr("transform", null);
axis.transition()
.duration(duration)
.ease(d3.easeLinear)
.call(x.axis);
path.transition()
.duration(duration)
.ease(d3.easeLinear)
.attr("transform", `translate( ${x(now - (n - 1) * duration)})`)
.on("end", tick);
data.shift();
}
return(
<div>
</div>
)
}
You're having problems because if you invoke that useEffect for every data change, you're completely throwing away the entire chart, that includes all the scales and domains you've calculated.
I would make 2 changes:
Pass your data into you're React component (or store it in a useState) and provide that to the chart. Then you're in control of the full data set.
Call a second useEffect, one that basically triggers the tick code but leaves the rest of the chart untouched. To achieve this you may have to put your D3Chart instance into state so you can access it.
Related
I'm making a bar chart but I'm having problems to match the bar positions with the xAxis. They're not in the right place, for example, by hovering the bar above the 2010 mark, you can see it shows a 2007 value. How can I fix that?
let url = "https://raw.githubusercontent.com/freeCodeCamp/ProjectReferenceData/master/GDP-data.json";
const padding = 50;
const height = 460;
const width = 940;
const barthickness = 2.909090909090909;
var svg = d3.select('body')
.append('svg')
.attr('width', width)
.attr('height', height);
var arr = [];
var years = [];
d3.json(url, function(data) {
for (let i = 0; i < data.data.length; i++) {
arr[i] = data.data[i];
years[i] = parseInt(data.data[i][0].slice(0,4));
}
const yScale = d3.scaleLinear()
.domain([0, d3.max(arr, (d) => d[1])])
.range([height - padding, padding]);
const xScale = d3.scaleLinear()
.domain([d3.min(years, d => d), d3.max(years, (d) => d)])
.range([padding, width - padding]);
const xAxis = d3.axisBottom(xScale);
const yAxis = d3.axisLeft(yScale);
svg.append("g")
.attr("transform", "translate(0," + (height - padding) + ")")
.call(xAxis);
svg.append('g')
.attr('transform', 'translate(' + padding + ', 0)')
.call(yAxis)
svg.selectAll('rect')
.data(arr)
.enter()
.append('rect')
.attr('fill', 'blue')
.attr('height', d => height - padding - yScale(d[1]))
.attr('width', barthickness)
.attr('x', (d, i) => padding + (3.2* i))
.attr('y', d => yScale(d[1]))
.append('title')
.text((d, i) => years[i] + ': ' + d[1])
});
<script src="https://d3js.org/d3.v4.min.js"></script>
The problem is that you are not using your x-scale to position the bars. You are using padding + (3.2* i) to set the x coordinate of the bars, which does not line up with your scale. Your chart is 840 pixels wide and has 275 bars, which would be ~3.055 pixels per bar. Your code is placing bars every 3.2 pixels, which is too far.
Typically with bar charts, rather than hard-coding a bar thickness, you use a band scale. You'll want to use your scales both in your axes and to position the bars.
Alternatively, since you are working with temporal data, you could also consider using an area chart instead of a bar chart.
Below I've provided two similarly looking charts for your data. One is a bar chart and the other an area chart.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="https://d3js.org/d3.v7.js"></script>
</head>
<body>
<div id="bar-chart"></div>
<div id="area-chart"></div>
<script>
const url = 'https://raw.githubusercontent.com/freeCodeCamp/ProjectReferenceData/master/GDP-data.json';
d3.json(url).then(json => {
// convert the string into Date objects
const parse = d3.timeParse('%Y-%m-%d');
const data = json.data.map(d => ({ date: parse(d[0]), value: d[1] }));
barchart(data);
areachart(data);
});
function barchart(data) {
// set up
const margin = { top: 20, right: 20, bottom: 20, left: 30 };
const width = 600 - margin.left - margin.right;
const height = 300 - margin.top - margin.bottom;
const svg = d3.select('#bar-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})`);
// scales
const x = d3.scaleBand()
.domain(data.map(d => d.date))
.range([0, width]);
const y = d3.scaleLinear()
.domain([0, d3.max(data, d => d.value)])
.range([height, 0]);
// axes
// by default, axes for band scales show tick marks for every bar
// that would be too cluttered for this data, so we override this
// by explicitly setting tickValues()
const [minDate, maxDate] = d3.extent(data, d => d.date);
const xAxis = d3.axisBottom(x)
.tickSizeOuter(0)
// only show the year in the tick labels
.tickFormat(d3.timeFormat('%Y'))
.tickValues(d3.timeTicks(minDate, maxDate, 10));
const yAxis = d3.axisLeft(y)
.tickSizeOuter(0)
.ticks(10, '~s');
svg.append('g')
.attr('transform', `translate(0,${height})`)
.call(xAxis);
svg.append('g')
.call(yAxis);
// bars
// function to convert Date into string showing the month and year
const format = d3.timeFormat('%b %Y');
svg.selectAll('rect')
.data(data)
.join('rect')
.attr('x', d => x(d.date))
.attr('width', d => x.bandwidth())
.attr('y', d => y(d.value))
.attr('height', d => height - y(d.value))
.attr('fill', 'steelblue')
.append('title')
.text(d => `${format(d.date)}: ${d.value}`)
}
function areachart(data) {
// set up
const margin = { top: 20, right: 20, bottom: 20, left: 30 };
const width = 600 - margin.left - margin.right;
const height = 300 - margin.top - margin.bottom;
const svg = d3.select('#area-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})`);
// scales
const x = d3.scaleTime()
.domain(d3.extent(data, d => d.date))
.range([0, width]);
const y = d3.scaleLinear()
.domain([0, d3.max(data, d => d.value)])
.range([height, 0]);
// area generator
const area = d3.area()
.x(d => x(d.date))
.y0(y(0))
.y1(d => y(d.value))
.curve(d3.curveStepAfter);
// axes
const xAxis = d3.axisBottom(x)
.tickSizeOuter(0)
// only show the year in the tick labels
.tickFormat(d3.timeFormat('%Y'));
const yAxis = d3.axisLeft(y)
.tickSizeOuter(0)
.ticks(10, '~s');
svg.append('g')
.attr('transform', `translate(0,${height})`)
.call(xAxis);
svg.append('g')
.call(yAxis);
// area
svg.append('path')
.attr('d', area(data))
.attr('fill', 'steelblue')
}
</script>
</body>
</html>
I'm trying to make a linechart with D3 and React where the x axis is based on Date.now() object and all the ticks are a minute apart on a 10mn window.
I can't generate the line because I get "NaNNaNNaN" in my svg path;
Can't seem to figure out how to have ticks minutes apart on my x axis;
Here's how the data looks like
// data state
data = [
{"loadAverage":0.008333333333333333,"timestamp":1632740462342},
{"loadAverage":0.008333333333333333,"timestamp":1632740459323},
{"loadAverage":0.013333333333333334,"timestamp":1632740471400}
];
the timestamp key is a new Date.now() coming from the server
useEffect(() => {
const svg = d3.select(d3Container.current);
let margin = { top: 20, right: 20, bottom: 50, left: 70 },
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
// set the ranges
let x = d3
.scaleTime()
.domain(d3.extent(data, (d) => timeFormat(d.timestamp)))
.range([0, width]);
let y = d3
.scaleLinear()
.domain([0, d3.max(data, (d) => d.loadAverage)])
.nice()
.range([height, 0]);
// Parse the date
let parseTime = d3.timeParse("%s");
let timeFormat = d3.timeFormat("%M:%S");
// Constructing the line
const myLine = d3
.line()
.x((d) => {
const convertedTime = parseTime(d.timestamp);
return x(convertedTime);
})
.y((d) => {
return y(d.loadAverage);
});
svg
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
svg
.select("svg")
.selectAll("path")
.data([data])
.join("path")
.attr("d", (value) => myLine(value))
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-width", 1.5)
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round");
// Add the x Axis
svg
.select("svg")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
// Add the y Axis
svg
.select("svg")
.append("g")
.call(d3.axisLeft(y).tickFormat(timeFormat).ticks(10));
}, [data]);
This is my first time using D3, any help would be greatly appreciated !
Edit: here's what I tried so far
// Constructing the line
const myLine = d3
.line()
.x((d) => {
const convertedTime = new Date(d.timestamp);
return x(convertedTime);
})
.y((d) => {
return y(d.loadAverage);
});
Even tried to return convertedTime wrapped up by parsetime like so parsetime(convertedTime) Didn't work either.
I think you have a problem in Initializing x scale domain
// set the ranges
let x = d3
.scaleTime()
// ⬇️ here is the issue, just get rid of timeFormat
.domain(d3.extent(data, (d) => timeFormat(d.timestamp)))
.range([0, width]);
the scaleTime expect the domain to be a [Date|number, Date|number], you are using timeFormat which convert number|Date into a string based on the given format.
Try to use this instead:
let x = d3
.scaleTime()
.domain(d3.extent(data, (d) => d.timestamp))
.range([0, width]);
// The short vesion
let x = d3.scaleTime(d3.extent(data, (d) => d.timestamp), [0, width])
Constructing the line
const myLine = d3
.line()
.x((d) => x(d.timestamp))
.y((d) => y(d.loadAverage));
If you need to convert timestamps into Dates, you can map the whole data array
data = data.map(d=> d.timestamp = new Date(d.timestamp), d)
The goal is to create a smooth scrolling real time plot with multiple traces.
I was able to do this for a single trace, but when I add more lines to transition, the animation seems to get messed up. I have a feeling that transitions are being looped through and colliding, but I can't figure out how to prevent this.
If you set N_CH = 1 in the snippet, things run smoothly. When it's set to N_CH = 4 then the animation becomes jerky (seems like the transitions aren't fully completing) and also (interestingly) the x-axis scrolling appears to become 4 times faster than when N_CH = 1.
You can recover the smoothness by changing the transform in the tick() function to match the number of channels (i.e. iScale(-4) for N_CH = 4) but this isn't "correct" as the translation speed is artificially fast. In the end, I need accurate time measurement in real-time.
I've tried various different approaches including:
adding traces to a group and trying to translate the group
refactoring the data object and allowing d3 to iterate through the data structure with a selectAll() call
... the results always seem to be the same.
// set up some variables
const N_CH = 4;
const N_PTS = 40;
const margin = {top: 20, right: 30, bottom: 30, left: 40};
const width = 800;
const height = 300;
const colors = ['steelblue', 'red', 'orange', 'magenta']
// instantiate data array (timestamps)
var data = [];
var channelData = [];
for (let ch = 0; ch < N_CH; ch++) {
channelData = [];
for (let i = 0; i < N_PTS; i++) {
channelData.push({
x: Date.now() + i * 1000,
y: ch + Math.random()
})
}
data.push({
name: "CH" + ch,
values: channelData
});
}
// initialize //////////////////////////////
// instantiate svg and attach to DOM element
var svg = d3
.select("#chart")
.append("svg")
.attr("viewBox", `0 0 ${width} ${height}`)
// add clip path for smooth entry/exit
svg.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("x", margin.left)
.attr("y", margin.bottom)
.attr("width", width - margin.left - margin.right)
.attr("height", height - margin.top - margin.bottom);
// set index scale for data buffer position/transition
var iScale = d3.scaleLinear()
.range([0, width - margin.right])
.domain([0, data[0].values.length - 1]);
// set up x-axis scale for data x units (time)
var xScale = d3.scaleUtc()
.range([margin.left, width - margin.right])
// add x-axis to svg
var xAxis = svg.append("g")
.attr("class", "x-axis")
.attr("transform", `translate(0, ${height - margin.top})`)
.call(d3.axisBottom(xScale));
// set up y-axis
var yScale = d3.scaleLinear()
.range([height - margin.top, margin.bottom]);
// add y-axis to svg
var yAxis = svg.append("g")
.attr("class", "y-axis")
.attr("transform", `translate(${margin.left}, 0)`)
.call(d3.axisLeft(yScale));
// set the domains
xScale.domain(d3.extent(this.data[0].values, d => d.x));
// get global y domain
var flatten = [].concat.apply([], data.map(o => o.values))
yScale.domain(d3.extent(flatten, d => d.y));
// define the line
var line = d3.line()
.x((d, i) => iScale(i))
.y(d => yScale(d.y));
// make a group where we will append our paths
traces = svg.append("g")
.attr("clip-path", "url(#clip)")
for (let ch=0; ch<N_CH; ch++) {
traces.append("path")
.datum(data[ch].values)
.attr("id", `trace-${ch}`)
.attr("class", "trace")
.attr("d", line)
.attr("stroke", colors[ch])
.attr("fill", "none")
.attr("stroke-width", 1.5)
.attr("transform", "translate(0)")
}
// end initialize ////////////////////
// animate
tick();
function tick() {
// add data to buffer
let lastData;
for (let ch = 0; ch < N_CH; ch++) {
lastData = data[ch].values[data[ch].values.length - 1];
data[ch].values.push({
x: lastData.x + 1000,
y: ch + Math.random()
});
}
// update individual trace path data
for (let ch = 0; ch < N_CH; ch++) {
traces.select(`#trace-${ch}`)
.attr("d", line)
}
// animate transition
traces
.selectAll('.trace')
.attr("transform", "translate(0)")
.transition().duration(1000).ease(d3.easeLinear)
.attr("transform", `translate(${iScale(-1)}, 0)`)
.on("end", tick)
// update the domain
xScale.domain(d3.extent(data[0].values, d => d.x));
// animate/redraw axis
xAxis
.transition().duration(1000).ease(d3.easeLinear)
.call(d3.axisBottom(xScale));
for (let ch=0; ch<N_CH; ch++) {
data[ch].values.shift();
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div id="chart"></div>
There are a few issues here:
xScale vs iScale:
You draw your data based on iScale, but draw your axis based on xScale: there's a discrepancy here right away: the ranges of each scale are different. But there is no reason why you shouldn't use the same scale for both: this way you'll never have any discrepancy between drawing and axis. If you remove the clip path and remove the tick function, you'll notice your lines aren't initially rendered where you expect them:
Misuse of transition.end()
D3's transition event listeners are for each transition. You are transitioning many elements, this is triggered when every line finishes. So after the four lines finish transitioning the first time, you trigger the tick function four times: this results in all sorts of chaos since the function is intended to be called once to transition all lines at once.
On re-read of the question, you've spotted this issue of calling the tick function 4x instead of once:
You can recover the smoothness by changing the transform in the tick()
function to match the number of channels (i.e. iScale(-4) for N_CH =
4) but this isn't "correct" as the translation speed is artificially
fast.
If we fix this so that we call the tick function once, when all line transitions are complete, we address the smoothness issue:
// set up some variables
const N_CH = 4;
const N_PTS = 40;
const margin = {top: 20, right: 30, bottom: 30, left: 40};
const width = 800;
const height = 300;
const colors = ['steelblue', 'red', 'orange', 'magenta']
// instantiate data array (timestamps)
var data = [];
var channelData = [];
for (let ch = 0; ch < N_CH; ch++) {
channelData = [];
for (let i = 0; i < N_PTS; i++) {
channelData.push({
x: Date.now() + i * 1000,
y: ch + Math.random()
})
}
data.push({
name: "CH" + ch,
values: channelData
});
}
// initialize //////////////////////////////
// instantiate svg and attach to DOM element
var svg = d3
.select("#chart")
.append("svg")
.attr("viewBox", `0 0 ${width} ${height}`)
// add clip path for smooth entry/exit
svg.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("x", margin.left)
.attr("y", margin.bottom)
.attr("width", width - margin.left - margin.right)
.attr("height", height - margin.top - margin.bottom);
// set index scale for data buffer position/transition
var iScale = d3.scaleLinear()
.range([0, width - margin.right])
.domain([0, data[0].values.length - 1]);
// set up x-axis scale for data x units (time)
var xScale = d3.scaleUtc()
.range([margin.left, width - margin.right])
// add x-axis to svg
var xAxis = svg.append("g")
.attr("class", "x-axis")
.attr("transform", `translate(0, ${height - margin.top})`)
.call(d3.axisBottom(xScale));
// set up y-axis
var yScale = d3.scaleLinear()
.range([height - margin.top, margin.bottom]);
// add y-axis to svg
var yAxis = svg.append("g")
.attr("class", "y-axis")
.attr("transform", `translate(${margin.left}, 0)`)
.call(d3.axisLeft(yScale));
// set the domains
xScale.domain(d3.extent(this.data[0].values, d => d.x));
// get global y domain
var flatten = [].concat.apply([], data.map(o => o.values))
yScale.domain(d3.extent(flatten, d => d.y));
// define the line
var line = d3.line()
.x((d, i) => iScale(i))
.y(d => yScale(d.y));
// make a group where we will append our paths
traces = svg.append("g")
.attr("clip-path", "url(#clip)")
for (let ch=0; ch<N_CH; ch++) {
traces.append("path")
.datum(data[ch].values)
.attr("id", `trace-${ch}`)
.attr("class", "trace")
.attr("d", line)
.attr("stroke", colors[ch])
.attr("fill", "none")
.attr("stroke-width", 1.5)
.attr("transform", "translate(0)")
}
// end initialize ////////////////////
// animate
tick();
function tick() {
// add data to buffer
let lastData;
for (let ch = 0; ch < N_CH; ch++) {
lastData = data[ch].values[data[ch].values.length - 1];
data[ch].values.push({
x: lastData.x + 1000,
y: ch + Math.random()
});
}
// update individual trace path data
for (let ch = 0; ch < N_CH; ch++) {
traces.select(`#trace-${ch}`)
.attr("d", line)
}
// animate transition
traces
.selectAll('.trace')
.attr("transform", "translate(0)")
.transition().duration(1000).ease(d3.easeLinear)
.attr("transform", `translate(${iScale(-1)}, 0)`)
.end().then(tick);
// update the domain
xScale.domain(d3.extent(data[0].values, d => d.x));
// animate/redraw axis
xAxis
.transition().duration(1000).ease(d3.easeLinear)
.call(d3.axisBottom(xScale));
for (let ch=0; ch<N_CH; ch++) {
data[ch].values.shift();
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.0.0/d3.min.js"></script>
<div id="chart"></div>
In the above I use transition.end() to return a promise when all selected elements finish transitioning. I have upped your version of D3 as this is a newer function:
.end().then(tick);
Improvements:
Your code makes use of loops to append and modify elements. This creates additional overhead: selecting elements in the DOM takes time, you have to identify each line so you can reselect it again, and you have to do some extra legwork in binding the data. Let's simplify this with the d3 enter/update cycle:
Create the lines to start:
let lines = traces.selectAll(null)
.data(data)
.enter()
.append("path")
.attr("d", d=>line(d.values))
.attr("stroke", (d,i)=>colors[i])
.attr("fill", "none")
.attr("stroke-width", 1.5)
.attr("transform","translate(0,0)");
And now in the update/tick function we can modify the bound data easily:
lines.each(function(d,i) {
d.values.push({
x: d.values[d.values.length-1].x + dt,
y: i + Math.random()
})
})
.attr("d", d=>line(d.values))
We can remove the first data point of each line with:
lines.each(d=>d.values.shift());
Generally speaking (explicit) loops are very rare in manipulating SVG elements with D3, as it runs counter to principles that D3 was designed with. See here for some discussion on why that might matter and how it might be useful.
Together with removing the iScale and using transition.end(), we might get something like:
// set up some variables
const N_CH = 4;
const N_PTS = 40;
const margin = {top: 20, right: 30, bottom: 30, left: 40};
const width = 800;
const height = 300;
const colors = ['steelblue', 'red', 'orange', 'magenta']
// instantiate data array (timestamps)
var data = [];
var channelData = [];
for (let ch = 0; ch < N_CH; ch++) {
channelData = [];
for (let i = 0; i < N_PTS; i++) {
channelData.push({
x: Date.now() + i * 1000,
y: ch + Math.random()
})
}
data.push({
name: "CH" + ch,
values: channelData
});
}
// initialize //////////////////////////////
// instantiate svg and attach to DOM element
var svg = d3
.select("#chart")
.append("svg")
.attr("viewBox", `0 0 ${width} ${height}`)
// add clip path for smooth entry/exit
svg.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("x", margin.left)
.attr("y", margin.bottom)
.attr("width", width - margin.left - margin.right)
.attr("height", height - margin.top - margin.bottom);
// set up x-axis scale for data x units (time)
var xScale = d3.scaleTime()
.range([margin.left, width - margin.right])
.domain(d3.extent(data[0].values,d=>d.x))
// add x-axis to svg
var xAxis = svg.append("g")
.attr("class", "x-axis")
.attr("transform", `translate(0, ${height - margin.top})`)
.call(d3.axisBottom(xScale));
// set up y-axis
var yScale = d3.scaleLinear()
.range([height - margin.top, margin.bottom]);
// add y-axis to svg
var yAxis = svg.append("g")
.attr("class", "y-axis")
.attr("transform", `translate(${margin.left}, 0)`)
.call(d3.axisLeft(yScale));
// set the domains
xScale.domain(d3.extent(this.data[0].values, d => d.x));
// get global y domain
var flatten = [].concat.apply([], data.map(o => o.values))
yScale.domain(d3.extent(flatten, d => d.y));
// define the line
var line = d3.line()
.x(d => xScale(d.x))
.y(d => yScale(d.y));
// make a group where we will append our paths
traces = svg.append("g")
.attr("clip-path", "url(#clip)")
// Create lines:
let lines = traces.selectAll(null)
.data(data)
.enter()
.append("path")
.attr("d", d=>line(d.values))
.attr("stroke", (d,i)=>colors[i])
.attr("fill", "none")
.attr("stroke-width", 1.5)
.attr("transform","translate(0,0)");
transition();
function transition() {
let dt = 1000; // difference in time.
let dx = xScale(d3.timeMillisecond.offset(xScale.domain()[0],dt)) - xScale.range()[0]; // difference in pixels.
lines.each(function(d,i) {
d.values.push({
x: d.values[d.values.length-1].x + dt,
y: i + Math.random()
})
})
.attr("d", d=>line(d.values))
.transition()
.duration(1000)
.attr("transform",`translate(${-dx}, 0)`)
.ease(d3.easeLinear)
.end().then(function() {
lines.each(d=>d.values.shift())
.attr("transform","translate(0,0)")
transition();
})
xScale.domain(xScale
.domain()
.map(d=>d3.timeMillisecond.offset(d,dt)))
xAxis
.transition().duration(1000).ease(d3.easeLinear)
.call(d3.axisBottom(xScale))
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.0.0/d3.min.js"></script>
<div id="chart"></div>
I am new to using D3 charts and am having an issue making my multi line chart responsive. I have reviewed some examples on how to make D3 responsive, but I can not make it work on my graph. If someone could assist me in making the code I currently have responsive I would really appreciate it.
Here is the code:
const svg = d3.select("#lineChart").attr("height", 400).attr("width", 850);
const width = +svg.attr("width");
const height = +svg.attr("height");
let hasMoreThan0 = false;
const render = (data) => {
const xValue = (d) => d.dateTest;
const yValue = (d) => d.total;
const margin = { top: 60, right: 40, bottom: 88, left: 105 };
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
const xScale = d3
.scaleTime()
.domain(d3.extent(data, xValue))
.range([0, innerWidth])
.nice();
let yScale;
let totalArray = [];
data.forEach((element) => {
totalArray.push(element.total);
});
const reducer = (accumulator, currentValue) => accumulator + currentValue;
let tc = totalArray.reduce(reducer);
data.forEach((d) => {
if (tc == 0) {
yScale = d3
.scaleLinear()
.domain([0, 51])
.range([innerHeight, 0])
.nice();
} else {
yScale = d3
.scaleLinear()
.domain(d3.extent(data, yValue))
.range([innerHeight, 0])
.nice();
}
});
const g = svg
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
const xAxis = d3
.axisBottom(xScale)
.tickPadding(15)
.ticks(d3.timeDay)
.tickFormat(d3.timeFormat("%a %d %B %y"));
const yAxis = d3.axisLeft(yScale).tickSize(-innerWidth).tickPadding(10);
const yAxisG = g.append("g").call(yAxis);
yAxisG.selectAll(".domain").remove();
yAxisG
.append("text")
.attr("class", "axis-label")
.attr("y", -60)
.attr("x", -innerHeight / 2)
.attr("fill", "black")
.attr("transform", `rotate(-90)`)
.attr("text-anchor", "middle");
const xAxisG = g
.append("g")
.call(xAxis)
.attr("transform", `translate(0,${innerHeight})`);
xAxisG.select(".domain").remove();
xAxisG
.append("text")
.attr("class", "axis-label")
.attr("y", 80)
.attr("x", innerWidth / 2)
.attr("fill", "black");
const areaGenerator = d3
.area()
.x((d) => xScale(xValue(d)))
.y0(innerHeight)
.y1((d) => yScale(yValue(d)))
.curve(d3.curveMonotoneX);
g.append("path")
.attr("class", "area-path")
.attr("d", areaGenerator(data));
const lineGenerator = d3
.line()
.x((d) => xScale(xValue(d)))
.y((d) => yScale(yValue(d)))
.curve(d3.curveMonotoneX);
g.append("path")
.attr("class", "line-path")
.attr("d", lineGenerator(data));
// g.append("text").attr("class", "title").attr("y", -10).text(title);
svg.append("line").attr("class", "line-dashet");
svg.append("line").attr("class", "line-dashet");
svg
.selectAll("circle")
.data(data)
.join("circle")
.attr("cx", function (d) {
return xScale(xValue(d)) + 105;
})
.attr("cy", function (d) {
return yScale(yValue(d)) + 60;
})
.attr("r", 4)
.on("mouseout", function (e) {
let toolTip = document.getElementsByClassName("tooltip")[0];
toolTip.classList.remove("tooltip-open");
})
.on("mouseover", function (e) {
let selectPoint = d3.select(this);
let xLineAnim = selectPoint._groups[0][0].cx.animVal.value;
let yLineAnim = selectPoint._groups[0][0].cy.animVal.value;
let selectPointData = selectPoint._groups[0][0].__data__;
let dateFormated = formatDate(selectPointData.dateTest, a, "-");
let toolTip = document.getElementsByClassName("tooltip")[0];
toolTip.style.top = yLineAnim - 55 + "px";
toolTip.style.left = xLineAnim + 5 + "px";
toolTip.innerHTML =
"<div><span>Day: </span>" +
dateFormated +
"</div><div><span>Total: </span>" +
selectPointData.total +
"</div>";
toolTip.classList.add("tooltip-open");
svg
.select(".line-dashet")
.attr("x1", xLineAnim)
.attr("y1", margin.top)
.attr("x2", xLineAnim)
.attr("y2", innerHeight + margin.top);
});
};
getTimeData();
I posted a previous question with perhaps too much specificity. I'm trying to create a multi-line chart in d3 with a dropdown, similar to this.
I've switched out the obvious changes needed for v7 but am still running into trouble, I believe in the if/else statement right after var initialGraph but I'm not 100% sure. It may also be because my d3.groups isn't set up / referenced correctly.
The current error I receive is:
Uncaught (in promise) TypeError: Cannot read property 'year' of undefined
at <anonymous>:23:33
at a (d3.v7.min.js:2)
at initialGraph (<anonymous>:83:18)
at <anonymous>:89:3
My dataset has four values: year, state, wvalues, and lvalues. state and lvalues are strings, year and wvalues are numeric. Here's my code so far:
var margin = { top: 50, right: 50, bottom: 50, left: 50 }
var h = 500 - margin.top - margin.bottom
var w = 700 - margin.left - margin.right
var formatDecimal = d3.format('.2')
d3.csv('15/data.csv').then(function (data) {
// Scales
var x = d3.scaleLinear()
.range([0,w])
var y = d3.scaleLinear()
.range([h,0])
y.domain([
d3.min([0,d3.min(data,function (d) { return d.wvalue })]),
d3.max([0,d3.max(data,function (d) { return d.wvalue })])
]);
x.domain([1968, 2016])
// Define the line
var valueLine = d3.line()
.x(function(d) { return x(d.year); })
.y(function(d) { return y(d.wvalue); })
// Create the svg canvas in the "d3block" div
var svg = d3.select("#d3block")
.append("svg")
.style("width", w + margin.left + margin.right + "px")
.style("height", h + margin.top + margin.bottom + "px")
.attr("width", w + margin.left + margin.right)
.attr("height", h + margin.top + margin.bottom)
.append("g")
.attr("transform","translate(" + margin.left + "," + margin.top + ")")
.attr("class", "svg");
//nest variable
var nest = d3.groups(data,
d => d.state, d => d.lvalue)
// X-axis
var xAxis = d3.axisBottom()
.scale(x)
.tickFormat(formatDecimal)
.ticks(7)
// Y-axis
var yAxis = d3.axisLeft()
.scale(y)
.tickFormat(formatDecimal)
.ticks(5)
// Create a dropdown
var legisMenu = d3.select("#legisDropdown")
legisMenu
.append("select")
.selectAll("option")
.data(nest)
.enter()
.append("option")
.attr("value", ([key, ]) => key)
.text(([key, ]) => key)
// Function to create the initial graph
var initialGraph = function(legis){
// Filter the data to include only state of interest
var selectLegis = nest.filter(([key, ]) => key == legis)
var selectLegisGroups = svg.selectAll(".legisGroups")
.data(selectLegis, function(d){
return d ? d.key : this.key;
})
.enter()
.append("g")
.attr("class", "legisGroups")
var initialPath = selectLegisGroups.selectAll(".line")
.data(([, values]) => values)
.enter()
.append("path")
initialPath
.attr("d", valueLine(([, values]) => values))
.attr("class", "line")
}
// Create initial graph
initialGraph("Alabama")
// Update the data
var updateGraph = function(legis){
// Filter the data to include only state of interest
var selectLegis = nest.filter(([key, ]) => key == legis)
// Select all of the grouped elements and update the data
var selectLegisGroups = svg.selectAll(".legisGroups")
.data(selectLegis)
// Select all the lines and transition to new positions
selectLegisGroups.selectAll("path.line")
.data(([, values]) => values)
.transition()
.duration(1000)
.attr("d", valueLine(([, values ]) => values))
}
// Run update function when dropdown selection changes
legisMenu.on('change', function(){
// Find which state was selected from the dropdown
var selectedLegis = d3.select(this)
.select("select")
.property("value")
// Run update function with the selected state
updateGraph(selectedLegis)
});
// X-axis
svg.append('g')
.attr('class','axis')
.attr('id','xAxis')
.attr('transform', 'translate(0,' + h + ')')
.call(xAxis)
.append('text') // X-axis Label
.attr('id','xAxisLabel')
.attr('fill','black')
.attr('y',-10)
.attr('x',w)
.attr('dy','.71em')
.style('text-anchor','end')
.text('')
// Y-axis
svg.append('g')
.attr('class','axis')
.attr('id','yAxis')
.call(yAxis)
.append('text') // y-axis Label
.attr('id', 'yAxisLabel')
.attr('fill', 'black')
.attr('transform','rotate(-90)')
.attr('x',0)
.attr('y',5)
.attr('dy','.71em')
.style('text-anchor','end')
.text('wvalue')
})
I did more digging and found the answer here.
I had to replace the initial path attribute .attr("d", valueLine(([, values]) => values)) with .attr('d', (d) => valueLine(Array.from(d.values())[1])). I also had to replace the code further down within the updateGraph function under selectLegisGroups .attr for it to update properly.