I am working with D3 v4 and JS. I have a scatter plot with a predefined set of data loaded along with axes with the ability to pan and zoom. I need to be able to then dynamically add points and eventually output them in data space not pixel space. I am using the "rescaleX" and "rescaleY" methods of the zoom object. They work fine for rescaling the axes but, when I try to add new points, the location of the plotted point does correspond to the mouse location. Here is a simplified version of the code:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
</head>
<body>
<script>
var data = [{x:17,y:3},
{x:20,y:16},
{x:2,y:13},
{x:19,y:10},
{x:13,y:15},
{x:2,y:2},
{x:5,y:8},
{x:11,y:19},
{x:20,y:12},
{x:10,y:20}];
var width = 600;
var height = 600;
var padding = 50;
var newXscale, newYscale;
var dataScale = d3.scaleLinear()
.domain([0,21])
.range([0, width]);
var svg = d3.select('body').append('svg')
.attr('width', width+2*padding)
.attr('height', height+2*padding)
.on('click', clicked);
var xAxis = d3.axisTop()
.scale(dataScale);
var gX = svg.append('g')
.attr('transform','translate(50,50)')
.call(xAxis);
var yAxis = d3.axisLeft()
.scale(dataScale);
var gY = svg.append('g')
.attr('transform','translate(50,50)')
.call(yAxis);
var canvas = svg.append('g')
var points = canvas.append('g');
points.selectAll('circle').data(data)
.enter().append('circle')
.attr('cx', function(d) {return dataScale(d.x)+padding})
.attr('cy', function(d) {return dataScale(d.y)+padding})
.attr('r', 5);
var zoom
var zoomOn = false;
window.addEventListener('keydown', function (event) {
if (event.key=='z') {
if (zoomOn) {
d3.select('#zoomBox').remove();
zoomOn = false;
} else {
zoom = d3.zoom()
.scaleExtent([0.1, 10])
.on('zoom', zoomed);
svg.append("rect")
.attr('cursor','move')
.attr("width", width+padding*2)
.attr("height", height+padding*2)
.attr('id','zoomBox')
.style("fill", "none")
.style("pointer-events", "all")
.call(zoom);
zoomOn = true;
}
}
});
function zoomed() {
canvas.attr("transform", d3.event.transform)
newXscale = d3.event.transform.rescaleX(dataScale);
newYscale = d3.event.transform.rescaleY(dataScale);
gX.call(xAxis.scale(newXscale));
gY.call(yAxis.scale(newYscale));
}
function clicked() {
var coords = d3.mouse(this);
points.append('circle')
.attr('cx',coords[0])
.attr('cy',coords[1])
.attr('r',5);
var x = newXscale.invert(coords[0]-padding);
var y = newYscale.invert(coords[1]-padding);
console.log(x+' '+y);
}
</script>
</body>
</html>
Create a variable to store the zoom level:
newZscale = d3.event.transform.k;
And, in your clicked function, use the dateScale to plot the new circles, dividing the padding by the zoom level:
function clicked() {
var coords = d3.mouse(this);
if (newXscale && newYscale) {
var x = newXscale.invert(coords[0] - padding);
var y = newYscale.invert(coords[1] - padding);
};
console.log(newZscale);
points.append('circle')
.attr('cx', (!x) ? coords[0] : dataScale(x) + (padding / newZscale))
.attr('cy', (!y) ? coords[1] : dataScale(y) + (padding / newZscale))
.attr('r', 5);
console.log(x + ' ' + y);
}
Here is the demo:
var data = [{
x: 17,
y: 3
}, {
x: 20,
y: 16
}, {
x: 2,
y: 13
}, {
x: 19,
y: 10
}, {
x: 13,
y: 15
}, {
x: 2,
y: 2
}, {
x: 5,
y: 8
}, {
x: 11,
y: 19
}, {
x: 20,
y: 12
}, {
x: 10,
y: 20
}];
var width = 600;
var height = 600;
var padding = 50;
var newXscale, newYscale, newZscale;
var dataScale = d3.scaleLinear()
.domain([0, 21])
.range([0, width]);
var svg = d3.select('body').append('svg')
.attr('width', width + 2 * padding)
.attr('height', height + 2 * padding)
.on('click', clicked);
var xAxis = d3.axisTop()
.scale(dataScale);
var gX = svg.append('g')
.attr('transform', 'translate(50,50)')
.call(xAxis);
var yAxis = d3.axisLeft()
.scale(dataScale);
var gY = svg.append('g')
.attr('transform', 'translate(50,50)')
.call(yAxis);
var canvas = svg.append('g')
var points = canvas.append('g');
points.selectAll('circle').data(data)
.enter().append('circle')
.attr('cx', function(d) {
return dataScale(d.x) + padding
})
.attr('cy', function(d) {
return dataScale(d.y) + padding
})
.attr('r', 5);
var zoom
var zoomOn = false;
window.addEventListener('keydown', function(event) {
if (event.key == 'z') {
if (zoomOn) {
d3.select('#zoomBox').remove();
zoomOn = false;
} else {
zoom = d3.zoom()
.scaleExtent([0.1, 10])
.on('zoom', zoomed);
svg.append("rect")
.attr('cursor', 'move')
.attr("width", width + padding * 2)
.attr("height", height + padding * 2)
.attr('id', 'zoomBox')
.style("fill", "none")
.style("pointer-events", "all")
.call(zoom);
zoomOn = true;
}
}
});
function zoomed() {
canvas.attr("transform", d3.event.transform)
newXscale = d3.event.transform.rescaleX(dataScale);
newYscale = d3.event.transform.rescaleY(dataScale);
newZscale = d3.event.transform.k;
gX.call(xAxis.scale(newXscale));
gY.call(yAxis.scale(newYscale));
}
function clicked() {
var coords = d3.mouse(this);
if (newXscale && newYscale) {
var x = newXscale.invert(coords[0] - padding);
var y = newYscale.invert(coords[1] - padding);
};
points.append('circle')
.attr('cx', (!x) ? coords[0] : dataScale(x) + (padding / newZscale))
.attr('cy', (!y) ? coords[1] : dataScale(y) + (padding / newZscale))
.attr('r', 5);
}
<script src="https://d3js.org/d3.v4.min.js"></script>
I figured it out. The problem lied in the fact that I was removing the zoom box when toggling the zoom. I switched the event listener to just hide the box and unbind the pointer-events. Here is the final code:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
</head>
<body>
<script>
var data = [{x:17,y:3},
{x:20,y:16},
{x:2,y:13},
{x:19,y:10},
{x:13,y:15},
{x:2,y:2},
{x:5,y:8},
{x:11,y:19},
{x:20,y:12},
{x:10,y:20}];
var width = 600;
var height = 600;
var padding = 50;
var newXscale, newYscale;
var zoomOn = false;
var xScale = d3.scaleLinear()
.domain([0,21])
.range([0, width]);
var yScale = d3.scaleLinear()
.domain([0,21])
.range([0, width]);
var svg = d3.select('body').append('svg')
.attr('width', width+2*padding)
.attr('height', height+2*padding)
.on('click', clicked)
.attr('cursor','crosshair');
var xAxis = d3.axisTop()
.scale(xScale);
var gX = svg.append('g')
.attr('transform','translate(50,50)')
.call(xAxis);
var yAxis = d3.axisLeft()
.scale(yScale);
var gY = svg.append('g')
.attr('transform','translate(50,50)')
.call(yAxis);
var canvas = svg.append('g')
var points = canvas.append('g');
points.selectAll('circle').data(data)
.enter().append('circle')
.attr('cx', function(d) {return xScale(d.x)+padding})
.attr('cy', function(d) {return yScale(d.y)+padding})
.attr('r', 5);
var zoom = d3.zoom()
.scaleExtent([0.1, 10])
.on('zoom', zoomed);
var zoombox = svg.append("rect")
.attr("width", width+padding*2)
.attr("height", height+padding*2)
.attr('id','zoomBox')
.style("fill", "none")
.style("pointer-events", "none")
.style('visibility','off')
.call(zoom);
window.addEventListener('keydown', function (event) {
if (event.key=='z') {
if (zoomOn) {
d3.select('#zoomBox')
.attr('cursor','auto')
.style('pointer-events','none')
.style('visibility','off');
zoomOn = false;
} else {
d3.select('#zoomBox')
.attr('cursor','move')
.style('pointer-events','all')
.style('visibilty','on')
zoomOn = true;
}
}
});
function zoomed() {
canvas.attr("transform", d3.event.transform)
newXscale = d3.event.transform.rescaleX(xScale);
newYscale = d3.event.transform.rescaleY(yScale);
gX.call(xAxis.scale(newXscale));
gY.call(yAxis.scale(newYscale));
newZscale = d3.event.transform.k;
}
function clicked() {
var coords = d3.mouse(this);
if (newXscale && newYscale) {
var x = newXscale.invert(coords[0] - padding);
var y = newYscale.invert(coords[1] - padding);
};
console.log(newZscale);
points.append('circle')
.attr('cx', (!x) ? coords[0] : xScale(x) + (padding / newZscale))
.attr('cy', (!y) ? coords[1] : yScale(y) + (padding / newZscale))
.attr('r', 5);
console.log(x + ' ' + y);
}
</script>
</body>
</html>
Related
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>
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>
Having Issue with d3.drag for angular 4. whenever I drag the rectangle object , it is moving fine for first time. After release of mousepress and again trying to drag the rectangle, it is going back to previous event and not able to make mouse control on draggable object. Please give solution to my problem.
import { Component,Input, ElementRef, OnInit } from '#angular/core';
import * as d3 from 'd3';
interface LineData{
xVal: number,
yVal:number
}
#Component({
selector: 'app-line-chart',
template:'<svg height="500" width="500" ></svg>',
styleUrl: []
})
export class LineChartComponent implements OnInit {
#Input() data : LineData[];
private parentNativeElement : any;
constructor(private element:ElementRef) {
this.parentNativeElement = element.nativeElement;
}
ngOnInit() {
var width = 300;
var height = 300;
var margin = {top: 10, right: 10, bottom: 30, left: 10}
var x = d3.scaleLinear().range([0, width]);
var y = d3.scaleLinear().range([height, 0]);
var xAxis = d3.axisBottom(x)
.scale(x)
.ticks(5);
var yAxis = d3.axisLeft(y)
.scale(y)
.ticks(5);
var valueline:any = d3.line()
.x(function (d) {
return x(d['xVal']);
})
.y(function (d) {
return y(d['yVal']);
});
console.log(valueline);
var svg = d3.select("svg");
d3.select(this.parentNativeElement)
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g");
// Get the data
var data = [
{
"xVal": 1,
"yVal": 2
},
{
"xVal": 2,
"yVal": 4
},
{
"xVal": 3,
"yVal": 1
},
{
"xVal": 4,
"yVal": 5
},
{
"xVal": 5,
"yVal": 3
}
];
// Scale the range of the data
x.domain(d3.extent(data,
function (d) {
return d.xVal;
}));
y.domain([
0, d3.max(data,
function (d) {
return d.yVal;
})
]);
let color = d3.scaleOrdinal(d3.schemeCategory10);
svg.append("path").datum(data).attr("class","path")// Add the valueline path.
.attr("fill", "none")
.attr("stroke", "red")
.attr("stroke-width", 1.5)
.attr("d", valueline(data)).attr("class", "line");
let rectangle:any = d3.range(1).map(function(){
return{
x: Math.floor(Math.random()*width),
y: Math.floor(Math.random()*height)
};
});
console.log(rectangle);
let dragRect = svg.selectAll('g').data(rectangle).enter().append("g")
dragRect.append("rect")
.attr("x",function(d){return d['x'];})
.attr("y",function(d){return d['y'];})
.attr("height",50)
.attr("width",50).style("fill", "steelblue");
svg.selectAll('g').attr("transform",
"translate(" + margin.left + "," + margin.top + ")").data(rectangle)
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
function dragstarted(d){
d3.select(this).raise().classed("active",true);
}
function dragged(d){
d.xVal = x.invert(d3.event.x);
d.yVal = y.invert(d3.event.y);
d3.select(this).select("rect")
.attr("x", x(d.xVal))
.attr("y", y(d.yVal))
.attr("transform","translate("+d.xVal+","+d.yVal+")")
console.log(d);
}
function dragended(d){
d3.select(this).raise().classed("active",false);
//d3.select('rect#no-drag').on('mousedown.drag',null);
}
svg.append("g") // Add the X Axis
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g") // Add the Y Axis
.attr("class", "y axis"`enter code here`)
.call(yAxis);
}
}
The basic problem seems to be that dragged function is not remembering the x & y between successive drag events.
To do this, you need
d3.select(this)
.attr("x", d.x = x(d.xVal))
.attr("y", d.y = y(d.yVal))
instead of
d3.select(this)
.attr("x", x(d.xVal))
.attr("y", y(d.yVal))
Run this code snippet to check it out
console.clear()
var width = 300;
var height = 300;
var margin = {top: 10, right: 10, bottom: 30, left: 10}
var x = d3.scaleLinear().range([0, width]);
var y = d3.scaleLinear().range([height, 0]);
var xAxis = d3.axisBottom(x).scale(x).ticks(5);
var yAxis = d3.axisLeft(y).scale(y).ticks(5);
var valueline = d3.line()
.x(function (d) { return x(d['xVal']); })
.y(function (d) { return y(d['yVal']); });
var svg = d3.select("svg");
d3.select(this.parentNativeElement)
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g");
// Get the data
var data = [
{
"xVal": 1,
"yVal": 2
},
{
"xVal": 2,
"yVal": 4
},
{
"xVal": 3,
"yVal": 1
},
{
"xVal": 4,
"yVal": 5
},
{
"xVal": 5,
"yVal": 3
}
];
// Scale the range of the data
x.domain(d3.extent(data,
function (d) {
return d.xVal;
}));
y.domain([
0, d3.max(data,
function (d) {
return d.yVal;
})
]);
let color = d3.scaleOrdinal(d3.schemeCategory10);
svg.append("path").datum(data).attr("class","path")
.attr("fill", "none")
.attr("stroke", "red")
.attr("stroke-width", 1.5)
.attr("d", valueline(data)).attr("class", "line");
let rectangle = d3.range(3).map(function() {
return {
x: Math.floor(Math.random()*width),
y: Math.floor(Math.random()*height)
};
});
let dragRect = svg.selectAll('g').data(rectangle).enter()
.append("g")
dragRect.append("rect")
.attr("x",function(d){return d['x'];})
.attr("y",function(d){return d['y'];})
.attr("height", 50)
.attr("width", 50)
.style("fill", "steelblue")
svg.selectAll('rect')
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.data(rectangle)
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended)
);
const dragBounds = {}
const tickHeight = 10;
function setDragBounds(subject) {
dragBounds.top = 0 - margin.top;
dragBounds.left = 0 - margin.left;
dragBounds.bottom = height - tickHeight - subject.attr('height');
dragBounds.right = width - margin.right - subject.attr('width');
}
function dragstarted(d){
/*
Calculate drag bounds at dragStart because it's one event vs many
events if done in 'dragged()'
*/
setDragBounds(d3.select(this))
d3.select(this).raise().classed("active", true);
}
function dragged(d){
d3.select(this)
.attr("x", getX(d.x = d3.event.x) )
.attr("y", getY(d.y = d3.event.y) );
}
function getX(x) {
return x < dragBounds.left ? dragBounds.left
: x > dragBounds.right ? dragBounds.right
: x
}
function getY(y) {
return y < dragBounds.top ? dragBounds.top
: y > dragBounds.bottom ? dragBounds.bottom
: y
}
function dragended(d){
d3.select(this).classed("active", false);
}
svg.append("g") // Add the X Axis
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
svg.append("g") // Add the Y Axis
.attr("class", "y axis")
.call(yAxis);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg height="500" width="500" ></svg>
Note I attached the drag events to rect instead of g, which I assume was a typo.
Keeping within bounds
To constrain the drag to bounds the way I found works best is a max/min function on the x & y values.
function dragged(d){
d3.select(this)
.attr("x", getX(d.x = d3.event.x) )
.attr("y", getY(d.y = d3.event.y) );
}
function getX(x) {
return x < dragBounds.left ? dragBounds.left
: x > dragBounds.right ? dragBounds.right
: x
}
function getY(y) {
return y < dragBounds.top ? dragBounds.top
: y > dragBounds.bottom ? dragBounds.bottom
: y
}
Bounds are set at drag start to avoid repetition of any calculation.
const dragBounds = {}
const tickHeight = 10;
function setDragBounds(subject) {
dragBounds.top = 0 - margin.top;
dragBounds.left = 0 - margin.left;
dragBounds.bottom = height - tickHeight - subject.attr('height');
dragBounds.right = width - margin.right - subject.attr('width');
}
function dragstarted(d){
/*
Calculate drag bounds at dragStart because it's one event vs many
events if done in 'dragged()'
*/
setDragBounds(d3.select(this))
d3.select(this).raise().classed("active", true);
}
I'm tying to implement semantic zoom with d3.js v4. Most examples and questions on Stackoverflow are for v3. So i tried to alter one of them, like from this answer. Example from the answer: bl.ocks.org example
I tried to adept the example for d3 v4:
var xOld, yOld;
var width = document.querySelector('body').clientWidth,
height = document.querySelector('body').clientHeight;
var randomX = d3.randomNormal(width / 2, 80),
randomY = d3.randomNormal(height / 2, 80);
var data = d3.range(2000).map(function() {
return [
randomX(),
randomY()
];
});
var xScale = d3.scaleLinear()
.domain(d3.extent(data, function (d) {
return d[0];
}))
.range([0, width]);
var yScale = d3.scaleLinear()
.domain(d3.extent(data, function (d) {
return d[1];
}))
.range([0, height]);
var xExtent = xScale.domain();
var yExtent = yScale.domain();
var zoomer = d3.zoom().scaleExtent([1, 8]).on("zoom", zoom);
var svg0 = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
var svg = svg0.append('g')
.attr("width", width)
.attr("height", height)
var circle = svg.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("r", 2.5)
.attr("transform", transform_);
svg0
.call(zoomer)
.call(zoomer.transform, d3.zoomIdentity);
function zoom(e) {
var transform = d3.zoomTransform(this);
var x = 0;
var y = 0;
if(d3.event.sourceEvent) {
var x = d3.event.sourceEvent.layerX;
var y = d3.event.sourceEvent.layerY;
}
var scale = Math.pow(transform.k, .8);
xScale = d3.scaleLinear()
.domain([xExtent[0], xExtent[1] / scale])
.range([0, width]);
yScale = d3.scaleLinear()
.domain([yExtent[0], yExtent[1] / scale])
.range([0, height]);
circle.attr('transform', transform_)
svg.attr("transform", "translate(" + d3.event.transform.x + "," + d3.event.transform.y + ")");
}
function transform_(d) {
var x = xScale(d[0]);
var y = yScale(d[1]);
return "translate(" + x + "," + y + ")";
}
The zoom itself works - basically. Like the normal zoom it should zoom to the position of the mouse pointer, which it doesn't. Also the panning looks a little bit unsmooth.
I tried to use the mouse position from the d3.event.sourceEvent as offset for the translation, but it didn't work.
So, how could the zoom use the mouse position? It would be also great to get smoother panning gesture.
The zoom on mouse pointer can be added using pointer-events attribute.
Also, I have an example for a semantic zoom for d3 version 4 with the mouse pointer and click controls and also displaying the scale value for reference.[enter link description here][1]
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
var randomX = d3.randomNormal(width / 2, 80),
randomY = d3.randomNormal(height / 2, 80),
data = d3.range(20).map(function() {
return [randomX(), randomY()];
});
var scale;
console.log(data);
var circle;
var _zoom = d3.zoom()
.scaleExtent([1, 8])
.on("zoom", zoom);
circle = svg.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("r", 5)
.attr("transform", transform(d3.zoomIdentity));
svg.append("rect")
.attr("fill", "none")
.attr("pointer-events", "all")
.attr("width", width)
.attr("height", height)
.call(_zoom);
function zoom() {
circle.attr("transform", transform(d3.event.transform));
scale = d3.event.transform.k;
console.log(scale);
document.getElementById('scale').value = scale;
}
function transform(t) {
return function(d) {
return "translate(" + t.apply(d) + ")";
}
}
var gui = d3.select("#gui");
gui.append("span")
.classed("zoom-in", true)
.text("+")
.on("click", function() {
_zoom.scaleBy(circle, 1.2);
});
gui.append("span")
.classed("zoom-out", true)
.text("-")
.on("click", function() {
_zoom.scaleBy(circle, 0.8);
});
please find the link to fiddle:
[1]: https://jsfiddle.net/sagarbhanu/5jLbLpac/3/
Can someone help me implementing a spiral chart similar to the one below using d3.js?
I've just got the basic spiral plot (a simple one) as of now but not been able to append bars to the plot based on the timeline as shown in the image. I'm trying out a few things (if you see the commented code).
Here's my fiddle, and my code:
var width = 400,
height = 430,
axes = 12,
tick_axis = 9,
start = 0,
end = 2.25;
var theta = function(r) {
return 2 * Math.PI * r;
};
var angle = d3.scale.linear()
.domain([0, axes]).range([0, 360])
var r = d3.min([width, height]) / 2 - 40;
var r2 = r;
var radius = d3.scale.linear()
.domain([start, end])
.range([0, r]);
var svg = d3.select("#chart").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + (height / 2 + 8) + ")");
var points = d3.range(start, end + 0.001, (end - start) / 1000);
var spiral = d3.svg.line.radial()
.interpolate("cardinal")
.angle(theta)
.radius(radius);
var path = svg.selectAll(".spiral")
.data([points])
.enter().append("path")
.attr("class", "spiral")
.attr("d", spiral)
var z = d3.scale.category20();
var circles = svg.selectAll('.circle')
.data(points);
/* circles.enter().append('circle')
.attr('r', 5)
.attr('transform', function(d) { return 'translate(' + d + ')'})
.style('fill', function(d) { return z(d); });
*/
var circle = svg.append("circle")
.attr("r", 13)
.attr("transform", "translate(" + points[0] + ")");
var movingCircle = circle.transition().duration(4000)
.attrTween('transform', translateAlongPath(path.node()))
// .attr('cx', function(d) { return radius(d) * Math.cos(theta(d))})
// .attr('cy', function(d) { return radius(d) * Math.sin(theta(d))})
function translateAlongPath(path) {
var l = path.getTotalLength();
return function(d, i, a) {
return function(t) {
var p = path.getPointAtLength(t * l);
//console.log(p)
return "translate(" + p.x + "," + p.y + ")";
};
};
}
function pathXY(path) {
var l = path.getTotalLength();
var start = 0;
/* for(i=start; i<l; i++) {
var point = path.getPointAtLength(i);
svg.append('rect').transition().duration(400).attr('transform', 'translate(' + point.x +','+point.y+')')
.attr('width', 10).attr('height', 30).style('fill', z);
}*/
}
pathXY(path.node());
/*var test = translateAlongPath(path.node())()();
//console.log(test)
var bars = svg.selectAll('.bar')
.data(points).enter().append('rect').transition().duration(2000)
// .attrTween('transform', translateAlongPath(path.node()))
.attr('class', 'bar')
.attr('width', 10)
.attr('height', 20)
.style('fill', function(d) { return z(d)});
*/
var rect = svg.append('rect').attr('width', 10).attr('height', 10);
rect.transition().duration(3400)
.attrTween('transform', translateAlongPath(path.node()));
It'd be great to have a few similar examples (i.e. spiral timeline plot).
Thanks.
Glad you came back and updated your question, because this is an interesting one. Here's a running minimal implementation. I've commented it ok, so let me know if you have any questions...
<!DOCTYPE html>
<html>
<head>
<script data-require="d3#4.0.0" data-semver="4.0.0" src="https://d3js.org/d3.v4.js"></script>
</head>
<body>
<div id="chart"></div>
<script>
var width = 500,
height = 500,
start = 0,
end = 2.25,
numSpirals = 4;
var theta = function(r) {
return numSpirals * Math.PI * r;
};
var r = d3.min([width, height]) / 2 - 40;
var radius = d3.scaleLinear()
.domain([start, end])
.range([40, r]);
var svg = d3.select("#chart").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
// create the spiral, borrowed from http://bl.ocks.org/syntagmatic/3543186
var points = d3.range(start, end + 0.001, (end - start) / 1000);
var spiral = d3.radialLine()
.curve(d3.curveCardinal)
.angle(theta)
.radius(radius);
var path = svg.append("path")
.datum(points)
.attr("id", "spiral")
.attr("d", spiral)
.style("fill", "none")
.style("stroke", "steelblue");
// fudge some data, 2 years of data starting today
var spiralLength = path.node().getTotalLength(),
N = 730,
barWidth = (spiralLength / N) - 1;
var someData = [];
for (var i = 0; i < N; i++) {
var currentDate = new Date();
currentDate.setDate(currentDate.getDate() + i);
someData.push({
date: currentDate,
value: Math.random()
});
}
// here's our time scale that'll run along the spiral
var timeScale = d3.scaleTime()
.domain(d3.extent(someData, function(d){
return d.date;
}))
.range([0, spiralLength]);
// yScale for the bar height
var yScale = d3.scaleLinear()
.domain([0, d3.max(someData, function(d){
return d.value;
})])
.range([0, (r / numSpirals) - 30]);
// append our rects
svg.selectAll("rect")
.data(someData)
.enter()
.append("rect")
.attr("x", function(d,i){
// placement calculations
var linePer = timeScale(d.date),
posOnLine = path.node().getPointAtLength(linePer),
angleOnLine = path.node().getPointAtLength(linePer - barWidth);
d.linePer = linePer; // % distance are on the spiral
d.x = posOnLine.x; // x postion on the spiral
d.y = posOnLine.y; // y position on the spiral
d.a = (Math.atan2(angleOnLine.y, angleOnLine.x) * 180 / Math.PI) - 90; //angle at the spiral position
return d.x;
})
.attr("y", function(d){
return d.y;
})
.attr("width", function(d){
return barWidth;
})
.attr("height", function(d){
return yScale(d.value);
})
.style("fill", "steelblue")
.style("stroke", "none")
.attr("transform", function(d){
return "rotate(" + d.a + "," + d.x + "," + d.y + ")"; // rotate the bar
});
// add date labels
var tF = d3.timeFormat("%b %Y"),
firstInMonth = {};
svg.selectAll("text")
.data(someData)
.enter()
.append("text")
.attr("dy", 10)
.style("text-anchor", "start")
.style("font", "10px arial")
.append("textPath")
// only add for the first of each month
.filter(function(d){
var sd = tF(d.date);
if (!firstInMonth[sd]){
firstInMonth[sd] = 1;
return true;
}
return false;
})
.text(function(d){
return tF(d.date);
})
// place text along spiral
.attr("xlink:href", "#spiral")
.style("fill", "grey")
.attr("startOffset", function(d){
return ((d.linePer / spiralLength) * 100) + "%";
})
</script>
</body>
</html>