d3.js scatter chart - clipping the sparklines - javascript

I created a dynamic d3.js chart that plots time series.
Each second I add a new sample and remove an old sample.
It gives the impression that the sparklines are moving from the right to the left, which is good.
However, I am not very happy with the way the new and old segment of a path are added and removed.
See this picture:
On this image, you can see a little gap between the y-axis on the right and the sparklines. As the sparklines move to the left, this gap gets bigger and once it is large enough the new segments of the sparklines are then added. That doesn't look very smooth.
I would like the new segments to be drawn as the sparklines move to the left (like you would do when drawing it by hand).
I am using a clip-path to hide the part of paths that I don't want (outside of the plot), but that doesn't seem to give me the correct behavior.
Definition of the clip-path:
this.container.append('defs')
.append('clipPath')
.attr('id', 'chart-content')
.append('rect')
.attr('height', this.height)
.attr('width', this.width);
Use of the clip-path:
group.path = this.paths
.append('g')
.attr('clip-path', 'url(#chart-content)')
.append('path')
.data([group.data])
On another note, the clip-path seem to work when I am panning and zooming.. Which confuse me even more! I hope someone can help me out with that!

I found the answer to my question..
Here is a snippet of a dynamic line chart!
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.line {
fill: none;
stroke: #000;
stroke-width: 1.5px;
}
#chart{
width: 100%;
height: 500px;
}
</style>
<div id='chart'></div>
<script src="https://d3js.org/d3.v4.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.5.1/moment.min.js"></script>
<script>
var margin = { top: 20, right: 20, bottom: 20, left: 20};
var padding = { top: 20, right: 40, bottom: 20, left: 40};
let width;
let height;
let svg;
let container;
let data;
let xScale;
let yScale;
let now;
let path;
let gX;
let xAxis;
let lineGenerator;
const duration = 500;
render = () => {
this.hostElement = d3.select('#chart');
this.width = this.hostElement.node().getBoundingClientRect().width - this.margin.left - this.margin.right - this.padding.left - this.padding.right;
this.height = this.hostElement.node().getBoundingClientRect().height - this.margin.top - this.margin.bottom - this.padding.bottom - this.padding.top;
this.createSvg();
this.createAxis();
this.createLine();
this.defineBounds();
this.tick();
}
createSvg = () => {
this.svg = this.hostElement
.append('svg')
.attr('transform', 'translate(' + this.margin.left + ',' + this.margin.top + ')')
.attr('width', this.width + this.padding.right + this.padding.left)
.attr('height', this.height + this.padding.top + this.padding.bottom)
this.container = this.svg
.append('g')
.attr('class', 'chart')
.attr('transform', 'translate(' + this.padding.left + ',' + this.padding.top + ')')
.attr('width', this.width)
.attr('height', this.height);
}
createAxis = () => {
// maxY
const maxY = d3.max(this.data, d => d.value);
// minX & maxX
const minX = d3.min(this.data, d => d.date);
const maxX = d3.max(this.data, d => d.date);
this.now = maxX;
const maxXToDisplay = moment(maxX).subtract(1, 's').toDate();
const minXToDisplay = moment(minX).add(2, 's').toDate();
// Update scales
this.xScale = d3.scaleTime()
.domain([minXToDisplay, maxXToDisplay])
.range([0, this.width]);
this.yScale = d3.scaleLinear()
.domain([0, maxY])
.range([this.height, 0]);
// Update axis
this.xAxis = d3.axisBottom(this.xScale);
const yAxis = d3.axisLeft(this.yScale);
// Draws the axis
this.gX = this.container.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0,' + this.height + ')')
.call(this.xAxis);
const gY = this.container.append('g')
.attr('class', 'y axis')
.call(yAxis);
}
createLine = () => {
this.lineGenerator = d3.line()
.x(d => this.xScale(d.date))
.y(d => this.yScale(d.value))
.curve(d3.curveMonotoneX);
this.path = this.container
.append('g')
.attr('class', 'path-container')
.attr('clip-path', 'url(#chart-content)')
.append('path')
.datum(this.data)
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-width", 1.5)
.attr('d', this.lineGenerator);
}
tick = () => {
this.now = moment(this.now).add(1, 's').toDate();
// Add new values
this.data.push({
value: Math.floor(10 + Math.random() * 15),
date: this.now
});
// Remove old values
this.data.shift();
this.path.attr('d', this.lineGenerator);
const numberSamplesToDisplay = this.data.length - 2;
const minX = moment(this.now).subtract(numberSamplesToDisplay, 's').toDate();
const maxX = this.now;
const maxXToDisplay = moment(maxX).subtract(1, 's').toDate();
const minXToDisplay = moment(minX).add(1, 's').toDate();
// Shift domain
this.xScale.domain([minXToDisplay, maxXToDisplay]);
// Slide x-axis left
const xTransition = this.gX.transition()
.duration(1000)
.ease(d3.easeLinear)
.call(this.xAxis);
// Slide paths left
this.path.attr('transform', null)
.transition()
.duration(1000)
.ease(d3.easeLinear)
.attr('transform', 'translate(' + this.xScale(minX) + ', 0)')
.on('end', this.tick);
}
defineBounds = () => {
this.container.append('defs')
.append('clipPath')
.attr('id', 'chart-content')
.append('rect')
.attr('height', this.height)
.attr('width', this.width);
}
generateData = () => {
const dataset = [
{
value: Math.floor(10 + Math.random() * 15),
date: moment().subtract(60, 'seconds').toDate()
}
];
for (let i = 0; i < 59; i ++) {
dataset.push({
value: Math.floor(10 + Math.random() * 15),
date: moment(dataset[dataset.length - 1].date).add(1, 'seconds').toDate()
})
}
return dataset;
}
this.data = generateData();
render();
</script>

