Slight lag in translation animation using D3.interval() - javascript

I have created a time series animation. Essentially, I generate a random datum every 20ms and the time axis slides continuously leftwards on a one second interval. D3.interval() creates what I want, but the graph seems "choppy". I assume it has something to do with the web browser or D3? Here is my code (expand the snippet window for a better view):
////////////////////////////////////////////////////////////
////////////////////// Set-up /////////////////////////////
////////////////////////////////////////////////////////////
const margin = { left: 80, right: 80, top: 30, bottom: 165},
TIME_MEASUREMENT = 20,
TIME_INTERVAL = 1000,
TIME_FRAME = 10000;
let width = $("#chart").width() - margin.left - margin.right,
height = $("#chart").height() - margin.top - margin.bottom;
let date1 = 0,
date2 = TIME_FRAME;
let accuracy = 20,
precision = 20,
aF = 2, //accuracyFactor
pF = 1; //precisionFactor
let random = d3.randomUniform(-(pF * precision), pF * precision),
count = TIME_FRAME/TIME_MEASUREMENT + 1;
let data = d3.range(count).map((d,i) => random() + accuracy );
const yDomain = [0, 50],
yTickValues = [0, 10, 20, 30, 40, 50];
////////////////////////////////////////////////////////////
///////////////////////// SVG //////////////////////////////
////////////////////////////////////////////////////////////
const svg = d3.select("#chart")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
const g = svg.append("g")
.attr("transform", "translate(" + margin.left + ", " + margin.top + ")");
////////////////////////////////////////////////////////////
///////////////////// Axes & Scales ////////////////////////
////////////////////////////////////////////////////////////
//X axis - dynamic
let xAxisGroup = g.append("g")
.attr("class", "x-axis")
.attr("transform", "translate(0," + (height) + ")");
let xScale = d3.scaleTime();
const xAxis = d3.axisBottom(xScale)
.ticks(d3.timeSecond.every(1))
.tickSizeInner(15)
.tickFormat(d3.timeFormat("%M:%S"));
//Y axis - static
let yAxisGroup = g.append("g")
.attr("class", "y-axis");
let yScale = d3.scaleLinear()
.domain(yDomain)
.range([height, 0]);
const yAxis = d3.axisLeft()
.scale(yScale)
.tickValues(yTickValues)
.tickFormat(d3.format(".0f"));
yAxisGroup.call(yAxis);
//Data points - static
let dataScale = d3.scaleTime()
.domain([date1,date2])
.range([0,width]);
////////////////////////////////////////////////////////////
/////////////////////// Functions //////////////////////////
////////////////////////////////////////////////////////////
function drawData(){
xScale.domain([date1, date2])
.range([0, width]);
xAxisGroup
.transition()
.duration(TIME_MEASUREMENT)
.ease(d3.easeLinear)
.call(xAxis);
g.selectAll("circle")
.data(data)
.join(
function(enter){
enter.append("circle")
.attr("cx", (d,i) => dataScale(i*TIME_MEASUREMENT))
.attr("cy", (d,i) => yScale(d))
.attr("fill", "black")
.attr("r","3");
},
function(update){
update
.attr("cx", (d,i) => dataScale(i*TIME_MEASUREMENT))
.attr("cy", (d,i) => yScale(d))
.transition()
.ease(d3.easeLinear)
.attr("transform", "translate(" + dataScale(-TIME_MEASUREMENT) + ", 0)");
}
);
data.push(random() + accuracy);
data.shift();
date1 += TIME_INTERVAL/50;
date2 += TIME_INTERVAL/50;
};
////////////////////////////////////////////////////////////
///////////////////////// Main /////////////////////////////
////////////////////////////////////////////////////////////
d3.interval(drawData, TIME_MEASUREMENT);
x-axis,
.y-axis {
font-size: 0.8em;
stroke-width: 0.06em;
}
#chart {
width: 600px;
height: 500px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="//d3js.org/d3.v6.min.js"></script>
<div id="chart"></div>
I don't get this "choppy effect" when I use d3.timer or setInterval, but each second slides as if it were ~850ms. Thank you for your input!

Related

How to place the bars of a bar chart in the right positions of the xAxis using d3.js?

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>

How to fix a multiple brush chart in d3

