Wrong point of origin reference - javascript

I'm facing problem with wrong point of origin when multiple elements are added to an SVG.
JSFiddle: https://jsfiddle.net/sbc6ejeu/2/
I've added an SVG and associated path and couple of circles to it. They seem to correspond to the correct origin. However when I move the slider, I expect the circle of id=movingCicle (as mentioned in the code) to move along the green curve (line). I'm unable to start the initial
position of the circle to the same origin as other svg elements.
Also I observe that the range of the red circle is not same as the other elements or the SVG to which it is appended. For the 2nd and 3rd drop down options, the red cicle moves out of the graph when the slider is increased. I feel I'm missing out on something.
Appretiate any help on this.Thanks!
function initialize() {
// Add circle data
jsonCircles = [{
"xAxis": 50,
"yAxis": 154
}, {
"xAxis": 150,
"yAxis": 154
}];
// Set the dimensions of the canvas / graph
var margin = {
top: 30,
right: 20,
bottom: 30,
left: 50
};
width = 600 - margin.left - margin.right;
height = 270 - margin.top - margin.bottom;
// Set the ranges
x = d3.scale.linear().range([0, width]);
y = d3.scale.linear().range([height, 0]);
// Define the axes
xAxis = d3.svg.axis().scale(x)
.orient("bottom").ticks(10);
yAxis = d3.svg.axis().scale(y)
.orient("left").ticks(7);
valueLine = d3.svg.line()
.x(function(d, i) {
return x(i);
})
.y(function(d) {
return y(d);
});
// Adds the svg canvas
svg = d3.select("#graph")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.attr("id", "svg1")
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
}
function updateCirclePosition(i) {
d3.select("#movingCircle").remove();
svg.append("circle")
.attr("cx", +i)
.attr("cy", yValues[i])
.attr("r", 5)
.attr("id", "movingCircle")
.style("fill", "red");
}
function addSvgElements() {
// Add the valueline path.
var path = svg.append("path")
.attr("class", "line")
.attr("id", "lineId")
.attr("d", valueLine(yValues));
}

Inside the function updateCirclePosition, the variable i contains the value of the budget, and yValues[i] is the corresponding revenue.
The corresponding coordinates in the chart can be found using x and y functions, therefore x(i) and y(yValues[i]) should be used to set the correct cx and cy:
svg.append("circle")
.attr("cx", x(i))
.attr("cy", y(yValues[i]))
updated fiddle: https://jsfiddle.net/sbc6ejeu/5/

Related

D3 scatterplot newbe

