I am trying to generate a graph based on some data from a CSV file.
My code:
<script>
var svg = d3.select("body").append("svg")
.attr("width", 1000)
.attr("height", 1000);
function render(data){
var circles = svg.selectAll("circle").data(data);
circles.enter().append("circle")
.attr("r", 1);
circles
.attr("cx", function (d){return d[' Monthly Term']})
.attr("cy", function (d){ return d[' Principal Balance']/1000});
circles.exit().remove();
}
d3.csv("{% static "data/Total.csv" %}" , type, function(myArray){
render(myArray);
myArray.forEach(function(d){
console.log(d[' Principal Payment'] + ", " + d[' Interest Payment'] + ", " + d[' Principal Balance'] + ", " +d[' Monthly Term']);
});
});
function type(d){
d.value = +d.value;
return d;
}
</script>
Everything "works" but the Y-axis seems reversed.
Not sure if you guys can see the inspection window but the Y-value should be decreasing as x value increases.
Any ideas?
In your console output, the Y value is decreasing as X value increases. In an SVG, the 0,0 location is top left. So a lower Y value is closer to the top of the screen. Try inverting the Y value:
.attr("cy", function (d){ return height - d[' Principal Balance']/1000});
As #genestd states in his answer, SVG y coordinates start at the top and increase in value moving down. Generally, you should be using a d3 scale to map your user space values to svg coordinate values. Looking at the classic bar chart example, you see these lines:
var x = d3.scaleBand().rangeRound([0, width]).padding(0.1),
y = d3.scaleLinear().rangeRound([height, 0]);
Here, the x scale goes from 0 to width and the y from height to 0. They are reversed from each other because of the very thing you are seeing, x increases going left to right while y increases going top to bottom. This .range call is mapping the SVG coordinates.
Later you'll see this line:
y.domain([0, d3.max(data, function(d) { return d.frequency; })]);
This part has now mapped the user space coordinates, saying they run from 0 to the maximum of our data.
You can then use the scales as functions when you plot your point.
In your code it might look like this:
var svg = d3.select("body").append("svg")
.attr("width", 1000)
.attr("height", 1000);
var x = d3.scaleLinear().range([0, 1000]),
y = d3.scaleLinear().range([1000, 0]);
function render(data){
x.domain([0, d3.max(data, function(d){
return d[' Monthly Term'];
});
y.domain([0, d3.max(data, function(d){
return d[' Principal Balance']/1000;
});
var circles = svg.selectAll("circle").data(data);
circles.enter().append("circle")
.attr("r", 1);
circles
.attr("cx", function (d){return x(d[' Monthly Term']); })
.attr("cy", function (d){ return y(d[' Principal Balance']/1000); });
...
Related
I have some circles I want to append to a radial time series chart to indicate key events. Equivalent block here.
static picture:
The code for the circles:
var eventCircles = g.selectAll('.eventCirc')
.data(eventData)
.enter()
.append('circle')
.attr('class','eventCirc')
.attr('cx', function(d) { return x(d.date); })
.attr('cy', function(d) { return y(0)})
.attr('r', 5)
.style('fill', "#003366");
The y(0) scale seems to work fine, because the units are both in pixels, but I can't figure out how to convert degrees to pixels for use with cx -- which is a required attribute for a circle.
The scales are set up as so:
var x = d3.scaleTime()
.range([0, fullCircle]);
var y = d3.scaleRadial()
.range([innerRadius, outerRadius]);
Question
How can I use d.date in conjunction with the x scale to give me a pixel coordinate for the cx attribute (and not simply a degree/radian)?
You need a bit of trigonometry here. Given the specific bl.ocks you linked, this is the math you need:
eventCircles.attr('cx', function(d) {
return y(d.Close) * -Math.sin(x(d.Date) + Math.PI)
})
.attr('cy', function(d) {
return y(d.Close) * Math.cos(x(d.Date) + Math.PI)
})
In the bl.coks you linked, Close is the y value and Date is the x value. Change them according to your data.
Here is the forked bl.ocks: https://blockbuilder.org/GerardoFurtado/16adc1bb5677adfa501b3a03b3637d75
I have a scatter plot made in d3.v3 and no matter how large i increase the width and height variables it does not take up more screen space.
var w = 700;
var h = 700;
var dataset = [
"Over 50 pairs of coordinates that look like [0,1][0,43],
];
//Create SVG element
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("x", function(d) {
return d[0];
})
.attr("y", function(d) {
return d[1];
})
.attr("width",5)
.attr("height",5);
There are more than 50 coordinates in my dataset and i want to be able to display them well so that is why i want this to take up more screen space. Currently there is nothing in my html, and no css for this. How can i adjust this so that the scatter plot takes more screen space?
The code you show doesn't place data points with any consideration of width or height, it places data points based on the values in the data:
.attr("x", function(d) {
return d[0];
})
.attr("y", function(d) {
return d[1];
})
The x and y attributes, without any SVG transformation, expect pixel values. If a point has the datum [25,25] it will be placed 25 pixels from the top and left. Height and width of the svg do not matter - you are stating the pixel position based on the data, not based on the svg dimensions in combination with the data.
Scales are a fundamental part of d3 - you can scale the x and y values of your data points across a range of pixel values. To do this we need to know the domain of the input data - the extent of input values - and the range of the output values - the extent of output values.
There are a number of scales built into D3, including power, logarithmic and linear. I'll demonstrate a linear scale below, but the form is very similar for other continuous scales, such as those noted above.
var xScale = d3.scaleLinear() // create a new linear scale
.domain([0,50]) // map values from 0 to 50 to:
.range([0,width]) // to pixel values of 0 through width
var yScale = d3.scaleLinear() // create a new linear scale
.domain([0,50]) // map values from 0 to 50 to:
.range([height,0]) // to pixel values of height through 0
Since in SVG coordinate space y=0 is the top of the SVG, and normally we want data with y=0 to be displayed at the bottom of the graph, we use a range of [height,0] rather than [0,height]
With the scales set up, to return the pixel value for a given data value we use:
xScale(d[0]); // assuming d[0] holds the x value
or
yScale(d[1]); // assuming d[1] holds the y value
Together this gives us:
var w = 700;
var h = 700;
var dataset = d3.range(50).map(function(d) {
return [Math.random()*50,Math.random()*50];
})
var x = d3.scaleLinear()
.domain([0,50]) // input extent
.range([0,w]) // output extent
var y = d3.scaleLinear()
.domain([0,50])
.range([h,0])
//Create SVG element
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("x", function(d) {
return x(d[0]);
})
.attr("y", function(d) {
return y(d[1]);
})
.attr("width",5)
.attr("height",5);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
Of course we might not know the domain of the data if its dynamic, so d3 has a few built in helpers including d3.min, d3.max, d3.extent.
d3.min and d3.max iterate through an array to find the minimum and maximum values of some property of each item in the data array:
d3.min(dataArray, function(d) {
return d.property;
})
d3.max(dataArray, function(d) {
return d.property;
})
D3.extent does both at the same time returning an array containing min and max:
d3.extent(dataArray, function(d) {
return d.property;
})
We can plug those into the scales too:
var w = 700;
var h = 700;
var dataset = d3.range(50).map(function(d) {
return [Math.random()*50,Math.random()*50];
})
var x = d3.scaleLinear()
.domain([0,d3.max(dataset, function(d) { return d[0]; }) ]) // input extent
.range([0,w]) // output extent
var y = d3.scaleLinear()
.domain( d3.extent(dataset, function(d) { return d[1]; }) )
.range([h,0])
//Create SVG element
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("x", function(d) {
return x(d[0]);
})
.attr("y", function(d) {
return y(d[1]);
})
.attr("width",5)
.attr("height",5);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
The below works perfectly fine for me. The things you might need to check is :
The coordinates range is more than 5. (or you might need to use scale and axis)
Is there a overriding styles to your g
var w = 700;
var h = 700;
var dataset = [[10,10],[10,30],[30,50],[30,70]];
//Create SVG element
var svg = d3.select("#scatterplot")
.append("svg")
.attr("width", w)
.attr("height", h);
svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("x", function(d) {
return d[0];
})
.attr("y", function(d) {
return d[1];
})
.attr("width",5)
.attr("height",5);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div id='scatterplot'></div>
I am trying to get brushing to work similar to this example, but with a grouped bar chart: http://bl.ocks.org/mbostock/1667367
I don't really have a good understanding of how brushing works (I haven't been able to find any good tutorials), so I'm a bit at a loss as to what is going wrong. I will try to include the relevant bits of code below. The chart is tracking the time to fix broken builds by day and then grouped by portfolio. So far the brush is created and the user can move and drag it, but the bars in the main chart are re-drawn oddly and the x axis is not updated at all. Any help you can give would be greatly appreciated. Thank you.
// x0 is the time scale on the X axis
var main_x0 = d3.scale.ordinal().rangeRoundBands([0, main_width-275], 0.2);
var mini_x0 = d3.scale.ordinal().rangeRoundBands([0, main_width-275], 0.2);
// x1 is the portfolio scale on the X axis
var main_x1 = d3.scale.ordinal();
var mini_x1 = d3.scale.ordinal();
// Define the X axis
var main_xAxis = d3.svg.axis()
.scale(main_x0)
.tickFormat(dateFormat)
.orient("bottom");
var mini_xAxis = d3.svg.axis()
.scale(mini_x0)
.tickFormat(dateFormat)
.orient("bottom");
After binding the data...
// define the axis domains
main_x0.domain(data.result.map( function(d) { return d.date; } )
.sort(d3.ascending));
mini_x0.domain(data.result.map( function(d) { return d.date; } )
.sort(d3.ascending));
main_x1.domain(data.result.map( function(d) { return d.portfolio; } )
.sort(d3.ascending))
.rangeRoundBands([0, main_x0.rangeBand() ], 0);
mini_x1.domain(data.result.map( function(d) { return d.portfolio; } )
.sort(d3.ascending))
.rangeRoundBands([0, main_x0.rangeBand() ], 0);
// Create brush for mini graph
var brush = d3.svg.brush()
.x(mini_x0)
.on("brush", brushed);
After adding the axis's, etc.
// Create the bars
var bar = main.selectAll(".bars")
.data(nested)
.enter().append("g")
.attr("class", function(d) { return d.key + "-group bar"; })
.attr("fill", function(d) { return color(d.key); } );
bar.selectAll("rect").append("rect")
.data(function(d) { return d.values; })
.enter().append("rect")
.attr("class", function(d) { return d.portfolio; })
.attr("transform", function(d) { return "translate(" + main_x0(d.date) + ",0)"; })
.attr("width", function(d) { return main_x1.rangeBand(); })
.attr("x", function(d) { return main_x1(d.portfolio); })
.attr("y", function(d) { return main_y(d.buildFixTime); })
.attr("height", function(d) { return main_height - main_y(d.buildFixTime); });
Here is the brush function (trying several different options)...
function brushed() {
main_x1.domain(brush.empty() ? mini_x1.domain() : brush.extent());
//main.select("rect")
//.attr("x", function(d) { return d.values; })
//.attr("width", function(d) { return d.values; });
bar.select("rect")
.attr("width", function(d) { return main_x1.rangeBand(); })
.attr("x", function(d) { return main_x1(d.portfolio); });
//.attr("y", function(d) { console.log(d); return main_y(d.buildFixTime); })
//.attr("height", function(d) { return main_height - main_y(d.buildFixTime); });
main.select(".x.axis").call(main_xAxis);
}
The problem comes from trying to use the brush to set the x-scale domain, when your x-scale is an ordinal scale. In other words, the expected domain of your x-axis is a list of categories, not a max-min numerical extent. So the problem is right at the top of the brushing function:
function brushed() {
main_x0.domain(brush.empty() ? mini_x0.domain() : brush.extent());
The domain set by brush.extent() is an array of two numbers, which then completely throws off your ordinal scale.
According to the wiki, if one of the scales attached to a brush function is an ordinal scale, the values returned by brush.extent() are values in the output range, not in the input domain. Ordinal scales don't have an invert() method to convert range values into domain values.
So, you have a few options on how to proceed:
You could re-do the whole graph using a linear time scale for your main x-axes instead of an ordinal scale. But then you have to write your own function to figure out the width of each day on that axis instead of being able to use .rangeBand().
You can create your own "invert" function to figure out which categorical values (dates on the mini_x0.domain) are included in the range returned by brush.extent(). Then you would have to both reset the main_x0.domain to only include those dates on the axis, and filter out your rectangles to only draw those rectangles.
Or you can leave the domain of main_x0. be, and change the range instead. By making the range of the graph larger, you space out the bars greater. In combination with a clipping path to cut off bars outside the plotting area, this has the effect of only showing a certain subset of bars, which is what you want anyway.
But what should the new range be? The range returned by brush.extent() is the beginning and end positions of the brushing rectangle. If you used these values as the range on the main graph, your entire graph would be squished down to just that width. That's the opposite of what you want. What you want is for the area of the graph that originally filled that width to be stretched to fill the entire plotting area.
So, if your original x range is from [0,100], and the brush covers the area [20,60], then you need a new range that satisfies these conditions:
the 20% mark of the new range width is at 0;
the 60% mark of the new range width is at 100.
Therefore,
the total width of the new range is ( (100-0) / (60-20) )*(100-0) = 250;
the start of the new range is at (0 - (20/100)*250) = -50;
the end of the new range is at (-50) + 250 = 200.
Now you could do all the algebra for figuring out this conversion yourself. But this is really just another type of scaling equation, so why not create a new scale function to convert between the old range and the zoomed-in range.
Specifically, we need a linear scale, with its output range set to be the actual range of the plotting area. Then set the domain according to the range of the brushed area that we want to stretch to cover the plotting area. Finally, we figure out the range of the ordinal scale by using the linear scale to figure out how far off the screen the original max and min values of the range would be. And from there, we-can resize the other ordinal scale and reposition all the rectangles.
In code:
//Initialization:
var main_xZoom = d3.scale.linear()
.range([0, main_width - 275])
.domain([0, main_width - 275]);
//Brushing function:
function brushed() {
var originalRange = main_xZoom.range();
main_xZoom.domain(brush.empty() ?
originalRange:
brush.extent() );
main_x0.rangeRoundBands( [
main_xZoom(originalRange[0]),
main_xZoom(originalRange[1])
], 0.2);
main_x1.rangeRoundBands([0, main_x0.rangeBand()], 0);
bar.selectAll("rect")
.attr("transform", function (d) {
return "translate(" + main_x0(d.date) + ",0)";
})
.attr("width", function (d) {
return main_x1.rangeBand();
})
.attr("x", function (d) {
return main_x1(d.portfolio);
});
main.select("g.x.axis").call(main_xAxis);
}
Working fiddle based on your simplified code (Note: you still need to set a clipping rectangle on the main plot):
http://fiddle.jshell.net/CjaD3/1/
i was trying to draw simple d3js graph.I got through drawing the axis and even plotted the data but the data isn't appearing where it is expected to be.
As per my json data below
var d1 = [{
value1: "30",
value2: "10"
}];
i'm trying to plot a circle at coordinates x axis 30 and y axis 10but the circle on the graph appears some where else.
Here is the jsfiddle demo
Here is my code
var d1 = [{
value1: "30",
value2: "10"
}];
function Update(){
var circles = vis.selectAll("circle").data(d1)
circles
.enter()
.insert("svg:circle")
.attr("cx", function (d) { return d.value1; })
.attr("cy", function (d) { return d.value2; })
.style("fill", "red")
circles
.transition().duration(1000)
.attr("cx", function (d) { return d.value1; })
.attr("cy", function (d) { return d.value2; })
.attr("r", function (d) { return 5; })
circles.exit ()
.transition().duration(1000)
.attr("r", 0)
.remove ();
}
/*************************************************/
/*******************Real Stuff starts here*******************/
var vis = d3.select("#visualisation"),
WIDTH = 600,
HEIGHT = 400,
MARGINS = {
top: 20,
right: 20,
bottom: 20,
left: 50
},
xRange = d3.scale.linear().range([MARGINS.left, WIDTH - MARGINS.right]).domain([0,100]),
yRange = d3.scale.linear().range([HEIGHT - MARGINS.top, MARGINS.bottom]).domain([0,300]),
xAxis = d3.svg.axis() // generate an axis
.scale(xRange) // set the range of the axis
.tickSize(5) // height of the ticks
.tickSubdivide(true), // display ticks between text labels
yAxis = d3.svg.axis() // generate an axis
.scale(yRange) // set the range of the axis
.tickSize(5) // width of the ticks
.orient("left") // have the text labels on the left hand side
.tickSubdivide(true); // display ticks between text labels
function init() {
vis.append("svg:g") // add a container for the axis
.attr("class", "x axis") // add some classes so we can style it
.attr("transform", "translate(0," + (HEIGHT - MARGINS.bottom) + ")") // move it into position
.call(xAxis); // finally, add the axis to the visualisation
vis.append("svg:g")
.attr("class", "y axis")
.attr("transform", "translate(" + (MARGINS.left) + ",0)")
.call(yAxis);
}
init();
$('#btn').click(function(){
Update();
});
It works if you
define the numbers as numbers and not as strings (i.e. value1: 30 instead of value1: "30") and
use the scales you define (i.e. return xRange(d.value1) instead of return d.value1).
Working jsfiddle here.
Your circle is appearing at pixel (30,10), but that doesn't correspond to the place 30,10 as labeled by your axes. Use your scales to set the point's location.
.attr("cx", function (d) { return xRange(d.value1); })
.attr("cy", function (d) { return yRange(d.value2); })
You will need to apply xScale and yScale to your coordinates to transform them into the plotting space.
See this jsFiddle
.attr("cx", function (d) { return xRange(d.value1); })
.attr("cy", function (d) { return yRange(d.value2); })
Actually it is working fine. It is just that top left corner is (0,0) and not bottom left (as I suspect, you must be assuming).
Set both x,y to 0. Circle will appear at top left corner.
I'm attempting to fill the area under the graph with different colors, depending on x value ranges, say for example, for x values 0 to 10 yellow, from 10 to 20 red and so on. Is there a way to do that?
My javascript for single fill color is
var m = 80;
var w = 900 - 3*m;
var h = 600- 3*m;
var x = d3.scale.linear().range([0, w]);
var y = d3.scale.linear().range([h, 0]);
x.domain(d3.extent(data, function(d) { return d.time; }));
y.domain(d3.extent(data, function(d) { return d.points; }));
var line = d3.svg.line()
.x(function(d) {
return x(d.time);
})
.y(function(d) {
return y(d.points);
})
var graph = d3.select("#graph").append("svg:svg")
.attr("width", w+3*m)
.attr("height", h+3*m)
.append("svg:g")
.attr("transform", "translate(" + 1.5*m + "," + 1.5*m + ")");
var area = d3.svg.area()
.x(function(d) { return x(d.time); })
.y0(h)
.y1(function(d) { return y(d.points); });
graph.append("path")
.datum(data)
.attr("class", "area")
.attr("d", area)
.style("fill","steelblue");
Thanks in advance!
You basically have two options for doing this.
You can define separate areas for the different colours.
You can define a single area and use a gradient to simulate different colours.
The second one is probably easier, as you don't need to draw any separate paths, you can simply fill the one like you're currently doing.
For the gradient, you would need to define the stops (i.e. colour changes) to correspond to the values. In particular, you would need to introduce two stops at the same place to make it appear like the colour is changing suddenly. More information on gradients here. The code would look something like this.
var grad = graph.append("defs")
.append("linearGradient")
.attr("id", "grad");
grad.append("stop").attr("offset", "0%").attr("stop-color", "yellow");
grad.append("stop").attr("offset", "10%").attr("stop-color", "yellow");
grad.append("stop").attr("offset", "10%").attr("stop-color", "red");
grad.append("stop").attr("offset", "20%").attr("stop-color", "red");
// etc
graph.append("path")
.style("fill", "url(#grad)");
The positions of the stops would be determined by your scale.