I am so close this is killing me. I've generated a simple brush for one column and it's generating the limits it's set to perfectly. The thing is I'd like multiple brushes for multiple columns ['A', 'B', 'C', 'D']. I could write this out four times, but I've put it in a function that doesn't appear to work. Please see the working code below, I've commented out the part that doesn't work. I know I need to somehow bind the data and loop through, but how do I do this efficiently?
var margin = {
top: 20,
right: 20,
bottom: 30,
left: 50
},
width = 960 - margin.left - margin.right,
height = 180 - margin.top - margin.bottom;
var x = d3.scale.ordinal()
.domain(["A", "B", "C", "D"])
.rangeBands([0, 200])
var y = d3.scale.linear()
.range([height, 0])
.domain([0, 100])
var xAxis = d3.svg.axis()
.scale(x)
var svg = d3.select("#timeline").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 + ")");
// define brush control element and its events
var brush = d3.svg.brush().y(y)
.on("brushend", () => console.log('A Extent: ', brush.extent()))
// create svg group with class brush and call brush on it
var brushg = svg.append("g")
.attr("class", "brush")
.call(brush);
// set brush extent to rect and define objects height
brushg.selectAll("rect")
.attr("x", x("A"))
.attr("width", 20);
/*
var brush = (d) => {
var brush = d3.svg.brush().y(y)
.on("brushend", () => console.log(d, ' Extent: ', brush.extent()))
var brushg = svg.append("g")
.attr("class", "brush")
.call(brush1);
brushg.selectAll("rect")
.attr("x", x("A"))
.attr("width", 20);
}
*/
.brush {
fill: lightgray;
fill-opacity: .75;
shape-rendering: crispEdges;
}
<script src='https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.6/d3.min.js'></script>
<div class="container">
<div id="timeline"></div>
</div>
Treat the brushes as data, as they map to each ordinal value on the x axis. Create the brushes with .enter and append all the necessary functionality. The .each function is similar to call, but runs on each element separately. This is very useful to contain the generation of the brushes.
xData = ["A", "B", "C", "D"];
var margin = {
top: 20,
right: 20,
bottom: 30,
left: 50
},
width = 960 - margin.left - margin.right,
height = 180 - margin.top - margin.bottom;
var x = d3.scale.ordinal()
.domain(xData)
.rangeBands([0, 200])
var y = d3.scale.linear()
.range([height, 0])
.domain([0, 100])
var xAxis = d3.svg.axis()
.scale(x)
var svg = d3.select("#timeline").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 + ")");
const brushes = svg.selectAll('g.brush')
.data(xData)
.enter()
.append('g')
.attr('class', 'brush')
.each(function(d) {
const el = d3.select(this);
const brush = d3.svg.brush().y(y).on("brushend", () => console.log(d, ' Extent: ', brush.extent()));
el.call(brush);
el.selectAll("rect")
.attr("x", x(d))
.attr("width", 20);
});
.brush {
fill: lightgray;
fill-opacity: .75;
shape-rendering: crispEdges;
}
<script src='https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.6/d3.min.js'></script>
<div class="container">
<div id="timeline"></div>
</div>

Making data points transition smoothly?