Related

How to add padding around to grid of squares?

I'm trying to create a grid of squares using D3. My code works, but I'd like to add some padding around each of the squares so that the grid does not look so "tight".
Here is my code:
class Matrix {
constructor(parentElement, data1, data2, data3) {
this.parentElement = parentElement;
this.data1 = data1;
this.data2 = data2;
this.data3 = data3;
this.cellHeight = 40;
this.cellWidth = 40;
this.initializeVisual()
}
initializeVisual() {
let vis = this;
vis.margin = {top: 20, right: 20, bottom: 20, left: 40};
vis.width = document.getElementById(vis.parentElement).getBoundingClientRect().width - vis.margin.left - vis.margin.right;
vis.height = 800 - vis.margin.top - vis.margin.bottom;
// init drawing area
vis.svg = d3.select('#' + vis.parentElement).append('svg')
.attr('width', vis.width + vis.margin.left + vis.margin.right)
.attr('height', vis.height + vis.margin.top + vis.margin.bottom)
.append('g')
.attr('transform', `translate (${vis.margin.left}, ${vis.margin.top})`);
// Draws the initial grid
let squaresPerRow = 16
let scale = d3.scaleLinear()
.domain([0, squaresPerRow -1])
.range([0, this.cellWidth * squaresPerRow])
vis.squares = vis.svg.selectAll('.squares')
.data(d3.range(256))
.enter()
.append('rect')
.attr('x', (d, i) => {
let n = i % squaresPerRow
return scale(n)
})
.attr('y', (d, i) => {
let n = Math.floor(i / 16)
return scale(n)
})
.attr('width', this.cellWidth)
.attr('height', this.cellHeight)
.attr('fill', 'lightgrey');
}
The resulting grid looks like this:
How can I add the padding?
Thanks!
Personally I'd change the linear scale for a point scale, but here's a solution using most of your code as it is.
First, get rid of that minus one in the domain, that's a cumbersome way for creating the padding:
.domain([0, squaresPerRow -1])
Then, after you set the padding (here named padding), you can translate the x and y positions by half of it, and subtracting the rectangles' width and height by that padding.
Here's a demo, change the variable padding for different spaces:
const svg = d3.select("div")
.append('svg')
.attr('width', 400)
.attr('height', 400);
let squaresPerRow = 8,
cellWidth = 40,
cellHeight = 40,
padding = 14;
let scale = d3.scaleLinear()
.domain([0, squaresPerRow])
.range([0, cellWidth * squaresPerRow])
const squares = svg.selectAll('.squares')
.data(d3.range(squaresPerRow ** 2))
.enter()
.append('rect')
.attr('x', (d, i) => {
let n = i % squaresPerRow
return scale(n) + padding / 2
})
.attr('y', (d, i) => {
let n = Math.floor(i / squaresPerRow)
return scale(n) + padding / 2
})
.attr('width', cellWidth - padding)
.attr('height', cellHeight - padding)
.attr('fill', 'lightgrey');
<script src="https://d3js.org/d3.v7.min.js"></script>
<div></div>

How to zoom my stream-graph with using D3V6?

Update:
The zooming stream graph question is resolved. Thank you, Andrew! But the zooming of the streamgraph doesn't align with the zooming of the X and Y axis.
Thisis the picture showing how it looks like now2
the original post is here:
I am new to StackOverflow and the javascript community. I am trying to zoom a streamgraph I did use javascript and D3 and I followed this tutorial: https://www.d3-graph-gallery.com/graph/interactivity_zoom.html#axisZoom.
My code can be viewed here: https://github.com/Feisnowflakes/zoomtest222/tree/main/streamgraph-test
However, currently, I can zoom X aXis and Y aXis, but not my stream graph is zoomable. I didn't see any errors in my console, so I am stuck now. Can anyone like to look at my codes and help me figure out why my streamgraph cannot be zoomed?
Thank you so much!
(function () {
// first, load the dataset from a CSV file
d3.csv("https://raw.githubusercontent.com/Feisnowflakes/zoomtest222/main/streamgraph-test/Los_Angeles_International_Airport_-_Passenger_Traffic_By_Terminal.csv")
.then(data => {
// log csv in browser console
console.log(data);
var advanceVisData = {};
var airport = new Set();
data.forEach(d => {
airport.add(d['Terminal']);
var period = new Date(d['ReportPeriod']);
if (period in advanceVisData) {
if (d['Terminal'] in advanceVisData[period]) {
advanceVisData[period][d['Terminal']] += Number(d['Passenger_Count']);
}
else {
advanceVisData[period][d['Terminal']] = Number(d['Passenger_Count']);
}
}
else {
advanceVisData[period] = {};
advanceVisData[period][d['Terminal']] = Number(d['Passenger_Count']);
}
});
console.log(airport);
console.log(advanceVisData);
// reformat the advanceVisData for d3.stack()
var formattedData = [];
Object.keys(advanceVisData).forEach(d => {
var item = {};
item['year'] = d;
airport.forEach(terminal => {
if (terminal in advanceVisData[d]) {
item[terminal] = advanceVisData[d][terminal];
} else {
item[terminal] = 0;
}
});
formattedData.push(item);
});
console.log(formattedData);
/*********************************
* Visualization codes start here
* ********************************/
var width = 1200;
var height = 400;
var margin = { left: 60, right: 20, top: 20, bottom: 60 };
//set the dimensions and margins of the graph
// append the svg object to the body of the page
var svg = d3.select('#container')
.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 + ")");
// List of groups = header of the csv files
var keys = Array.from(airport);
//stack the data?
var stackedData = d3.stack()
//.offset(d3.stackOffsetSilhouette)
.keys(keys)
(formattedData);
console.log(stackedData);
var max_val = 0;
var min_val = 0;
stackedData.forEach(terminal => {
terminal.forEach(year => {
if (year[0] < min_val) min_val = year[0];
if (year[1] < min_val) min_val = year[1];
if (year[0] > max_val) max_val = year[0];
if (year[1] > max_val) max_val = year[1];
})
});
//console.log(max_val, min_val);
// Add X axis
var x = d3.scaleTime()
.domain(d3.extent(formattedData, function (d) {
return new Date(d.year);
}))
.range([0, width]);
var xAxis = svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x).ticks(20));
// Add Y axis
var y = d3.scaleLinear()
.domain([min_val, max_val])
.range([height, 0]);
var yAxis = svg.append("g")
.call(d3.axisLeft(y));
// color palette
var color = d3.scaleOrdinal()
.domain(keys)
.range(['#e41a1c', '#377eb8', '#4daf4a', '#984ea3', '#ff7f00', '#f781bf', "#87sbf", "#ff981bf","#d6a3b6", '#b3afb0', '#ddd8c2']);
// create a tooltip
var Tooltip = svg
.append("text")
.attr("x", 0)
.attr("y", 0)
.style("opacity", 0)
.style("font-size", 17)
// Show the areas
var stream = svg.append("g")
stream
.selectAll(".myStreamArea")
.data(stackedData)
.enter()
.append("path")
.attr("class", "myStreamArea")
.style("fill", function (d) {
return color(d.key);
})
.style("opacity", 1)
.attr("d", d3.area()
.x(function (d) {
return x(new Date(d.data.year));
})
.y0(function (d) {
return y(d[0]);
})
.y1(function (d) {
return y(d[1]);
})
);
// Set the zoom and Pan features: how much you can zoom, on which part, and what to do when there is a zoom
var zoom = d3.zoom()
.scaleExtent([.5, 20]) // This control how much you can unzoom (x0.5) and zoom (x20)
.extent([[0, 0], [width, height]])
.on("zoom", updateChart);
// This add an invisible rect on top of the chart area. This rect can recover pointer events: necessary to understand when the user zoom
svg.append("rect")
.attr("width", width)
.attr("height", height)
.style("fill", "none")
.style("pointer-events", "all")
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
.call(zoom);
// now the user can zoom and it will trigger the function called updateChart
// A function that updates the chart when the user zoom and thus new boundaries are available
function updateChart() {
// recover the new scale
var transform = d3.zoomTransform(this);
var newX = transform.rescaleX(x);
var newY = transform.rescaleY(y);
// var newX = d3.event.transform.rescaleX(x);
// var newY = d3.event.transform.rescaleY(y);
// update axes with these new boundaries
xAxis.call(d3.axisBottom(newX))
yAxis.call(d3.axisLeft(newY))
stream
.selectAll(".myStreamArea")
.attr("d", d3.area()
.x(function (d) {
return newX(new Date(d.data.year));
})
.y0(function (d) {
return newY(d[0]);
})
.y1(function (d) {
return newY(d[1]);
}));
}
})
})();
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Zoomable streamgraph</title>
<script src="https://d3js.org/d3.v6.min.js"></script>
<style>
#tooltip {
min-width: 100px;
min-height: 50px;
background-color: white;
}
</style>
</head>
<body>
<div id="container"> <div id="tooltip"></div></div>
<script src="main.js"></script>
</body>
</html>

