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>
Related
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.
Link to the notebook.
I'm working on a small multiple line chart using d3.v5 on Observable, with the dataset structured like follows:
For visualization, the y scale takes num from the values array for the domain. There are several rows with unique key values, which I wanted to use to produce the small multiples. The image above shows the first key.
After visualizing the small multiple, I noticed that all the line charts are using the same y scale, which is not what I intended to do. This is what I currently have:
const y_scale = d3
.scaleLinear()
.domain([0, d3.max(series, d => d3.max(d.values, m => m.num))])
.range([width/2, width/2 - start_y - margin.bottom]);
Is there a way to adjust the domain so that each chart would have its own scale based on its own num values?
Edit 1: Notebook link added on top
The idiomatic D3 solution here would be using local variables. However, there are several different working alternatives.
For using local variables, we first declare them...
const localScale = d3.local();
const localLine = d3.local();
Then, we set the different scales in the "enter" selection:
var enter = my_group
.enter()
.append("g")
.attr("class", "chart_group")
.each(function(d) {
const yScale = localScale.set(this, d3
.scaleLinear()
.domain([0, d3.max(d.values, d => d.num)])
.range([panel_width / 2, panel_width / 2 - start_y - margin]));
localLine.set(this, d3
.line()
.x(d => x_scale(d.date))
.y(d => yScale(d.num)));
});
Finally, we get those scales:
sub_group
.select(".chart_line")
.attr("d", function(d) {
return localLine.get(this)(d)
})
Here is the whole cell, copy/paste this in your notebook, replacing your cell:
chart = {
const panels_per_row = 4;
const panel_width = (width - margin * 8) / panels_per_row;
const height =
margin + (panel_width + margin) * (parseInt(my_data.length / 2) + 1);
const svg = d3.create("svg").attr("viewBox", [0, 0, width, height]);
const start_x = 2;
const start_y = panel_width / 3 + margin;
const x_scale = d3
.scaleBand()
.domain(d3.set(series[0].values, d => d.date).values())
.range([0, panel_width]);
const localScale = d3.local();
const localLine = d3.local();
//join
var my_group = svg.selectAll('.chart_group').data(series, d => d.key);
//exit and remove
my_group.exit().remove();
//enter new groups
var enter = my_group
.enter()
.append("g")
.attr("class", "chart_group")
.each(function(d) {
const yScale = localScale.set(this, d3
.scaleLinear()
.domain([0, d3.max(d.values, d => d.num)])
.range([panel_width / 2, panel_width / 2 - start_y - margin]));
localLine.set(this, d3
.line()
.x(d => x_scale(d.date))
.y(d => yScale(d.num)));
});
//append elements to new group
enter.append("rect").attr("class", "group_rect");
enter.append("text").attr("class", "group_text");
enter.append("g").attr("class", "sub_chart_group");
//merge
my_group = my_group.merge(enter);
position_group_elements(my_group);
//join
var sub_group = my_group
.select(".sub_chart_group")
.selectAll('.sub_chart_elements_group')
.data(d => [d.values]); // data is wrapped in an array because this is a line/area chart
//exit and remove
sub_group.exit().remove();
//enter new groups
var sub_enter = sub_group
.enter()
.append("g")
.attr("class", "sub_chart_elements_group");
//append elements to new group
sub_enter.append("path").attr("class", "chart_line");
//merge
sub_group = sub_group.merge(sub_enter);
sub_group
.select(".chart_line")
.attr("d", function(d) {
return localLine.get(this)(d)
})
.attr("fill", "none")
.attr("stroke", "black")
.attr("stroke-width", 1)
.attr("transform", "translate(" + start_x + "," + start_y + ")");
function position_group_elements(my_group) {
//position rectangle
my_group
.select(".group_rect")
.attr("x", function(d, i) {
//two groups per row so
var position = i % panels_per_row;
d.x_pos = position * (panel_width + margin) + margin;
d.y_pos =
parseInt(i / panels_per_row) * (panel_width + margin) + margin;
return d.x_pos;
})
.attr("y", d => d.y_pos)
.attr("fill", "#eee")
.attr("stroke", "#aaa")
.attr("stroke-width", 1)
.attr("width", panel_width)
.attr("height", panel_width);
//then position sub groups
my_group
.select(".sub_chart_group")
.attr("id", d => d.key)
.attr("transform", d => "translate(" + d.x_pos + "," + d.y_pos + ")");
}
return svg.node();
}
I am creating an application which works with a bluetooth heart rate monitor, and I will be using D3.js to graph the heart rate in real time. Currently, I'm just creating a simple example which I can tweak later to work with real data.
Basically all I am trying to do at this stage is plot randomly generated data over the last minute. The data, readings, is an array of dummy readings of the form {timestamp: (the time which it was generated), hr:(num between 0 and 1)}. Initially readings will be empty but every second, a new random value with the current time will be pushed on.
For some reason, the graph isn't showing, however there are no errors being reported in my code. The x-axis seems to be correct and transitioning fine. Any advice or help would be much appreciated.
I am going to post my entire code as I am not sure of the origin of the issue.
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.line {
fill: none;
stroke: blue;
stroke-width: 2px;
}
</style>
<svg width="600" height="400"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
var n = 40,
random = d3.randomUniform(0, 1),
readings = [],
currentReadings = [],
duration = 1000,
now = new Date(Date.now() - duration);
startTime = new Date();
var svg = d3.select("svg"),
margin = {top: 20, right: 20, bottom: 20, left: 40},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var xScale = d3.scaleTime()
.domain([now - 60000, now])
.range([0, width]);
var yScale = d3.scaleLinear()
.domain([0, 1])
.range([height, 0]);
var line = d3.line()
.x(function(data) { return xScale(data.time); })
.y(function(data) { return yScale(data.hr); })
.curve(d3.curveBasis);
g.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height);
var xAxisGroup = g.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0, " + height + ")")
.call(xAxis = d3.axisBottom(xScale).
ticks(3));
var yAxisGroup = g.append("g")
.attr("class", "axis axis--y")
.call(d3.axisLeft(yScale));
var path = g.append("g")
.attr("clip-path", "url(#clip)")
.append("path")
.datum(currentReadings)
.attr("class", "line")
.transition()
.duration(1000)
.ease(d3.easeLinear)
.on("start", tick);
function tick() {
now = new Date();
// update the x axis domain and slide left
xScale.domain([now - 60000, now]);
xAxisGroup.call(xAxis);
// generate new random reading
reading = {time: new Date(), hr: random()};
// Push the new reading onto readings and currentReadings
readings.push(reading);
currentReadings.push(reading);
// Remove readings not within graph period
for (var i=0; i<currentReadings.length; i++) {
if (now - currentReadings[i].time > 60000)
currentReadings.shift();
else
break;
}
// Redraw the line.
d3.select(this)
.datum(currentReadings)
.attr("d", line)
.attr("transform", null);
// Slide it to the left.
d3.active(this)
.attr("transform", "translate(" + xScale(0) + ",0)")
.transition()
.duration(duration)
.on("start", tick);
}
</script>
The key issue is that you are translating the graph too much. It is drawing, just not anywhere remotely visible. Why?
d3.active(this)
.attr("transform", "translate(" + xScale(0) + ",0)")
.transition()
.duration(duration)
.on("start", tick);
You are translating by xScale(0), which is some very high magnitude negative number:
var now = new Date();
var xScale = d3.scaleTime()
.domain([now - 60000, now])
.range([0, 100]);
console.log(xScale(0));
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
So you'll never see your graph, it's well to the left. Instead, you could translate by the difference in x values between coordinates (as you update once per second, and show 60 values across your width, that could be: width/60). If the incoming points are inconsistent, then you'll need to translate by the width difference between the incoming points is in order to scroll the graph by an appropriate amount.
The snippet below shows your chart assuming a constant rate of data input (I've also applied a transition to the x axis so it scrolls and doesn't jump):
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.line {
fill: none;
stroke: blue;
stroke-width: 2px;
}
</style>
<svg width="600" height="400"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
var n = 40,
random = d3.randomUniform(0, 1),
readings = [],
currentReadings = [],
duration = 1000,
now = new Date(Date.now() - duration);
startTime = new Date();
var svg = d3.select("svg"),
margin = {top: 20, right: 20, bottom: 20, left: 40},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var xScale = d3.scaleTime()
.domain([now - 60000, now])
.range([0, width]);
var yScale = d3.scaleLinear()
.domain([0, 1])
.range([height, 0]);
var line = d3.line()
.x(function(data) { return xScale(data.time); })
.y(function(data) { return yScale(data.hr); })
.curve(d3.curveBasis);
g.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height);
var xAxisGroup = g.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0, " + height + ")")
.call(xAxis = d3.axisBottom(xScale).
ticks(3));
var yAxisGroup = g.append("g")
.attr("class", "axis axis--y")
.call(d3.axisLeft(yScale));
var path = g.append("g")
.attr("clip-path", "url(#clip)")
.append("path")
.datum(currentReadings)
.attr("class", "line")
.transition()
.duration(1000)
.ease(d3.easeLinear)
.on("start", tick);
function tick() {
now = new Date();
// update the x axis domain and slide left
xScale.domain([now - 60000, now]);
// transition the axis:
xAxisGroup.transition().duration(1000).ease(d3.easeLinear).call(xAxis);
// generate new random reading
reading = {time: new Date(), hr: random()};
// Push the new reading onto readings and currentReadings
readings.push(reading);
currentReadings.push(reading);
// Remove readings not within graph period
for (var i=0; i<currentReadings.length; i++) {
if (now - currentReadings[i].time > 60000)
currentReadings.shift();
else
break;
}
// Redraw the line.
d3.select(this)
.datum(currentReadings)
.attr("d", line(currentReadings))
.attr("transform", null);
// Slide it to the left.
d3.active(this)
.attr("transform", "translate(" + -width/60 + ",0)") // transition based on distance between each data point.
.transition()
.duration(duration)
.on("start", tick);
}
</script>
The jumpiness of the leading part of the chart is do to the easing and corrections to the easing made once the next point is generated
I am trying to create a line graph in d3.js but only my axes are appearing; the line doesn't show.
Things that are working:1. My axes are labelled correctly 2. Looking at the elements of the page in Chrome it seems the x and y attributes for the line are 'working' (i.e. the data for coordinates are defined for the line/are not 'NaN' values). I think there must be something wrong with attributes associated with my line (end of the Javascript code).
Are there any reasons this might be happening?
This is what my plot/graph output currently looks like:
Current state of plot
Here is my HTML, Javascript and the data I've used for the plot:
HTML:
<html>
<head>
<script src="https://d3js.org/d3.v4.min.js"></script>
</head>
<body>
<div id="merit-order-chart"></div>
</body>
<script type="text/javascript" src="/src.js"></script>
</html>
JAVASCRIPT:
// create a SVG element
let svg2 = d3.select("#merit-order-chart").append("svg");
// sizing parameters
let margin2 = {top: 20, right: 50, bottom: 40, left: 80};
let width2 = 800;
let height2 = 400;
let chartWidth2 = width2 - margin2.left - margin2.right;
let chartHeight2 = height2 - margin2.top - margin2.bottom;
// sizing the SVG
svg2.attr("width", width2 + "px")
.attr("height", height2 + "px");
// creating the x and y scales
let y2 = d3.scaleLinear()
.clamp(true)
.range([chartHeight2, 0]);
let x2 = d3.scaleTime()
.clamp(true)
.range([0, chartWidth2]);
// formatting of the x and y axes
let xAxis2 = d3.axisBottom()
.scale(x2)
.tickFormat(d3.timeFormat("%Y-%m-%d %H:%M:%S"))
.ticks(4);
let yAxis2 = d3.axisLeft()
.scale(y2)
.ticks(8);
// adding a 'group' element for all the things attached to the chart
let chart2 = svg2.append("g")
.attr("transform", `translate(${margin2.left},${margin2.top})`);
// adding the x and y axis elements to the chart group (g)
const xg2 = chart2.append("g")
.classed("x axis", true)
.attr("transform", `translate(0,${chartHeight2})`)
.call(xAxis2);
const yg2 = chart2.append("g")
.classed("y axis", true)
.call(yAxis2);
d3.csv("/price-data.csv", (err, csv) => {
const clean2 = csv.map(d2 => {
// clean up number formats
d2.p = parseFloat(d2.p);
d2.settlementdate = Date.parse(d2.settlementdate)
d2.index = parseFloat(d2.index);
return d2;
});
// re-sizing the x and y axes
x2.domain([d3.min(clean2, d2 => d2.settlementdate), d3.max(clean2, d2 => d2.settlementdate)]);
xg2.call(xAxis2);
y2.domain([-1000, 14125]);
yg2.call(yAxis2);
chart2.selectAll(".prices")
.data(clean2)
.enter()
.append("line")
.attr("x", d2 => x2(d2.settlementdate))
.attr("y", d2 => y2(d2.p))
.attr("stroke-width", 5)
.attr("stroke", "black")
//.style("stroke", "rgb(6,120,155)");
});
DATA (.csv):
settlementdate,p,index
1/1/2017 0:00,50,1
1/1/2017 0:05,35,2
1/1/2017 0:10,100,3
1/1/2017 0:15,5000,4
You need to use a line generator, currently you are passing an array of objects representing each point, and appending a line for each one - this approach won't work (partly because lines don't have x and y attributes, but x1,x2,y1,y2 attributes).
You need to use a line generator:
let line = d3.line()
.x(function(d) { return x2(d.settlementdate); }) // x value for each point
.y(function(d) { return y2(d.p); }) // y value for each point
This will return a path with one vertex for every coordinate fed to it. Consequently you'll want to append a path rather than a line, and the drawing instructions for a path are contained in the d attribute, so you can use .attr("d", line).
Lastly, since you want one path per dataset, rather than one path per datapoint, nest your data into an array. By doing so you are getting one line with many points, rather than many lines with no points.
I changed the scale to show the curve, but it cuts out the peak as a result:
chart2.selectAll(".prices")
.data([clean2])
.enter()
.append("path")
.attr("d",line)
.attr("stroke-width", 5)
.attr("stroke", "black")
.attr("fill","none")
var csv = [
{ settlementdate: "1/1/2017 0:00",p:50,index:1 },
{ settlementdate: "1/1/2017 0:05",p:35,index:2 },
{ settlementdate: "1/1/2017 0:10",p:100,index:3 },
{ settlementdate: "1/1/2017 0:15",p:5000,index:4 }
]
// create a SVG element
let svg2 = d3.select("#merit-order-chart").append("svg");
// sizing parameters
let margin2 = {top: 20, right: 50, bottom: 40, left: 80};
let width2 = 800;
let height2 = 400;
let chartWidth2 = width2 - margin2.left - margin2.right;
let chartHeight2 = height2 - margin2.top - margin2.bottom;
// sizing the SVG
svg2.attr("width", width2 + "px")
.attr("height", height2 + "px");
// creating the x and y scales
let y2 = d3.scaleLinear()
.clamp(true)
.range([chartHeight2, 0]);
let x2 = d3.scaleTime()
.clamp(true)
.range([0, chartWidth2]);
// formatting of the x and y axes
let xAxis2 = d3.axisBottom()
.scale(x2)
.tickFormat(d3.timeFormat("%Y-%m-%d %H:%M:%S"))
.ticks(4);
let yAxis2 = d3.axisLeft()
.scale(y2)
.ticks(8);
// adding a 'group' element for all the things attached to the chart
let chart2 = svg2.append("g")
.attr("transform", `translate(${margin2.left},${margin2.top})`);
// adding the x and y axis elements to the chart group (g)
const xg2 = chart2.append("g")
.classed("x axis", true)
.attr("transform", `translate(0,${chartHeight2})`)
.call(xAxis2);
const yg2 = chart2.append("g")
.classed("y axis", true)
.call(yAxis2);
let line = d3.line()
.x(function(d) { return x2(d.settlementdate); })
.y(function(d) { return y2(d.p); })
const clean2 = csv.map(d2 => {
// clean up number formats
d2.p = parseFloat(d2.p);
d2.settlementdate = Date.parse(d2.settlementdate)
d2.index = parseFloat(d2.index);
return d2;
});
// re-sizing the x and y axes
x2.domain([d3.min(clean2, d2 => d2.settlementdate), d3.max(clean2, d2 => d2.settlementdate)]);
xg2.call(xAxis2);
y2.domain([0, 200]);
yg2.call(yAxis2);
chart2.selectAll(".prices")
.data([clean2])
.enter()
.append("path")
.attr("d",line)
.attr("stroke-width", 5)
.attr("stroke", "black")
.attr("fill","none")
//.style("stroke", "rgb(6,120,155)");
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
<div id="merit-order-chart"></div>
I am a beginner and I got a problem when I try to select some element in a lineChart.
I know there are so many elites, So I come here for some help :)
I find a brush demo in there and a linechart(sorry they don't allow me to insert 2 links because I don't have 10 reputations) demo Then I write the code blow:
<!DOCTYPE html>
<meta charset="utf-8">
<style type="text/css">
/* 13. Basic Styling with CSS */
/* Style the lines by removing the fill and applying a stroke */
.line {
fill: none;
stroke: #ffab00;
stroke-width: 3;
}
/* Style the dots by assigning a fill and stroke */
.dot {
fill: #1 fab40;
stroke: #fff;
}
.dot .selected {
stroke: red;
}
</style>
<!-- Body tag is where we will append our SVG and SVG objects-->
<body>
</body>
<!-- Load in the d3 library -->
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
// 2. Use the margin convention practice
var margin = {top: 20, right: 50, bottom: 20, left: 50}
, width = window.innerWidth - margin.left - margin.right // Use the window's width
, height = window.innerHeight - margin.top - margin.bottom; // Use the window's height
// The number of datapoints
var n = 21;
// 5. X scale will use the index of our data
var xScale = d3.scaleLinear()
.domain([0, n-1]) // input
.range([0, width]); // output
// 6. Y scale will use the randomly generate number
var yScale = d3.scaleLinear()
.domain([0, 1]) // input
.range([height, 0]); // output
// 7. d3's line generator
var line = d3.line()
.x(function(d, i) { return xScale(i); }) // set the x values for the line generator
.y(function(d) { return yScale(d.y); }) // set the y values for the line generator
.curve(d3.curveMonotoneX);// apply smoothing to the line
// 8. An array of objects of length N. Each object has key -> value pair, the key being "y" and the value is a random number
var dataset = d3.range(n).map(function(d) { return {"y": d3.randomUniform(1)() } });
// 1. Add the SVG to the page and employ #2
var svg = d3.select("body").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 + ")");
// 3. Call the x axis in a group tag
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(xScale)); // Create an axis component with d3.axisBottom
// 4. Call the y axis in a group tag
svg.append("g")
.attr("class", "y axis")
.call(d3.axisLeft(yScale)); // Create an axis component with d3.axisLeft
// 9. Append the path, bind the data, and call the line generator
svg.append("path")
.datum(dataset) // 10. Binds data to the line
.attr("class", "line") // Assign a class for styling
.attr("d", line); // 11. Calls the line generator
// 12. Appends a circle for each datapoint
var dot = svg.append("g")
.attr("class", "dot") // Assign a class for styling
.selectAll(".dot")
.data(dataset)
.enter().append("circle") // Uses the enter().append() method
.attr("r", 5)
.attr("cx", function(d, i) { return xScale(i) })
.attr("cy", function(d) { return yScale(d.y) });
var brush = svg.append("g")
.attr("class", "brush")
.call(d3.brush()
.extent([[0, 0], [width, height]])
.on("start brush end", brushed));
function brushed() {
var selection = d3.event.selection;
dot.classed("selected", selection && function(d) {
return selection[0][0] <= d.x && d.x < selection[1][0]
&& selection[0][1] <= d.y && d.y < selection[1][1];
});
}
</script>
I try to write the same code like this demo, But I can't selected any dot, Where is the problem?
Thanks a lot!
Rewrite your brushed function like this:
function brushed() {
var selection = d3.event.selection;
dot.classed("selected", selection && function(d,i) {
return selection[0][0] <= xScale(i) && xScale(i) < selection[1][0]
&& selection[0][1] <= yScale(d.y) && yScale(d.y) < selection[1][1];
});
}
Pay attention, we use xScale and yScale here, for correctly matching selected area and coordinates of dots. Working example is below:
// 2. Use the margin convention practice
var margin = {top: 20, right: 50, bottom: 20, left: 50}
, width = window.innerWidth - margin.left - margin.right // Use the window's width
, height = window.innerHeight - margin.top - margin.bottom; // Use the window's height
// The number of datapoints
var n = 21;
// 5. X scale will use the index of our data
var xScale = d3.scaleLinear()
.domain([0, n-1]) // input
.range([0, width]); // output
// 6. Y scale will use the randomly generate number
var yScale = d3.scaleLinear()
.domain([0, 1]) // input
.range([height, 0]); // output
// 7. d3's line generator
var line = d3.line()
.x(function(d, i) { return xScale(i); }) // set the x values for the line generator
.y(function(d) { return yScale(d.y); }) // set the y values for the line generator
.curve(d3.curveMonotoneX);// apply smoothing to the line
// 8. An array of objects of length N. Each object has key -> value pair, the key being "y" and the value is a random number
var dataset = d3.range(n).map(function(d) { return {"y": d3.randomUniform(1)() } });
// 1. Add the SVG to the page and employ #2
var svg = d3.select("body").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 + ")");
// 3. Call the x axis in a group tag
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(xScale)); // Create an axis component with d3.axisBottom
// 4. Call the y axis in a group tag
svg.append("g")
.attr("class", "y axis")
.call(d3.axisLeft(yScale)); // Create an axis component with d3.axisLeft
// 9. Append the path, bind the data, and call the line generator
svg.append("path")
.datum(dataset) // 10. Binds data to the line
.attr("class", "line") // Assign a class for styling
.attr("d", line); // 11. Calls the line generator
// 12. Appends a circle for each datapoint
var dot = svg.append("g")
.attr("class", "dot") // Assign a class for styling
.selectAll(".dot")
.data(dataset)
.enter().append("circle") // Uses the enter().append() method
.attr("r", 5)
.attr("cx", function(d, i) { return xScale(i) })
.attr("cy", function(d) { return yScale(d.y) });
var brush = svg.append("g")
.attr("class", "brush")
.call(d3.brush()
.extent([[0, 0], [width, height]])
.on("start brush", brushed)
.on("end", getSelectedDots)
);
var selectedDots = [];
function getSelectedDots() {
var selection = d3.event.selection;
dot.classed("selected", selection && function(d,i) {
var isSelectedDot = selection[0][0] <= xScale(i) && xScale(i) < selection[1][0]
&& selection[0][1] <= yScale(d.y) && yScale(d.y) < selection[1][1];
if (isSelectedDot) {
selectedDots.push(d);
}
return isSelectedDot;
});
console.log('selectedDots ', selectedDots);
}
function brushed() {
var selection = d3.event.selection;
dot.classed("selected", selection && function(d,i) {
return selection[0][0] <= xScale(i) && xScale(i) < selection[1][0]
&& selection[0][1] <= yScale(d.y) && yScale(d.y) < selection[1][1];
});
}
/* 13. Basic Styling with CSS */
/* Style the lines by removing the fill and applying a stroke */
.line {
fill: none;
stroke: #ffab00;
stroke-width: 3;
}
/* Style the dots by assigning a fill and stroke */
.dot {
fill: #1fab40;
stroke: #fff;
}
.dot .selected {
stroke: red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.11.0/d3.min.js"></script>