I'm looking for an example of how to draw a scatterplot in D3.js.
I haven't been able to find a simple example by looking through the official D3.js examples (impressive though they are). I just want to know how to:
draw and label the x- and y-axes
draw scatter points on the graph.
I did find this example in this D3 reusable library, but it is much more complex than I need, with external files that make it hard to pull out the essential points. Could anyone point me at a simple scatterplot example to get started?
Thanks very much.
This should get you started. You can see it in action at http://bl.ocks.org/2595950.
// data that you want to plot, I've used separate arrays for x and y values
var xdata = [5, 10, 15, 20],
ydata = [3, 17, 4, 6];
// size and margins for the chart
var margin = {top: 20, right: 15, bottom: 60, left: 60}
, width = 960 - margin.left - margin.right
, height = 500 - margin.top - margin.bottom;
// x and y scales, I've used linear here but there are other options
// the scales translate data values to pixel values for you
var x = d3.scale.linear()
.domain([0, d3.max(xdata)]) // the range of the values to plot
.range([ 0, width ]); // the pixel range of the x-axis
var y = d3.scale.linear()
.domain([0, d3.max(ydata)])
.range([ height, 0 ]);
// the chart object, includes all margins
var chart = d3.select('body')
.append('svg:svg')
.attr('width', width + margin.right + margin.left)
.attr('height', height + margin.top + margin.bottom)
.attr('class', 'chart')
// the main object where the chart and axis will be drawn
var main = chart.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
.attr('width', width)
.attr('height', height)
.attr('class', 'main')
// draw the x axis
var xAxis = d3.svg.axis()
.scale(x)
.orient('bottom');
main.append('g')
.attr('transform', 'translate(0,' + height + ')')
.attr('class', 'main axis date')
.call(xAxis);
// draw the y axis
var yAxis = d3.svg.axis()
.scale(y)
.orient('left');
main.append('g')
.attr('transform', 'translate(0,0)')
.attr('class', 'main axis date')
.call(yAxis);
// draw the graph object
var g = main.append("svg:g");
g.selectAll("scatter-dots")
.data(ydata) // using the values in the ydata array
.enter().append("svg:circle") // create a new circle for each value
.attr("cy", function (d) { return y(d); } ) // translate y value to a pixel
.attr("cx", function (d,i) { return x(xdata[i]); } ) // translate x value
.attr("r", 10) // radius of circle
.style("opacity", 0.6); // opacity of circle
Used like this:
<!DOCTYPE html>
<html>
<head>
<title>The d3 test</title>
<script type="text/javascript" src="http://mbostock.github.com/d3/d3.v2.js" charset="utf-8"></script>
</head>
<body>
<div class='content'>
<!-- /the chart goes here -->
</div>
<script type="text/javascript" src="scatterchart.js"></script>
</body>
</html
NVD3.js has great examples. You will need to include their library also or take a look at their implementation. Take a look at this example of scatterplot: http://nvd3.org/livecode/#codemirrorNav
Take a look at this example of scatterplot with C3.js (D3-based): http://c3js.org/samples/chart_scatter.html
You can chang radius size like this : http://grokbase.com/t/gg/c3js/14a7jfj28c/bubble-chart-with-changing-radius-size-in-c3-js
Related
I am trying to make a multi-line chart with d3.js in react. The plot looks fine and comes up well, but the gridlines are not aligned sometimes. It is very random, and sometimes some graphs have aligned gridlines, some don't.
This is how some of them look:
I have this code for my gridlines:
svg
.append('g')
.attr('class', 'grid')
.attr('transform', `translate(0,${height})`)
.call(
d3.axisBottom(xScale)
.tickSize(-height)
.tickFormat(() => ""),
);
svg
.append('g')
.attr('class', 'grid')
.call(
d3.axisLeft(yScale)
.tickSize(-width)
.tickFormat(() => ""),
);
I followed this example: https://betterprogramming.pub/react-d3-plotting-a-line-chart-with-tooltips-ed41a4c31f4f
Any help on how I can align those lines perfectly would be appreciated.
You may consider niceing your y-scale so that minima and maxima of your data sets are rounded down/ up such that the ticks are equally spaced.
In your tutorial this bit of code:
const yScale = d3
.scaleLinear()
.range([height, 0])
.domain([0, yMaxValue]);
Can become:
const yScale = d3
.scaleLinear()
.range([height, 0])
.domain([0, yMaxValue])
.nice(); // <--------------------------- here
Here's a basic example of using nice on an x-scale where the first example is 'not nice' and the second is 'nice'.
// note gaps of 10 between data points
// apart from first and last where gap is different
const data = [3, 4, 14, 24, 34, 44, 47];
// svg
const margin = 20;
const width = 360;
const height = 140;
const svg = d3.select("body")
.append("svg")
.attr("width", width + margin + margin)
.attr("height", height + margin + margin);
// scale without 'nice'
const xScale1 = d3.scaleLinear()
.range([0, width])
.domain(d3.extent(data));
// scale with nice
const xScale2 = d3.scaleLinear()
.range([0, width])
.domain(d3.extent(data))
.nice();
// plot axes with both scales for comparison
// not 'nice'
svg.append("g")
.attr("transform", `translate(${margin},${margin})`)
.call(d3.axisBottom(xScale1));
// 'nice'
svg.append("g")
.attr("transform", `translate(${margin},${margin + 50})`)
.call(d3.axisBottom(xScale2));
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
I'm working on simple horizontal bar chart using vue and d3. I wanna customize my axis labels along vertical axis. Now my code where I specify what to put in every label is looking like this:
let yAxis = d3.axisLeft()
.scale(this.yScale)
.tickSize(0)
.tickPadding(4)
.tickFormat((d, i) => { return this.data[i].country + " " + this.data[i].value })
I'd like my labels to look this way:
country
value
Country1
10.2
Country2
200.3
Country2
3000.4
i.e. country must be aligned left, while value must be right. Besides value must be in bold. The problem is though that it seems as if .tickFormat doesn't accept any html tags
Here is an example of styling the axis labels:
const svg = d3.select("svg")
.attr("width", 1000)
// Create the scale
const x = d3.scaleLinear()
.domain([0, 100]) // This is what is written on the Axis: from 0 to 100
.range([100, 800]); // This is where the axis is placed: from 100px to 800px
// Draw the axis
svg
.append("g")
.classed('x-axis', true)
.attr("transform", "translate(0,50)") // This controls the vertical position of the Axis
.call(d3.axisBottom(x));
svg.select('.x-axis')
.selectAll('text')
.style('fill', 'red')
.style('font-size', '32px')
.attr('y', 20)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>
I am trying to build the d3 chart with the positive and negative number values as below
and I found some examples of this and this. I am facing difficulties in customizing it because I have no prior experience in d3 and I think it would need some time for learning. I tried that as well. Created some simple chart examples but could not achieve the above. So I thought of reaching for help. Maybe someone can help with this if they have already done a similar chart or some guidance would be greatly appreciated. Thanks in advance.
The first step would be to identify how this chart can be simplified. Removing features until the most basic thing remains. Then, build that and gradually add features until it resembles what you want.
In your case, that'd be a horizontal bar chart. Then, add some negative values and a centred zero-line. Finally, make the height of the bars less so they become nodes, and add the text.
I'll try to add something like this, in these steps, without the layout and everything, but hopefully you'll be able to see my logic.
The basic vertical bar chart
// Some fake data
const data = ['SaaS', 'Sales', 'Fruits & Veggies', 'IT'].map((v, i) => ({
name: v,
value: 3 * i + 2
}));
const width = 600,
height = 300
margin = {
top: 20,
left: 100,
right: 40,
bottom: 40
};
// Process it to find the x and y axis domains
// scaleLinear because it considers numbers
const x = d3.scaleLinear()
.domain([0, d3.max(data.map(d => d.value))]) // the possible values
.range([0, width]); // the available screen space
// scaleBand because it's just categorical data
const y = d3.scaleBand()
.domain(data.map(d => d.name)) // all possible values
.range([height, 0]) // little weird, y-axis is always backwards, because (0,0) is the top left
.padding(0.1);
const svg = d3.select('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom);
const g = svg
// Append a container element. This will hold the chart
.append('g')
// Move it a little to account for the axes and labels
.attr('transform', `translate(${margin.left} ${margin.right})`);
// Draw the bars
// First, assign the data to the bar objects, this will decide which to remove, update, and add
const bars = g.append('g')
.selectAll('rect')
.data(data);
// Good practice: always call remove before adding stuff
bars.exit().remove();
// Add the new bars and assign any attributes that do not depend on the data
// for example, font for texts
bars.enter()
.append('rect')
.attr('fill', 'steelblue')
// Now merge it with the existing bars
.merge(bars)
// From now on we operate on both the old and the new bars
// Bars are weird, first we position the top left corner of each bar
.attr('x', 0)
.attr('y', d => y(d.name))
// Then we determine the width and height
.attr('width', d => x(d.value))
.attr('height', y.bandwidth())
// Draw the x and y axes
g.append('g')
.classed('x-axis', true)
.attr('transform', `translate(0, ${height})`)
.call(d3.axisBottom(x))
g.append('g')
.classed('y-axis', true)
.call(d3.axisLeft(y))
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>
Now I'll remove all old comments and explain what I'm doing differently.
The negative horizontal bar chart
// Now, the data can also be negative
const data = ['SaaS', 'Sales', 'Fruits & Veggies', 'IT'].map((v, i) => ({
name: v,
value: 3 * i - 5
}));
const width = 600,
height = 300,
margin = {
top: 20,
left: 100,
right: 40,
bottom: 40
};
// Now, we don't use 0 as a minimum, but get it from the data using d3.extent
const x = d3.scaleLinear()
.domain(d3.extent(data.map(d => d.value)))
.range([0, width]);
const y = d3.scaleBand()
.domain(data.map(d => d.name))
.range([height, 0])
.padding(0.1);
const svg = d3.select('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom);
const g = svg
.append('g')
.attr('transform', `translate(${margin.left} ${margin.right})`);
const bars = g.append('g')
.selectAll('rect')
.data(data);
bars.exit().remove();
bars.enter()
.append('rect')
.merge(bars)
// All the same until here
// Now, if a bar is positive it starts at x = 0, and has positive width
// If a bar is negative it starts at x < 0 and ends at x = 0
.attr('x', d => d.value > 0 ? x(0) : x(d.value))
.attr('y', d => y(d.name))
// If the bar is positive it ends at x = v, but that means it's x(v) - x(0) wide
// If the bar is negative it ends at x = 0, but that means it's x(0) - x(v) wide
.attr('width', d => d.value > 0 ? x(d.value) - x(0) : x(0) - x(d.value))
.attr('height', y.bandwidth())
// Let's color the bar based on whether the value is positive or negative
.attr('fill', d => d.value > 0 ? 'darkgreen' : 'darkred')
g.append('g')
.classed('x-axis', true)
.attr('transform', `translate(0, ${height})`)
.call(d3.axisBottom(x))
g.append('g')
.classed('y-axis', true)
.call(d3.axisLeft(y))
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>
And now, I'll change the bars to the nodes you have in your example code.
The horizontal chart with nodes
const data = ['SaaS', 'Sales', 'Fruits & Veggies', 'IT'].map((v, i) => ({
name: v,
value: 3 * i - 5
}));
// We want to center each rect around the value it's supposed to have.
// That means that we need to have a node width
const nodeWidth = 60;
const width = 600,
height = 300,
margin = {
top: 20,
left: 100,
right: 40,
bottom: 40
};
// We also need to make sure there is space for all nodes, even at the edges.
// One way to get this is by just extending the domain a little.
const domain = d3.extent(data.map(d => d.value));
const x = d3.scaleLinear()
.domain([domain[0] - 1.5, domain[1] + 1.5])
.range([0, width]);
const y = d3.scaleBand()
.domain(data.map(d => d.name))
.range([height, 0])
.padding(0.1);
const svg = d3.select('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom);
const g = svg
.append('g')
.attr('transform', `translate(${margin.left} ${margin.right})`);
const bars = g.append('g')
.selectAll('rect')
.data(data);
bars.exit().remove();
// All the same until here
bars.enter()
.append('rect')
// width has become a constant
.attr('width', nodeWidth)
// Now, transform each node so it centers around the value it's supposed to have
.attr('transform', `translate(${-nodeWidth / 2} 0)`)
// Round the corners for aesthetics
.attr('rx', 15)
.merge(bars)
// `x` denotes the placement directly again
.attr('x', d => x(d.value))
.attr('y', d => y(d.name))
.attr('height', y.bandwidth())
.attr('fill', d => d.value > 0 ? 'darkgreen' : 'darkred');
// Now one more thing, we want to add labels to each node.
// `<rect>` can't have children, we we add them to the plot seperately
// using the same `data` as for the bars
const labels = g.append('g')
.selectAll('text')
.data(data);
labels.exit().remove();
labels.enter()
.append('text')
.attr('fill', 'white')
.attr('text-anchor', 'middle') // center-align the text
.attr('dy', 5) // place it down a little so it middles nicely in the node.
.merge(bars)
// `x` denotes the placement directly
.attr('x', d => x(d.value))
// Add half a bar's height to target the center of each node
.attr('y', d => y(d.name) + y.bandwidth() / 2)
// Actually fill in the text
.text(d => d.value);
g.append('g')
.classed('x-axis', true)
.attr('transform', `translate(0, ${height})`)
.call(d3.axisBottom(x))
g.append('g')
.classed('y-axis', true)
.call(d3.axisLeft(y))
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>
I hope you can follow this. Let me know if anything about this tutorial is unclear.
I am making a line graph for a set of data regarding letter vs frequency. I have made proper code for x and y axis, but while plotting line I am getting error and not able to plot the line-graph. Can someone help fix the issue?
SNIPPET:
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.12/angular.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.3.0/d3.min.js"></script>
</head>
<body ng-app="myApp" ng-controller="myCtrl">
<svg></svg>
<script>
//module declaration
var app = angular.module('myApp',[]);
//Controller declaration
app.controller('myCtrl',function($scope){
$scope.svgWidth = 800;//svg Width
$scope.svgHeight = 500;//svg Height
//Data in proper format
var data = [
{"letter": "A","frequency": "5.01"},
{"letter": "B","frequency": "7.80"},
{"letter": "C","frequency": "15.35"},
{"letter": "D","frequency": "22.70"},
{"letter": "E","frequency": "34.25"},
{"letter": "F","frequency": "10.21"},
{"letter": "G","frequency": "7.68"},
];
//removing prior svg elements ie clean up svg
d3.select('svg').selectAll("*").remove();
//resetting svg height and width in current svg
d3.select("svg").attr("width", $scope.svgWidth).attr("height", $scope.svgHeight);
//Setting up of our svg with proper calculations
var svg = d3.select("svg");
var margin = {top: 20, right: 20, bottom: 30, left: 40};
var width = svg.attr("width") - margin.left - margin.right;
var height = svg.attr("height") - margin.top - margin.bottom;
//Plotting our base area in svg in which chart will be shown
var g = svg.append("g");
//shifting the canvas area from left and top
g.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
//X and Y scaling
var x = d3.scaleLinear().rangeRound([0, width]);
var y = d3.scaleBand().rangeRound([height, 0]).padding(0.4);
//Feeding data points on x and y axis
x.domain([0, d3.max(data, function(d) { return +d.frequency; })]);
y.domain(data.map(function(d) { return d.letter; }));
//Final Plotting
//for x axis
g.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
//for y axis
g.append("g")
.call(d3.axisLeft(y))
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", "0.71em")
.attr("text-anchor", "end");
//the line function for path
var lineFunction = d3.line()
.x(function(d) {return xScale(d.x); })
.y(function(d) { return yScale(d.y); })
.curve(d3.curveLinear);
//defining the lines
var path = g.append("path");
//plotting lines
path
.attr("d", lineFunction(data))
.attr("stroke", "blue")
.attr("stroke-width", 2)
.attr("fill", "none");
});
</script>
</body>
</html>
ERROR:
NEW ERROR:
Look at the console: you don't have a xScale or a yScale.
So, the line generator should be:
var lineFunction = d3.line()
.x(function(d) {return x(d.frequency); })
.y(function(d) { return y(d.letter); })
.curve(d3.curveLinear);
Besides that, frequency is a string, not a number. So, it's a good idea turning it into a number. Write this right after your data variable:
data.forEach(function(d){
d.frequency = +d.frequency;
});
Note: it's a good practice defining your variable names properly, with descriptive names, like xScale, yAxis, chartLegend or formatNumber... Look at your line generator: you have two different x in a single line. If you don't take care, you'll mix them.
If you want to use xScale and yScale , you need to define these functions. Syntax is given below (ignore values):
Below code is for d3 version 3
me.xscale = d3.scale.linear() // for horizontal distances if using for 2D
.domain([0, 500])
.range([0, 700]);
me.yscale = d3.scale.linear() // for vertical distances if using for 2D
.domain([0, 600])
.range([0, 200]);
These functions are used to define mapping of a values in one range to values in other range.
e.g - Suppose you want draw a graph on your browser screen. And you want assume that width 500px on your browser screen should be counted as 500 on your graph.
You need to define xScale as above . In this case , this function will map every value in domain (0-500) to unique value in range (0-700) and vice versa.
I'm trying to make a scatterplot that appears below an interactive worldmap with the help of D3. The scatterplot contains data from the country that the user clicked on in the worldmap. The problem is that when the user clicks on another country, the scatterplot of the previous country should disappear. This is not the case unfortunately, the second scatterplot just appears under the first scatterplot. Does anyone know how I can fix this? Any help would be greatly appreciated.
A part of the code I use for the scatterplot:
function ScatterCorruption(dataset, title){
var xValue = function(d) { return d.GDP;}
var yValue = function(d) { return d.Variable;}
// determine parameters
var margin = {top: 20, right: 20, bottom: 200, left: 70},
width = 600 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
// formatters for axis and labels
var currencyFormat = d3.format("0.2f");
var decimalFormat = d3.format("0.2f");
// determine x scale
var x = d3.scale.linear()
.range([0, width]);
// determine y scale
var y = d3.scale.linear()
.range([height, 0]);
// determine x-axis
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
// determine y-axis
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
// make svg
var svg = d3.select("body").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 + ")");
// add the tooltip area to the webpage
var tooltip = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
console.log(dataset)
// load in data
d3.tsv(dataset, function(error, data) {
if (error) throw error;
// convert data
data.forEach(function(d) {
d.GDP = +d.GDP;
d.Variable = +d.Variable;
});
You'll need to call this before rendering a new scatterplot: d3.selectAll("svg > *").remove(); so that your svg is clear again. Alternatively you can also do d3.select("svg").remove(); and then recreate the svg.