I am trying to make a time series move smoothly like my x-axis. Essentially, I continuously update an array of fixed length with a new random measurement (i.e. add one, delete one) each 200 milliseconds. Here is my original code:
////////////////////////////////////////////////////////////
////////////////////// Set-up /////////////////////////////
////////////////////////////////////////////////////////////
const margin = {
left: 80,
right: 80,
top: 30,
bottom: 165
};
//Actual graph smaller than svg container
var width = $('#chart').width() - margin.left - margin.right;
var height = $('#chart').height() - margin.top - margin.bottom;
const yDomain = [0, 70];
const yTickValues = [0, 10, 20, 30, 40, 50, 60, 70];
const TIME_INTERVAL = 200;
//xAxis domain-> 10 seconds
const originalTime1 = "1970-01-01T00:00:00",
originalTime2 = "1970-01-01T00:00:10";
//Get the milliseconds since UNIX epoch
var date1 = new Date(originalTime1).getTime(),
date2 = new Date(originalTime2).getTime();
//Random measurements
var measurements = [];
//Equally spaced time measurements within millisecond interval
var arraySize = Math.floor(Math.abs(date2 - date1) / TIME_INTERVAL);
//Random measurements
var trueValue = 25;
var accuracyValue = 45;
var precisionValue = 25;
////////////////////////////////////////////////////////////
///////////////////////// SVG //////////////////////////////
////////////////////////////////////////////////////////////
const svg = d3.select("#chart")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
const g = svg.append("g")
.attr("transform", "translate(" + margin.left + ", " + margin.top + ")");
////////////////////////////////////////////////////////////
///////////////////// Axes & Scales ////////////////////////
////////////////////////////////////////////////////////////
var xAxisGroup = g.append("g")
.attr("class", "x-axis");
var yAxisGroup = g.append("g")
.attr("class", "y-axis");
//Dynamic
var xScale = d3.scaleTime();
//Fixed
var yScale = d3.scaleLinear()
.domain([yDomain[0], yDomain[1]])
.range([height - margin.bottom, 0]);
const xAxis = d3.axisBottom(xScale)
.ticks(d3.timeSecond.every(1))
.tickSizeInner(15)
.tickFormat(d3.timeFormat("%M:%S"));
const yAxis = d3.axisLeft()
.scale(yScale)
.tickValues(yTickValues)
.tickFormat(d3.format(".0f"));
////////////////////////////////////////////////////////////
/////////////////////// Functions //////////////////////////
////////////////////////////////////////////////////////////
function draw() {
//Transition parameters
t = d3.transition()
.duration(TIME_INTERVAL)
.ease(d3.easeLinear);
//Update xAxis scale
xScale.domain([date1, date2])
.range([0, width]);
//Call axes
xAxisGroup.attr("transform", "translate(0," + (height - margin.bottom) + ")")
.transition(t)
.call(xAxis);
yAxisGroup.call(yAxis);
//DATA JOIN
var circles = g.selectAll("circle").data(measurements);
//EXIT
circles.exit().remove();
//UPDATE
//Update DOM elements on the screen with smooth linear transition
circles
.attr("cx", function(d) {
return xScale(d[0]);
})
.attr("cy", function(d) {
return yScale(d[1]);
});
//ENTER
circles.enter()
.append("circle")
.attr("class", "enter")
.attr("fill", "black")
.attr("r", "4")
.attr("cx", function(d) {
return xScale(d[0]);
})
.attr("cy", function(d) {
return yScale(d[1]);
});
//Update dates & random array for next iteration
date1 += TIME_INTERVAL;
date2 += TIME_INTERVAL;
measurements.push(addNewMeasurement());
measurements.shift();
};
function initializeRandomArray() {
for (i = 1; i <= arraySize; i++) {
var temp = [];
temp[0] = date1;
//Math.random not exactly 1 -> but VERY CLOSE
var precision = (Math.random() * 2 * precisionValue) - precisionValue;
temp[1] = accuracyValue + precision;
//Add new element at the end of the array
measurements.push(temp);
date1 += TIME_INTERVAL;
}
//Reset back date1
date1 = new Date(originalTime1).getTime();
};
function addNewMeasurement() {
var temp = [];
temp[0] = date2;
var precision = (Math.random() * 2 * precisionValue) - precisionValue;
temp[1] = accuracyValue + precision;
return temp;
};
////////////////////////////////////////////////////////////
///////////////////////// Main /////////////////////////////
////////////////////////////////////////////////////////////
initializeRandomArray();
draw();
d3.interval(function() {
draw();
}, TIME_INTERVAL);
.x-axis,
.y-axis {
font-size: 0.8em;
stroke-width: 0.06em;
}
#chart {
width: 600px;
height: 500px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="chart"></div>
The problem is when I apply the transition t to my data points, I get:
////////////////////////////////////////////////////////////
////////////////////// Set-up /////////////////////////////
////////////////////////////////////////////////////////////
const margin = {
left: 80,
right: 80,
top: 30,
bottom: 165
};
//Actual graph smaller than svg container
var width = $('#chart').width() - margin.left - margin.right;
var height = $('#chart').height() - margin.top - margin.bottom;
const yDomain = [0, 70];
const yTickValues = [0, 10, 20, 30, 40, 50, 60, 70];
const TIME_INTERVAL = 200;
//xAxis domain-> 10 seconds
const originalTime1 = "1970-01-01T00:00:00",
originalTime2 = "1970-01-01T00:00:10";
//Get the milliseconds since UNIX epoch
var date1 = new Date(originalTime1).getTime(),
date2 = new Date(originalTime2).getTime();
//Random measurements
var measurements = [];
//Equally spaced time measurements within millisecond interval
var arraySize = Math.floor(Math.abs(date2 - date1) / TIME_INTERVAL);
//Random measurements
var trueValue = 25;
var accuracyValue = 45;
var precisionValue = 25;
////////////////////////////////////////////////////////////
///////////////////////// SVG //////////////////////////////
////////////////////////////////////////////////////////////
const svg = d3.select("#chart")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
const g = svg.append("g")
.attr("transform", "translate(" + margin.left + ", " + margin.top + ")");
////////////////////////////////////////////////////////////
///////////////////// Axes & Scales ////////////////////////
////////////////////////////////////////////////////////////
var xAxisGroup = g.append("g")
.attr("class", "x-axis");
var yAxisGroup = g.append("g")
.attr("class", "y-axis");
//Dynamic
var xScale = d3.scaleTime();
//Fixed
var yScale = d3.scaleLinear()
.domain([yDomain[0], yDomain[1]])
.range([height - margin.bottom, 0]);
const xAxis = d3.axisBottom(xScale)
.ticks(d3.timeSecond.every(1))
.tickSizeInner(15)
.tickFormat(d3.timeFormat("%M:%S"));
const yAxis = d3.axisLeft()
.scale(yScale)
.tickValues(yTickValues)
.tickFormat(d3.format(".0f"));
////////////////////////////////////////////////////////////
/////////////////////// Functions //////////////////////////
////////////////////////////////////////////////////////////
function draw() {
//Transition parameters
t = d3.transition()
.duration(TIME_INTERVAL)
.ease(d3.easeLinear);
//Update xAxis scale
xScale.domain([date1, date2])
.range([0, width]);
//Call axes
xAxisGroup.attr("transform", "translate(0," + (height - margin.bottom) + ")")
.transition(t)
.call(xAxis);
yAxisGroup.call(yAxis);
//DATA JOIN
var circles = g.selectAll("circle").data(measurements);
//EXIT
circles.exit().remove();
//UPDATE
circles
.transition(t)
.attr("cx", function(d) {
return xScale(d[0]);
})
.attr("cy", function(d) {
return yScale(d[1]);
});
//ENTER
circles.enter()
.append("circle")
.attr("class", "enter")
.attr("fill", "black")
.attr("r", "4")
.attr("cx", function(d) {
return xScale(d[0]);
})
.attr("cy", function(d) {
return yScale(d[1]);
});
//Update dates & random array for next iteration
date1 += TIME_INTERVAL;
date2 += TIME_INTERVAL;
measurements.push(addNewMeasurement());
measurements.shift();
};
function initializeRandomArray() {
for (i = 1; i <= arraySize; i++) {
var temp = [];
temp[0] = date1;
//Math.random not exactly 1 -> but VERY CLOSE
var precision = (Math.random() * 2 * precisionValue) - precisionValue;
temp[1] = accuracyValue + precision;
//Add new element at the end of the array
measurements.push(temp);
date1 += TIME_INTERVAL;
}
//Reset back date1
date1 = new Date(originalTime1).getTime();
};
function addNewMeasurement() {
var temp = [];
temp[0] = date2;
var precision = (Math.random() * 2 * precisionValue) - precisionValue;
temp[1] = accuracyValue + precision;
return temp;
};
////////////////////////////////////////////////////////////
///////////////////////// Main /////////////////////////////
////////////////////////////////////////////////////////////
initializeRandomArray();
draw();
d3.interval(function() {
draw();
}, TIME_INTERVAL);
.x-axis,
.y-axis {
font-size: 0.8em;
stroke-width: 0.06em;
}
#chart {
width: 600px;
height: 500px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="chart"></div>
Is there a simple way of making the data points move horizontally smoothly? Thank you very much for your input!