D3.js v5 - appending lines about a circle from length of array

I want to make a visual that shows ordinal data (ratings). There are 12 rating dimensions, and each rating will have its own dedicated line appended to a circle. The polar orientation of the line designates a category (i.e. lines pointing to 1 o'clock = category 1, 2 o'clock = category 2, and so forth). The length of the line indicates the ratings value (short = bad, long = good). The result should resemble a snow flake or a sun burst.
The name is stored in a string. The ratings for each company are stored in an array. Here are two slices of my data variable:
{'fmc':'fmc1', 'ratings':[10,10,10,10,10,10,10,10,10,10,10,10]},
{'fmc':'fmc2', 'ratings':[8,10,10,5,10,10,10,10,10,7,10,5]},
I have the grid-system placement for the companies functioning, but there seems to be an issue with the way I'm aligning the lines about the circle. Relevant code:
var rotationDegree = d3.scalePoint().domain([0,12]).range([0, 2*Math.PI - Math.PI/6]);
fmcG.append('line')
.data([10,10,10,10,10,10,10,10,10,10,10,10])
.attr("x1", r)
.attr("y1", r)
.attr("x2", function(d,i) { return length(10) * Math.cos(rotationDegree(i) - Math.PI/2) + (width/2); })
.attr("y2", function(d,i) { return length(10) * Math.sin(rotationDegree(i) - Math.PI/2) + (height/2); })
.style("stroke", function(d) { return "#003366" });
It would seem that I have the trig mapped out correctly, but in implementation I am proven wrong: the lines are not being appended about the circle like a snow flake / sun burst / clock.
Snippet:
var margins = {top:20, bottom:300, left:30, right:100};
var height = 600;
var width = 900;
var totalWidth = width+margins.left+margins.right;
var totalHeight = height+margins.top+margins.bottom;
var svg = d3.select('body')
.append('svg')
.attr('width', totalWidth)
.attr('height', totalHeight);
var graphGroup = svg.append('g')
.attr('transform', "translate("+margins.left+","+margins.top+")");
var data = [
//{'fmc':'fmc1', 'ratings':[{'r1':10,'r2':10,'r3':10,'r4':10,'r5':10}]}
{'fmc':'fmc1', 'ratings':[10,10,10,10,10,10,10,10,10,10,10,10]},
{'fmc':'fmc2', 'ratings':[8,10,10,5,10,10,10,10,10,7,10,5]},
{'fmc':'fmc3', 'ratings':[10,10,10,10,10,10,10,10,10,10,10,10]},
];
var r = 30;
var length = d3.scaleLinear().domain([0, 10]).range([0, 50]);
var rotationDegree = d3.scalePoint().domain([0,12]).range([0, 2*Math.PI - Math.PI/6]);
var columns = 5;
var spacing = 220;
var vSpacing = 250;
var fmcG = graphGroup.selectAll('.fmc')
.data(data)
.enter()
.append('g')
.attr('class', 'fmc')
.attr('id', (d,i) => 'fmc' + i)
.attr('transform', (d,k) => {
var horSpace = (k % columns) * spacing;
var vertSpace = ~~((k / columns)) * vSpacing;
return "translate("+horSpace+","+vertSpace+")";
});
fmcG.append('circle')
.attr('cx',100)
.attr('cy',100)
.attr('r', r)
.style('fill','none')
.style('stroke','#003366');
fmcG.append('text')
.attr('x',100)
.attr('y',105)
.style('text-anchor','middle')
.text(function(d) {return d.fmc});
fmcG.append('line')
//.data(function(d) {return d.ratings}) why doesnt it workk??????
.data([10,10,10,10,10,10,10,10,10,10,10,10])
.attr("x1", r)
.attr("y1", r)
.attr("x2", function(d,i) { return length(10) * Math.cos(rotationDegree(i) - Math.PI/2) + (width/2); })
.attr("y2", function(d,i) { return length(10) * Math.sin(rotationDegree(i) - Math.PI/2) + (height/2); })
.style("stroke", function(d) { return "#003366" });
<script src="https://d3js.org/d3.v5.min.js"></script>
Question
How can I take an 12-item array and append lines about the circle in 30 degree increments (360 divided by 12) while using the value of each item in the array to determine the line's length?
The main issue is that, right now, you're appending a single line. For appending as many lines as data points you have to set up a proper enter selection:
fmcG.selectAll(null)
.data(function(d) {
return d.ratings
})
.enter()
.append('line')
//etc...
And that, by the way, is the reason your data is not working (as you ask in your comment "why doesnt it workk??????")
Other issues:
A point scale needs to have a discrete domain, for instance d3.range(12)
For whatever reason you're moving the circles 100px right and down. I'm moving the lines by the same amount.
Here is the snippet with those changes:
var margins = {
top: 20,
bottom: 300,
left: 30,
right: 100
};
var height = 600;
var width = 900;
var totalWidth = width + margins.left + margins.right;
var totalHeight = height + margins.top + margins.bottom;
var svg = d3.select('body')
.append('svg')
.attr('width', totalWidth)
.attr('height', totalHeight);
var graphGroup = svg.append('g')
.attr('transform', "translate(" + margins.left + "," + margins.top + ")");
var data = [
//{'fmc':'fmc1', 'ratings':[{'r1':10,'r2':10,'r3':10,'r4':10,'r5':10}]}
{
'fmc': 'fmc1',
'ratings': [10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10]
},
{
'fmc': 'fmc2',
'ratings': [8, 10, 10, 5, 10, 10, 10, 10, 10, 7, 10, 5]
},
{
'fmc': 'fmc3',
'ratings': [10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10]
},
];
var r = 30;
var length = d3.scaleLinear().domain([0, 10]).range([0, 50]);
var rotationDegree = d3.scalePoint().domain(d3.range(12)).range([0, 2 * Math.PI]);
var columns = 5;
var spacing = 220;
var vSpacing = 250;
var fmcG = graphGroup.selectAll('.fmc')
.data(data)
.enter()
.append('g')
.attr('class', 'fmc')
.attr('id', (d, i) => 'fmc' + i)
.attr('transform', (d, k) => {
var horSpace = (k % columns) * spacing;
var vertSpace = ~~((k / columns)) * vSpacing;
return "translate(" + horSpace + "," + vertSpace + ")";
});
fmcG.append('circle')
.attr('cx', 100)
.attr('cy', 100)
.attr('r', r)
.style('fill', 'none')
.style('stroke', '#003366');
fmcG.append('text')
.attr('x', 100)
.attr('y', 105)
.style('text-anchor', 'middle')
.text(function(d) {
return d.fmc
});
fmcG.selectAll(null)
.data(function(d) {
return d.ratings
})
.enter()
.append('line')
.attr("x1", 100)
.attr("y1", 100)
.attr("x2", function(d, i) {
return 100 + length(d) * Math.cos(rotationDegree(i));
})
.attr("y2", function(d, i) {
return 100 + length(d) * Math.sin(rotationDegree(i));
})
.style("stroke", function(d) {
return "#003366"
});
<script src="https://d3js.org/d3.v5.min.js"></script>