I'm new to D3 and need a simple scatterplot.
The problem is that the data is not showing up where I want it to show up. I made some test data giving values for x and y between 100 an 200 but the dots
always seem to be in the same place on the screen. What I change to domain or range they show up on the same place. I think It must be something fundamental but I cant find it. Please give me a clue.
This is the code from the test:
<!DOCTYPE html>
<meta charset="utf-8">
<!-- Load d3.js -->
<script src="d3.v6.js"></script>
<!-- Create a div where the graph will take place -->
<div id="my_dataviz"></div>
<script>
// set the dimensions and margins of the graph
const margin = {top: 10, right: 30, bottom: 30, left: 60},
width = 920 - margin.left - margin.right,
height = 800 - margin.top - margin.bottom;
// append the svg object to the body of the page
const svg = d3.select("#my_dataviz")
.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})`);
let dataExample = [];
for (let i = 0; i < 10000; i++) {
const x = Math.floor(Math.random() * 100) + 100;
const y = Math.floor(Math.random() * 100) + 100;
dataExample.push([x, y]);
}
//Read the data (DataFile.csv is NOT used. Using data from dataExample
d3.csv("DataFile.csv").then( function(data) {
// Add X axis
const x = d3.scaleLinear()
.domain([0, 10000])
.range([ 0, width ]);
svg.append("g")
.attr("transform", `translate(0, ${height})`)
.call(d3.axisBottom(x));
// Add Y axis
const y = d3.scaleLinear()
.domain([0, 10000])
.range([ height, 0]);
svg.append("g")
.call(d3.axisLeft(y));
// Add dots
svg.append('g')
.selectAll("dot")
.data(dataExample)
.join("circle")
.attr("cx", function (d) { return d[0]; } )
.attr("cy", function (d) { return d[1]; } )
//console.log(dataExample)
.attr("r", 1.5)
.style("fill", "#69b3a2")
})
</script>
The axes are 0 to 10000 but the plotted data shows op between y=7200 to 8800 and x=800 and 2500.
You need to use your x and y scales when setting the "cx" and "cy" attributes of the circles. Right now you're setting these attributes to the values in your data, without using the scales. This code should look like this:
svg.append('g')
.selectAll("circle")
.data(dataExample)
.join("circle")
.attr("cx", function (d) { return x(d[0]); } )
.attr("cy", function (d) { return y(d[1]); } )
.attr("r", 1.5)
.style("fill", "#69b3a2");
The purpose of the x and y scales in the scatterplot are to map values in your data to positions in the scatterplot.
With this fix, you'll likely want to update the domains of the scales to better match the values in the data, such as by using d3.extent to get the min and max values.

Scaling y-axis appropriate to data in multiple line chart display

The multiple line chart example at https://www.d3-graph-gallery.com/graph/line_smallmultiple.html quite clearly provides the examples I need for what I'm trying to do...
except...
I need the y-axis scale for each of the charts to be appropriate for the data associated with the individual keys. As is, the example does d3.max on the entire data set, not the filtered data set controlling the individual lines.
I've tried various ways to apply the filter in the y-axis definition and can't get anything to work.
The closest I've been able to get is to make it use the max value from one of the specific keys for all the charts.
var y = d3.scaleLinear()
// .domain([0, d3.max(data, function(d) { return +d.n; })])
.domain([0, d3.max(data.filter(d => d.name === "Helen"), e => +e.n)])
.range([ height, 0 ]);
svg.append("g")
.call(d3.axisLeft(y).ticks(5));
I think I want it to filter d.name against the CURRENT-CHART key (whatever it might be) rather than a specific one (like "Helen" above), but can't figure out how to do it. Is it some feature of nesting that I haven't found yet? Something amazingly simple that I can't see??
Any suggestions?
Thanks in advance
I have built a demo for you, i hope you are looking for something like this. Please let me know if there is any issue.
// set the dimensions and margins of the graph
var margin = {top: 30, right: 0, bottom: 30, left: 50},
width = 210 - margin.left - margin.right,
height = 210 - margin.top - margin.bottom;
//Read the data
d3.csv("https://raw.githubusercontent.com/holtzy/data_to_viz/master/Example_dataset/5_OneCatSevNumOrdered.csv", function(data) {
// group the data: I want to draw one line per group
var sumstat = d3.nest() // nest function allows to group the calculation per level of a factor
.key(function(d) { return d.name;})
.entries(data);
// What is the list of groups?
allKeys = sumstat.map(function(d){return d.key})
// Add X axis --> it is a date format
var x = d3.scaleLinear()
.domain(d3.extent(data, function(d) { return d.year; }))
.range([ 0, width ]);
// color palette
var color = d3.scaleOrdinal()
.domain(allKeys)
.range(['#e41a1c','#377eb8','#4daf4a','#984ea3','#ff7f00','#ffff33','#a65628','#f781bf','#999999'])
// Add an svg element for each group. The will be one beside each other and will go on the next row when no more room available
var svg = d3.select("#my_dataviz")
.selectAll("uniqueChart")
.data(sumstat)
.enter()
.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 + ")")
.each(multiple);
svg
.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x).ticks(3));
// Add titles
svg
.append("text")
.attr("text-anchor", "start")
.attr("y", -5)
.attr("x", 0)
.text(function(d){ return(d.key)})
.style("fill", function(d){ return color(d.key) })
function multiple(item) {
var svg = d3.select(this);
var y = d3.scaleLinear()
.domain([0, d3.max(item.values, function(d) { return +d.n; })])
.range([height, 0]);
svg.append("g")
.call(d3.axisLeft(y).ticks(5));
var line = d3.line()
.x(function(d) { return x(+d.year); })
.y(function(d) { return y(+d.n); });
// Draw the line
svg
.append("path")
.attr("fill", "none")
.attr("stroke", function(d){ return color(d.key) })
.attr("stroke-width", 1.9)
.attr("d", line(item.values))
}
})
<!DOCTYPE html>
<meta charset="utf-8">
<!-- Load d3.js -->
<script src="https://d3js.org/d3.v4.js"></script>
<!-- Create a div where the graph will take place -->
<div id="my_dataviz"></div>

Creating a zoom function in D3

I have a function where that when a button is pressed (Several buttons the represent several animal types), that animal types SVG is updated with its corresponding data. I'm trying to replicate this zoom function but am having issues implementing it with my code. There are several SVGs that are used globally like this (one for each animal type):
let x = d3.scaleLinear()
.domain([0, 1000])
.range([ 0, width ]);
var xAxis = d3.axisBottom(x);
svgReptile.append("g")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
const yAxis = d3.scaleLinear()
.domain([0, 220])
.range([ height, 0])
svgReptile.append("g")
.call(d3.axisLeft(yAxis))
The function below is called when one of the animal buttons is pressed.
function update(animal, whatSVG, xAxis, yAxis, color) {
const points = whatSVG
.selectAll("circle")
.data(data);
points.enter()
.append("circle")
.attr("cx", function(d) {
return xAxis(d.state);
})
.attr("cy", function(d) {
return yAxis(d.percentage);
})
.merge(points)
.attr("r", 3)
.attr("cx", function(d) {
return xAxis(d.decade)
})
.attr("cy", function(d) {
return yAxis(d.count)
})
.style("fill", function (d) { return colour(d.animal) } );
points.exit()
.attr('r', 0)
.remove();
}
Question:
How can I implement a zoom feature that expands the x-axis when zoomed (or anything similar) like the one linked above?
I think you're looking for a 'brush zoom' from the last line of your question.
The following source code if from an example in a d3 graph gallery
The cross hair allows you to select an area to expand. If you follow the link there is a graph above it that is entitled "Zoom with axis" but it doesn't zoom in the way you've described, it just moves the axis, but doesn't enlarge the graph contents with it. Perhaps both will be useful!
Hope this helps
// set the dimensions and margins of the graph
var margin = {top: 10, right: 20, bottom: 20, left: 20},
width = 500 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
// append the svg object to the body of the page
var Svg = d3.select("#brushZoom")
.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 + ")");
//Read the data
d3.csv("https://raw.githubusercontent.com/holtzy/D3-graph-gallery/master/DATA/iris.csv", function(data) {
// Add X axis
var x = d3.scaleLinear()
.domain([4, 8])
.range([ 0, width ]);
var xAxis = Svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
// Add Y axis
var y = d3.scaleLinear()
.domain([0, 9])
.range([ height, 0]);
Svg.append("g")
.call(d3.axisLeft(y));
// Add a clipPath: everything out of this area won't be drawn.
var clip = Svg.append("defs").append("svg:clipPath")
.attr("id", "clip")
.append("svg:rect")
.attr("width", width )
.attr("height", height )
.attr("x", 0)
.attr("y", 0);
// Color scale: give me a specie name, I return a color
var color = d3.scaleOrdinal()
.domain(["setosa", "versicolor", "virginica" ])
.range([ "#440154ff", "#21908dff", "#fde725ff"])
// Add brushing
var brush = d3.brushX() // Add the brush feature using the d3.brush function
.extent( [ [0,0], [width,height] ] ) // initialise the brush area: start at 0,0 and finishes at width,height: it means I select the whole graph area
.on("end", updateChart) // Each time the brush selection changes, trigger the 'updateChart' function
// Create the scatter variable: where both the circles and the brush take place
var scatter = Svg.append('g')
.attr("clip-path", "url(#clip)")
// Add circles
scatter
.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx", function (d) { return x(d.Sepal_Length); } )
.attr("cy", function (d) { return y(d.Petal_Length); } )
.attr("r", 8)
.style("fill", function (d) { return color(d.Species) } )
.style("opacity", 0.5)
// Add the brushing
scatter
.append("g")
.attr("class", "brush")
.call(brush);
// A function that set idleTimeOut to null
var idleTimeout
function idled() { idleTimeout = null; }
// A function that update the chart for given boundaries
function updateChart() {
extent = d3.event.selection
// If no selection, back to initial coordinate. Otherwise, update X axis domain
if(!extent){
if (!idleTimeout) return idleTimeout = setTimeout(idled, 350); // This allows to wait a little bit
x.domain([ 4,8])
}else{
x.domain([ x.invert(extent[0]), x.invert(extent[1]) ])
scatter.select(".brush").call(brush.move, null) // This remove the grey brush area as soon as the selection has been done
}
// Update axis and circle position
xAxis.transition().duration(1000).call(d3.axisBottom(x))
scatter
.selectAll("circle")
.transition().duration(1000)
.attr("cx", function (d) { return x(d.Sepal_Length); } )
.attr("cy", function (d) { return y(d.Petal_Length); } )
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<div id="brushZoom"></div>

D3.JS Y-axis label issue

To start, I am fairly new to D3.Js. I have spent the past week or so working on a D3.JS issue-specifically making a graph with a Y-axis label. However, I cannot get the graph exactly how I want. It is almost there but inverted or my data comes out wrong. Now I will briefly show some of my code and images of my main problem before showing all of the code. I have spent time looking at other Stack Overflow posts with a similar issue and I do what is on those posts and still have the same issue.
For example, I thought that this post would have the solution: reversed Y-axis D3
The data is the following:
[0,20,3,8] (It is actually an array of objects but I think this may be all that is needed.
So, to start, when the yScale is like this:
var yScale = d3.scaleLinear()
.domain([0, maxPound]) //Value of maxpound is 20
.range([0, 350]);
The bar chart looks like this:
As one can see the Y chart starts with zero at the top and 20 at the bottom-which at first I thought was an easy fix of flipping the values in the domain around to this:
var yScale = d3.scaleLinear()
.domain([0, maxPound]) //Value of maxpound is 20
.range([0, 350]);
I get this image:
In the second image the y-axis is right-20 is on top-Yay! But the graphs are wrong. 0 now returns a value of 350 pixels-the height of the SVG element. That is the value that 20 should be returning! If I try to switch the image range values, I get the same problem!
Now the code:
var w = 350;
var h = 350;
var barPadding = 1;
var margin = {top: 5, right: 200, bottom: 70, left: 25}
var maxPound = d3.max(poundDataArray,
function(d) {return parseInt(d.Pounds)}
);
//Y-Axis Code
var yScale = d3.scaleLinear()
.domain([maxPound, 0])
.range([0, h]);
var yAxis = d3.axisLeft()
.scale(yScale)
.ticks(5);
//Creating SVG element
var svg = d3.select(".pounds")
.append('svg')
.attr("width", w)
.attr('height', h)
.append("g")
.attr("transform", "translate(" + margin.left + "," +
margin.top + ")");
svg.selectAll("rect")
.data(poundDataArray)
.enter()
.append("rect")
.attr('x', function(d, i){
return i * (w / poundDataArray.length);
})
.attr('y', function(d) {
return 350 - yScale(d.Pounds);
})
.attr('width', (w / 4) - 25)
.attr('height', function(d){
return yScale(d.Pounds);
})
.attr('fill', 'steelblue');
//Create Y axis
svg.append("g")
.attr("class", "axis")
.call(yAxis);
Thank you for any help! I believe that the error may be in the y or height values and have spent time messing around there with no results.
That is not a D3 issue, but an SVG feature: in an SVG, the origin (0,0) is at the top left corner, not the bottom left, as in a common Cartesian plane. That's why using [0, h] as the range makes the axis seem to be inverted... actually, it is not inverted: that's the correct orientation in an SVG. By the way, HTML5 Canvas has the same coordinates system, and you would have the same issue using a canvas.
So, you have to flip the range, not the domain:
var yScale = d3.scaleLinear()
.domain([0, maxPound])
.range([h, 0]);//the range goes from the bottom to the top now
Or, in your case, using the margins:
var yScale = d3.scaleLinear()
.domain([0, maxPound])
.range([h - margin.bottom, margin.top]);
Besides that, the math for the y position and height is wrong. It should be:
.attr('y', function(d) {
return yScale(d.Pounds);
})
.attr('height', function(d) {
return h - margin.bottom - yScale(d.Pounds);
})
Also, as a bonus tip, don't hardcode the x position and the width. Use a band scale instead.
Here is your code with those changes:
var poundDataArray = [{
Pounds: 10
}, {
Pounds: 20
}, {
Pounds: 5
}, {
Pounds: 8
}, {
Pounds: 14
}, {
Pounds: 1
}, {
Pounds: 12
}];
var w = 350;
var h = 350;
var barPadding = 1;
var margin = {
top: 5,
right: 20,
bottom: 70,
left: 25
}
var maxPound = d3.max(poundDataArray,
function(d) {
return parseInt(d.Pounds)
}
);
//Y-Axis Code
var yScale = d3.scaleLinear()
.domain([0, maxPound])
.range([h - margin.bottom, margin.top]);
var xScale = d3.scaleBand()
.domain(d3.range(poundDataArray.length))
.range([margin.left, w - margin.right])
.padding(.2);
var yAxis = d3.axisLeft()
.scale(yScale)
.ticks(5);
//Creating SVG element
var svg = d3.select("body")
.append('svg')
.attr("width", w)
.attr('height', h)
.append("g")
.attr("transform", "translate(" + margin.left + "," +
margin.top + ")");
svg.selectAll("rect")
.data(poundDataArray)
.enter()
.append("rect")
.attr('x', function(d, i) {
return xScale(i);
})
.attr('y', function(d) {
return yScale(d.Pounds);
})
.attr('width', xScale.bandwidth())
.attr('height', function(d) {
return h - margin.bottom - yScale(d.Pounds);
})
.attr('fill', 'steelblue');
//Create Y axis
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(" + margin.left + ",0)")
.call(yAxis);
<script src="https://d3js.org/d3.v4.min.js"></script>

How better organize grid lines?

There are grid lines from points.
Is there another solution with better performance, because if I add many svg elements(etc. rects, circles, paths) and increase the dimension of the grid I will see the freeze effect when I use zoom, move element...
The size of the grid is changed.
Also, how can I create endless grid lines, instead limited (gridCountX, gridCountY)?
Thanks
var svg = d3.select("body").append("svg");
var svgG = svg.append("g");
var gridLines = svgG.append("g").classed("grid-lines-container", true).data(["gridLines"]);
var gridCountX = _.range(100);
var gridCountY = _.range(100);
var size = 10;
gridLines.selectAll("g").data(gridCountY)
.enter()
.append("g")
.each(function(d) {
d3.select(this).selectAll("circle").data(gridCountX).enter()
.append("circle")
.attr("cx", function(_d) {return _d*size;})
.attr("cy", function(_d) {return d*size;})
.attr("r", 0.5)
.attr("style", function() {
return "stroke: black;";
});
});
var zoomSvg = d3.zoom()
.scaleExtent([1, 10])
.on("zoom", function(){
svgG.attr("transform", d3.event.transform);
});
svg.call(zoomSvg);
svg {
width: 100%;
height: 100%;
border: 1px solid #a1a1a1;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>
<script src="https://d3js.org/d3.v4.min.js"></script>
As you note, this approach is not really scalable and has a larger impact on performance. I have found the approach of utilizing d3 axes for grids to have minimal performance impact while also being relatively straightforward to incorporate with zoom such that you can have infinite zoom with the grid lines updating in a sensible manner due to the "magic" of automatic generation of sensible tick locations in d3.
To implement something similar in d3 v4, you can do something along these lines:
var svg = d3.select("svg"),
margin = {top: 20, right: 140, bottom: 50, left: 70},
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 + ")"),
innerSvg = g.append("svg").attr("width", width).attr("height", height);
// Calculate domain for x and y from data and store in x0, y0 (not shown here)
x.domain(x0);
y.domain(y0);
xGridAxis = d3.axisBottom(x).ticks(10);
yGridAxis = d3.axisLeft(y).ticks(10 * height / width);
// Create grouping and additional set of axes for displaying grid
innerSvg.append("g")
.attr("class", "grid x-grid")
.attr("transform", "translate (0," + height + ")")
.call(xGridAxis
.tickSize(-height, 0, 0)
.tickFormat("")
)
.selectAll(".tick");
innerSvg.append("g")
.attr("class", "grid y-grid")
.attr("transform", "translate (" + width + ", 0)")
.call(yGridAxis
.tickSize(width)
.tickFormat("")
);
// Add element to capture mouse events for drag and pan of plots
var zoom = d3.zoom()
.on("zoom", zoomed);
var scrollZoom = innerSvg.append("rect")
.attr("class", "zoom")
.attr("width", width)
.attr("height", height)
.attr("pointer-events", "all") // Defaults to panning with mouse
.call(zoom);
// Mouse panning and scroll-zoom implementation using d3.zoom
// Modification of : http://bl.ocks.org/lorenzopub/013c0c41f9ffab4d27f860127f79c5f5
function zoomed() {
lastEventTransform = d3.event.transform;
// Rescale the grid using the new transform associated with zoom/pan action
svg.select(".x-grid").call(xGridAxis.scale(lastEventTransform.rescaleX(x)));
svg.select(".y-grid").call(yGridAxis.scale(lastEventTransform.rescaleY(y)));
// Calculate transformed x and y locations which are used to redraw all plot elements
var xt = lastEventTransform.rescaleX(x),
yt = lastEventTransform.rescaleY(y);
// Code below just shows how you might do it. Will need to tweak based on your plot
var line = d3.line()
.x(function(d) { return xt(d.x); })
.y(function(d) { return yt(d.y); });
innerSvg.selectAll(".line")
.attr("d", function(d) { return line(d.values); });
innerSvg.selectAll(".dot")
.attr("cx", function(d) {return xt(d.x); })
.attr("cy", function(d) {return yt(d.y); });
}
Here is a worked out example in d3 v4 that inspired my version above:
http://bl.ocks.org/lorenzopub/013c0c41f9ffab4d27f860127f79c5f5

Categories

Resources