D3 js - line not showing in real time line graph

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

d3.js: how to conditionally render a curve?

I've got a function that draws a curve through dots I give as argument, as follows:
var data = [
// stage 1-9, intensity %, draw disk
{x:1, y: 0, point:true},
{x:4, y: 30, point:true},
{x:5, y: 70, point:true},
{x:6, y:100, point:true},
{x:7, y: 90, point:true},
{x:8, y: 40, point:true},
{x:9, y: 10, point:false}
];
I'd like to handle the point members that tells whether or not to draw an additional spot.
How to do that?
The function that draws the curve as it is per today:
function curveChart(data) {
for (i in data) {
data[i].y = 5.5*data[i].y/100; // normalize
data[i].id = i;
}
var margin = {top: 10, right: 190, bottom: 275, left: 35},
width = 915 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var x = d3.scale.linear() //.time.scale()
.domain([1, 9]) // 9 stages
.range([0, width]);
var y = d3.scale.linear()
.domain([0, 6])
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
var line = d3.svg.line()
.interpolate("monotone")
.x(function(d) { return x(d.x); })
.y(function(d) { return y(d.y); });
var svg = d3.select(".curveChart").append("svg")
.datum(data)
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
svg.append("path")
.attr("class", "line")
.attr("d", line);
var n = 1;
svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("class", "dot")
.attr("cx", line.x())
.attr("cy", line.y())
.attr("r", 2)
.attr("bubbleid", function(d){return d.id; })
.transition(1000)
.duration(800)
.attr("r", 10);
svg.selectAll("circle").on("click", function(){
d3.selectAll(".active").classed("active", false);
d3.select(this).classed("active", true);
var id = $(this).attr('bubbleid');
console.log("clicked on "+$(this).attr('bubbleid'));
$(".bubble").removeClass("show");
$("#bubble"+id).addClass("show");
d3.select(this)
.transition()
.duration(400)
.attr('r', 25)
.transition()
.duration(400)
.attr('r', 10)
;
});
}
The fiddle : http://jsfiddle.net/stephanedeluca/R44cB/
The easiest way to do this is to filter the data before passing it to .data(), retaining only the elements where point is true:
svg.selectAll("circle")
.data(data.filter(function(d) { return d.point; }))
Complete demo here.

Categories