D3 line chart and adding data points real time

I am beginner to d3 and trying to create real time chart which adds in new values on the go. I want the chart to shift the old points to the left as new points are added. Below is my code but for some reason, the browser freezes with the code (I have commented out the .on() line in the end that causes the freeze).
What am I doing wrong?
<!DOCTYPE html>
<head></head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js">
</script>
<script>
<!DOCTYPE html>
<head></head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js">
</script>
<script>
var t = -1;
var n = 40;
var duration = 750;
var data = [];
console.log('hello');
function next() {
return {
time: ++t,
value: Math.random() * 10
}
}
var margin = {
top: 6,
right: 0,
bottom: 20,
left: 40
},
width = 560 - margin.right,
height = 120 - margin.top - margin.bottom;
var xScale = d3.scaleTime()
.domain([t - n + 1, t])
.range([0, width]);
var yScale = d3.scaleLinear()
.domain([0, 10])
.range([height, 0]);
var line = d3.line()
.x((d) => xScale(d.time))
.y((d) => yScale(d.value));
var svg = d3.select('body').append('p').append('svg');
var chartArea = svg.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`);
chartArea.append('defs').append('clipPath')
.attr('id', 'clip2')
.append('rect')
.attr('x', 0)
.attr('y', 0)
.attr('width', width)
.attr('height', height);
chartArea.append('rect')
.attr('class', 'bg')
.attr('x', 0)
.attr('y', 0)
.attr('width', this.chartWidth)
.attr('height', this.chartHeight);
var xAxis = d3.axisBottom(xScale);
var xAxisG = chartArea.append('g')
.attr('class', 'x-axis')
.attr('transform', `translate(0, ${height})`);
xAxisG.call(xAxis);
d3.selectAll('x-axis path').style('stroke', 'red')
.style('stroke-width', 2);
var yAxis = d3.axisLeft(yScale);
var yAxisG = chartArea.append('g').attr('class', 'y-axis');
yAxisG.call(yAxis);
var grids = chartArea.append('g')
.attr('class', 'grid')
.call(d3.axisLeft(yScale).tickSize(-(width)).tickFormat((domain, number) => {
return ""
}));
var pathsG = chartArea.append('g')
.attr('id', 'paths')
.attr('class', 'paths')
.attr('clip-path', 'url(#clip2)');
tick();
function tick() {
console.log('working');
var newValue = {
time: ++t,
value: Math.random() * 10
};
data.push(newValue);
xScale.domain([newValue.time - n + 2, newValue.time]);
xAxisG.transition().duration(500).ease(d3.easeLinear).call(xAxis);
console.log('is it?');
var minerG = pathsG.selectAll('.minerLine').data([data]);
var minerGEnter = minerG.enter()
.append('g')
.attr('class', 'minerLine')
.merge(minerG);
var minerSVG = minerGEnter.selectAll('path').data((d) => [d]);
var minerSVGEnter = minerSVG.enter()
.append('path')
.attr('class', 'line')
.merge(minerSVG)
.transition()
.duration(500)
.ease(d3.easeLinear, 2)
.attr('d', line(data))
.on('end', () => {
requestAnimationFrame(tick)
})
}
</script>
</body>
</html>
The problem is caused by the fact that tick is called immediately and synchronously at the end of the transition. The CPU process executing the JavaScript remains busy updating data and chart, and is not available to do anything else on this tab.
One way to fix this is to use Window.requestAnimationFrame().
.on('end', () => {
requestAnimationFrame(tick)
})
The updated snippet below shows this solution in action.
It does not fix other issues not mentioned in the question, like the fact that no data is shown in the chart.
<!DOCTYPE html>
<head></head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js">
</script>
<script>
var t = -1;
var n = 40;
var duration = 750;
var data = [];
console.log('hello');
function next() {
return {
time: ++t,
value: Math.random() * 10
}
}
var margin = {
top: 6,
right: 0,
bottom: 20,
left: 40
},
width = 560 - margin.right,
height = 120 - margin.top - margin.bottom;
var xScale = d3.scaleTime()
.domain([t - n + 1, t])
.range([0, width]);
var yScale = d3.scaleLinear()
.domain([0, 10])
.range([height, 0]);
var line = d3.line()
.x((d) => xScale(d.time))
.y((d) => yScale(d.value));
var svg = d3.select('body').append('p').append('svg');
var chartArea = svg.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`);
chartArea.append('defs').append('clipPath')
.attr('id', 'clip2')
.append('rect')
.attr('x', 0)
.attr('y', 0)
.attr('width', width)
.attr('height', height);
chartArea.append('rect')
.attr('class', 'bg')
.attr('x', 0)
.attr('y', 0)
.attr('width', this.chartWidth)
.attr('height', this.chartHeight);
var xAxis = d3.axisBottom(xScale);
var xAxisG = chartArea.append('g')
.attr('class', 'x-axis')
.attr('transform', `translate(0, ${height})`);
xAxisG.call(xAxis);
d3.selectAll('x-axis path').style('stroke', 'red')
.style('stroke-width', 2);
var yAxis = d3.axisLeft(yScale);
var yAxisG = chartArea.append('g').attr('class', 'y-axis');
yAxisG.call(yAxis);
var grids = chartArea.append('g')
.attr('class', 'grid')
.call(d3.axisLeft(yScale).tickSize(-(width)).tickFormat((domain, number) => {
return ""
}));
var pathsG = chartArea.append('g')
.attr('id', 'paths')
.attr('class', 'paths')
.attr('clip-path', 'url(#clip2)');
tick();
function tick() {
console.log('working');
var newValue = {
time: ++t,
value: Math.random() * 10
};
data.push(newValue);
xScale.domain([newValue.time - n + 2]);
xAxisG.transition().duration(500).ease().call(xAxis);
console.log('is it?');
var minerG = pathsG.selectAll('.minerLine').data([data]);
var minerGEnter = minerG.enter()
.append('g')
.attr('class', 'minerLine')
.merge(minerG);
var minerSVG = minerGEnter.selectAll('path').data((d) => [d]);
var minerSVGEnter = minerSVG.enter()
.append('path')
.attr('class', 'line')
.merge(minerSVG)
.transition()
.duration(500)
.ease(d3.easeLinear, 2)
.attr('d', line(data))
.on('end', () => {
requestAnimationFrame(tick)
})
}
</script>
</body>
</html>

How to move d3 ticks on y-axis

I have a bar chart see plunker the problem is that I would like to move the y-axis ticks to be at the middle left side of the rects but they appear on the top and end. and I cannot seem to move them without destroying the chart.
my code
var info = [{
name: "Walnuts",
value: 546546
}, {
name: "Almonds",
value: 456455
}
];
/* Set chart dimensions */
var width = 960,
height = 500,
margin = {
top: 10,
right: 10,
bottom: 20,
left: 60
};
//subtract margins
width = width - margin.left - margin.right;
height = height - margin.top - margin.bottom;
//sort data from highest to lowest
info = info.sort(function(a, b) {
return b.value - a.value;
});
//Sets the y scale from 0 to the maximum data element
var max_n = 0;
var category = []
for (var d in info) {
max_n = Math.max(info[d].value, max_n);
category.push(info[d].name)
}
var dx = width / max_n;
var dy = height / info.length;
var y = d3.scale.ordinal()
.domain(category)
.range([0, height]);
var yAxis = d3.svg.axis()
.scale(y)
.orient('left')
var svg = d3.select("#chart")
.append("svg")
.attr("width", "100%")
.attr("height", "100%")
.attr('preserveAspectRatio', 'xMidYMin')
.attr("viewBox", '0 0 ' + parseInt(width + margin.left + margin.right) + ' ' + parseInt(height + margin.top + margin.bottom))
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
svg.selectAll(".bar")
.data(info)
.enter()
.append("rect")
.attr("class", function(d, i) {
return "bar" + d.name;
})
.attr("x", function(d, i) {
return 0;
})
.attr("y", function(d, i) {
return dy * i;
})
.attr("width", function(d, i) {
return dx * d.value
})
.attr("height", dy)
.attr("fill", function(d, i) {
if (d.name == 'Walnuts') {
return 'red'
} else {
return 'green'
}
});
var y_xis = svg.append('g')
.attr('id', 'yaxis')
.call(yAxis);
You are using range in y axis like this:
var y = d3.scale.ordinal()
.domain(category)
.range([0, height]);
You should be using 'rangeRoundBands' since the y scale is ordinal
var y = d3.scale.ordinal()
.domain(category)
.rangeRoundBands([0, height], .1);
working code here
For d3 versions like v4/v5.
Defining height as the graph/plot height, and max as the maximum value of y.
import { parseSvg } from 'd3-interpolate/src/transform/parse'
const yScale = d3
.scaleLinear()
.domain([0, max])
.rangeRound([height, 0])
const yAxis = d3.axisLeft(yScale)
svg
.append('g')
.call(yAxis)
.selectAll('.tick')
.each(function(data) {
const tick = d3.select(this)
const { translateX, translateY } = parseSvg(tick.attr('transform'))
tick.attr(
'transform',
translate(translateX, translateY + height / (2 * max))
)
})
Recently I needed something very very similar and I solved this with a call with selecting all text elements in the selection and moving their dy upwards. I will give an example with OP's code:
var y_xis = svg.append('g')
.attr('id','yaxis')
.call(yAxis)
.call(selection => selection
.selectAll('text')
.attr('dy', '-110') // this moves the text labels upwards
.attr('x', '110')); // this does the same job but horizontally

Categories

Resources