Suppose I create a bar chart from some data that I have. This bar chart is generated on a set of x- and y- axes. How can I plot a function (in the form y=f(x)) as a line over that same set of axes? I want to do this so that you can easily compare the trends shown by the bar chart and the function.
You just generate the data to feed to d3 by running the function over the desired values of the domain and then use for example line genrators to draw the line.
var w = 400;
var h = 400;
var padding = 50;
var svg = d3.select('svg').attr('width', w + padding * 2).attr('height', h + padding * 2);
var xScale = d3.scale.linear().domain([-10, 10]).range([0, w]);
var yScale = d3.scale.linear().domain([2, -2]).range([0, h]);
var xAxis = d3.svg.axis().scale(xScale);
var yAxis = d3.svg.axis().scale(yScale).orient("left");
var g = svg.append("g").attr("transform", "translate(" + padding + "," + padding + ")");
g.append("g").attr("transform", "translate(0, " + w / 2 + ")").call(xAxis);
g.append("g").attr("transform", "translate(" + h / 2 + ", 0)").call(yAxis);
var line = d3.svg.line().interpolate("basis")
.x(function(d, i) {
return xScale(d.x);
})
.y(function(d, i) {
return yScale(d.y);
})
g.append('path')
.data([fn1()])
.attr("d", line);
g.append('path')
.data([fn2()])
.attr("d", line);
g.append('path')
.data([fn3()])
.attr("d", line);
g.append('path')
.data([fn4()])
.attr("d", line);
function fn1() {
function f(x) {
return Math.cos(x);
}
return _.chain(_.range(-10, 10)).map(function(x) {
return {
y: f(x),
x: x
};
}).value();
}
function fn2() {
function f(x) {
return Math.sin(x);
}
return _.chain(_.range(-10, 10)).map(function(x) {
return {
y: f(x),
x: x
};
}).value();
}
function fn3() {
function f(x) {
return Math.exp(x);
}
return _.chain(_.range(-10, 10)).map(function(x) {
return {
y: f(x),
x: x
};
}).value();
}
function fn4() {
function f(x, m, b) {
return m * x + b;
}
return _.chain(_.range(-10, 10)).map(function(x) {
return {
y: f(x, 1, 1),
x: x
};
}).value();
}
path {
stroke: black;
fill: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<body>
<svg></svg>
</body>
I am making use of D3 and Math.js.
The reason I am using Math.js is that the function can be very complex
from:
4 * sin(x) + 5 * tan(x/2)
4 * sin(x^2) + 5 * cos(x/2)^3
Anything but f(x) :)
You can define your any domain of your choice here:
//define xDomain change it domain of your choice
var xDomain = [-10, 10];
//define yDomain change it domain of your choice
var yDomain = [-10, 10];
Then make the conventional bar chart for the domain nothing special this is a standard D3 code for making bar charts.
//make the bar chart by loading the tsv
d3.tsv("data.tsv", type, function(error, data) {
if (error) throw error;
//make x axis
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + y(0) + ")")
.call(xAxis);
//make y axis
svg.append("g")
.attr("class", "y axis")
.attr("transform", "translate(" + x(0) + ",0)")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Frequency");
//make bar chart rectangle
svg.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("x", function(d) {
return x(d.letter) - 10;
})
.attr("width", 20)
.attr("y", function(d) {
return y(d.frequency);
})
.attr("height", function(d) {
return height / 2 - y(d.frequency);
});
makeLineFunction();
});
This is my function which will make the line chart based on the value entered in the text box(for the xdomain):
function makeLineFunction() {
//remove any line if present
d3.selectAll(".line").remove();
//make an array of all x points
var xArray = d3.range(xDomain[0], xDomain[1], 0.5);
//get the formula from the text box above
var value = d3.select("#function_text").node().value;
//generate the data using math.js
var data = xArray.map(function(x1) {
return {
x: x1,
//using math.js for evaluating y point for the given x
y: math.eval(value, {
x: x1
})
};
});
//make the path
svg.append("path")
.datum(data)
.attr("class", "line")
.attr("d", line);
}
Working code here, necessary comments added for help.
Hope this helps!
Here is an example which illustrate the line chart over bar chat. You can generate y=f(x) values using jsp or php at run time.
update the data part
series: [
{
title: 'Column',
type: 'column',
axisY: 'y1',
data: [['A', 1], ['B', 4], ['C', 3],
['D', 5], ['E', 2], ['F', 1]]
},
{
title: 'Line',
type: 'line',
axisY: 'y2',
data: [['A', 40], ['B', 60], ['C', 62],
['D', 52], ['E', 70], ['F', 75]]
}
]
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>
Multiple Axes Example
</title>
<link rel="stylesheet" type="text/css" href="http://www.jqchart.com/css/jquery.jqChart.css" />
<link rel="stylesheet" type="text/css" href="http://ajax.aspnetcdn.com/ajax/jquery.ui/1.8.21/themes/smoothness/jquery-ui.css" />
<script src="http://www.jqchart.com/js/jquery-1.11.1.min.js" type="text/javascript"></script>
<script src="http://www.jqchart.com/js/jquery.mousewheel.js" type="text/javascript"></script>
<script src="http://www.jqchart.com/js/jquery.jqChart.min.js" type="text/javascript"></script>
<script src="http://www.jqchart.com/js/jquery.jqRangeSlider.min.js" type="text/javascript"></script>
<script lang="javascript" type="text/javascript">
$(document).ready(function () {
var background = {
type: 'linearGradient',
x0: 0,
y0: 0,
x1: 0,
y1: 1,
colorStops: [{ offset: 0, color: '#d2e6c9' },
{ offset: 1, color: 'white' }]
};
$('#jqChart').jqChart({
title: { text: 'Multiple Axes' },
border: { strokeStyle: '#6ba851' },
background: background,
animation: { duration: 1 },
shadows: {
enabled: true
},
axes: [
{
name: 'y1',
location: 'left'
},
{
name: 'y2',
location: 'right',
strokeStyle: '#FCB441',
majorGridLines: {
strokeStyle: '#FCB441'
},
majorTickMarks: {
strokeStyle: '#FCB441'
}
}
],
series: [
{
title: 'Column',
type: 'column',
axisY: 'y1',
data: [['A', 1], ['B', 4], ['C', 3],
['D', 5], ['E', 2], ['F', 1]]
},
{
title: 'Line',
type: 'line',
axisY: 'y2',
data: [['A', 40], ['B', 60], ['C', 62],
['D', 52], ['E', 70], ['F', 75]]
}
]
});
});
</script>
</head>
<body>
<div>
<div id="jqChart" style="width: 500px; height: 300px;"></div>
</div>
</body>
</html>
Related
I am trying to make a horizontal stacked bar chart, starting with this code snippet, updating to d3 v7. Instead of getting a neatly stacked bar chart, each subsequent bar in a stack is getting offset vertically down from where it should be. When I inspect the yScale value, I get the expected value, so I'm extra-confused about this behavior.
I'd include just the relevant piece of the puzzle, but I honestly don't know where my problem is -- am I appending to the wrong 'g' element? Using enter() on the wrong piece of data?
<script src="https://d3js.org/d3.v7.min.js"></script>
<body>
<div id="bar_chart">
<script>
var data = [{
dep_time: "5:30",
risk: 100,
details: [{
time: 19,
source: 'Drive'
},
{
time: 10,
source: 'Margin'
},
{
time: 42,
source: 'Full'
},
{
time: 35,
source: 'Crossing'
},
{
time: 23,
source: 'Drive'
}
]
},
{
dep_time: "6:20",
risk: 80,
details: [{
time: 25,
source: 'Drive'
},
{
time: 1,
source: 'Margin'
},
{
time: 38,
source: 'Full'
},
{
time: 35,
source: 'Crossing'
},
{
time: 25,
source: 'Drive'
}
]
},
{
dep_time: "7:10",
risk: 5,
details: [{
time: 8,
source: 'Drive'
},
{
time: 28,
source: 'Margin'
},
{
time: 38,
source: 'Full'
},
{
time: 35,
source: 'Crossing'
},
{
time: 18,
source: 'Drive'
}
]
}
];
var chartContainer = '.chart-container';
var units = [];
var xMax = 0;
data.forEach(function(s) {
var total = 0;
s.details.forEach(function(s) {
s["x0"] = total; //Abs left
s["x"] = s.time; //Width
s["x1"] = total + s.time; //Abs right
total = total + s.time;
if (total > xMax) xMax = total;
});
s["y"] = s.dep_time;
units.push(s.dep_time);
});
//Need it to look like: newdata = [(Drive) [19, 25, 32.] Margin [0, 1, 28]. Full [42, 38, 38]. Crossing [35, 35, 35]. Drive [23, 25, 18].]
//So it's a row in the array for each column of data.
//re-arrange the data so it makes more sense to d3 (and less sense to any sane human)
var newdata = [];
for (var i = 0; i < data[0].details.length; i++) {
var row = [];
data.forEach(function(s) {
row.push({
x: s.details[i].x,
y: s.dep_time,
x0: s.details[i].x0
});
});
newdata.push(row);
}
console.log("newdata");
console.log(newdata);
var margins = {
left: 50,
bottom: 50,
top: 25,
right: 25
};
var sizes = {
width: 500,
height: 150
};
var width = sizes.width - margins.left - margins.right;
var height = sizes.height - margins.bottom - margins.top;
var svg = d3.select("#bar_chart")
.append('svg')
.attr('width', width + margins.left + margins.right)
.attr('height', height + margins.bottom)
.append('g')
.attr('transform', 'translate(' + margins.left + ', ' + margins.top + ")");
var yScale = d3.scaleBand()
.domain(units)
.rangeRound([0, height]);
var yAxis = d3.axisLeft(yScale);
var yAxisG = svg.append("g")
.attr("transform", "translate(0,0)")
.attr("id", "yaxis")
.call(yAxis);
const xScale = d3.scaleLinear()
.domain([0, xMax])
.range([0, width]);
var xAxis = d3.axisBottom(xScale);
var xAxisG = svg.append("g")
.attr("transform", "translate(0, " + height + ")")
.attr("id", "xaxis")
.call(xAxis
.ticks(8));
var bar_colors = ['red', 'purple', 'green', 'lightblue', 'yellow'];
var colors = function(i) {
return bar_colors[i];
}
var groups = svg.selectAll('g')
.data(newdata)
//.exit()
.append('g')
.style('fill', function(d, i) {
console.log("d");
console.log(d);
//console.log("i"); console.log(i);
return colors(i);
});
groups.selectAll('rect')
.data(function(d) {
//console.log(d);
return d;
})
.enter()
.append('rect')
.attr('x', function(d) {
//console.log("x0"); console.log(d.x0);
return xScale(d.x0);
})
.attr('y', function(d, i) {
//console.log(yScale(d.y));
//console.log(i);
return yScale(d.y);
})
.attr('height', 10) //function (d) {return yScale.rangeBand();})
.attr('width', function(d) {
return xScale(d.x);
});
</script>
</div>
</body>
You are appending the rectangles to existing translated groups (the axes) because of this:
var groups = svg.selectAll("g")
Instead, select nothing (and also remember to enter the selection):
var groups = svg.selectAll(null)
Here's your code with that change:
<script src="https://d3js.org/d3.v7.min.js"></script>
<body>
<div id="bar_chart">
<script>
var data = [{
dep_time: "5:30",
risk: 100,
details: [{
time: 19,
source: 'Drive'
},
{
time: 10,
source: 'Margin'
},
{
time: 42,
source: 'Full'
},
{
time: 35,
source: 'Crossing'
},
{
time: 23,
source: 'Drive'
}
]
},
{
dep_time: "6:20",
risk: 80,
details: [{
time: 25,
source: 'Drive'
},
{
time: 1,
source: 'Margin'
},
{
time: 38,
source: 'Full'
},
{
time: 35,
source: 'Crossing'
},
{
time: 25,
source: 'Drive'
}
]
},
{
dep_time: "7:10",
risk: 5,
details: [{
time: 8,
source: 'Drive'
},
{
time: 28,
source: 'Margin'
},
{
time: 38,
source: 'Full'
},
{
time: 35,
source: 'Crossing'
},
{
time: 18,
source: 'Drive'
}
]
}
];
var chartContainer = '.chart-container';
var units = [];
var xMax = 0;
data.forEach(function(s) {
var total = 0;
s.details.forEach(function(s) {
s["x0"] = total; //Abs left
s["x"] = s.time; //Width
s["x1"] = total + s.time; //Abs right
total = total + s.time;
if (total > xMax) xMax = total;
});
s["y"] = s.dep_time;
units.push(s.dep_time);
});
//Need it to look like: newdata = [(Drive) [19, 25, 32.] Margin [0, 1, 28]. Full [42, 38, 38]. Crossing [35, 35, 35]. Drive [23, 25, 18].]
//So it's a row in the array for each column of data.
//re-arrange the data so it makes more sense to d3 (and less sense to any sane human)
var newdata = [];
for (var i = 0; i < data[0].details.length; i++) {
var row = [];
data.forEach(function(s) {
row.push({
x: s.details[i].x,
y: s.dep_time,
x0: s.details[i].x0
});
});
newdata.push(row);
}
var margins = {
left: 50,
bottom: 50,
top: 25,
right: 25
};
var sizes = {
width: 500,
height: 150
};
var width = sizes.width - margins.left - margins.right;
var height = sizes.height - margins.bottom - margins.top;
var svg = d3.select("#bar_chart")
.append('svg')
.attr('width', width + margins.left + margins.right)
.attr('height', height + margins.bottom)
.append('g')
.attr('transform', 'translate(' + margins.left + ', ' + margins.top + ")");
var yScale = d3.scaleBand()
.domain(units)
.rangeRound([0, height]);
var yAxis = d3.axisLeft(yScale);
var yAxisG = svg.append("g")
.attr("transform", "translate(0,0)")
.attr("id", "yaxis")
.call(yAxis);
const xScale = d3.scaleLinear()
.domain([0, xMax])
.range([0, width]);
var xAxis = d3.axisBottom(xScale);
var xAxisG = svg.append("g")
.attr("transform", "translate(0, " + height + ")")
.attr("id", "xaxis")
.call(xAxis
.ticks(8));
var bar_colors = ['red', 'purple', 'green', 'lightblue', 'yellow'];
var colors = function(i) {
return bar_colors[i];
}
var groups = svg.selectAll(null)
.data(newdata)
.enter()
.append('g')
.style('fill', function(d, i) {
return colors(i);
});
groups.selectAll('rect')
.data(function(d) {
//console.log(d);
return d;
})
.enter()
.append('rect')
.attr('x', function(d) {
return xScale(d.x0);
})
.attr('y', function(d, i) {
return yScale(d.y);
})
.attr('height', 10) //function (d) {return yScale.rangeBand();})
.attr('width', function(d) {
return xScale(d.x);
});
</script>
</div>
</body>
How do I achieve this with D3? desired output
It's easy to have two layers of pie charts https://embed.plnkr.co/plunk/2p0zmp
Or to use d3 network with graph and nodes, http://using-d3js.com/05_08_links.html
but how could I overlay the concept of "nodes" and "links" onto these arcs of a piechart?
What kind of data structure is preferred?
{
nodes: [
{
layer: 1,
data: [
{name: A },
{name: B },
{name: C },
{name: D }
]
},
{
layer: 2,
data: [
{name: E },
{name: F },
{name: G }
]
}
],
links: [{ source: 'B', target: 'E'}, { source: 'D', target: 'F'}]
}
This gets pretty close with what you're looking for. You can use some additional arc generators and arc.centroid() to get the positions for the start and ends of the links. Then you can use a link generator to draw the links. One drawback of this is that the links can overlap the nodes.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="https://d3js.org/d3.v7.js"></script>
</head>
<body>
<div id="chart"></div>
<script>
/*
set up
*/
const width = 700;
const height = 400;
const svg = d3.select('#chart')
.append('svg')
.attr('width', width)
.attr('height', height);
const g = svg.append('g')
.attr('transform', `translate(${width / 2},${height})`);
/*
data
*/
const level1Nodes = [
{ name: 'A', value: 50, level: 1, color: 'RoyalBlue' },
{ name: 'B', value: 50, level: 1, color: 'DarkOrange' },
{ name: 'C', value: 50, level: 1, color: 'DarkOrange' },
{ name: 'D', value: 30, level: 1, color: 'Gold' }
];
const level2Nodes = [
{ name: 'E', value: 75, level: 2, color: 'RoyalBlue' },
{ name: 'F', value: 75, level: 2, color: 'DarkOrange' },
{ name: 'G', value: 30, level: 2, color: 'RoyalBlue' },
];
const links = [
{ source: 'B', target: 'E'},
{ source: 'D', target: 'F'}
];
/*
pie generator
*/
const pie = d3.pie()
.value(d => d.value)
.startAngle(-Math.PI / 2)
.endAngle(Math.PI / 2)
.padAngle(Math.PI / 45);
// calculate the angles for the slices of the nodes
const slices = [
...pie(level1Nodes),
...pie(level2Nodes)
];
/*
arcs
*/
const level1InnerRadius = 130;
const level1OuterRadius = 200;
const level2InnerRadius = 270;
const level2OuterRadius = 340;
// for drawing the nodes
const level1Arc = d3.arc()
.innerRadius(level1InnerRadius)
.outerRadius(level1OuterRadius);
const level2Arc = d3.arc()
.innerRadius(level2InnerRadius)
.outerRadius(level2OuterRadius);
const levelToArc = new Map([
[1, level1Arc],
[2, level2Arc]
]);
// for positioning the links along the outside
// of the level 1 nodes and the inside of the
// level 2 nodes
const level1OuterArc = d3.arc()
.innerRadius(level1OuterRadius)
.outerRadius(level1OuterRadius);
const level2InnerArc = d3.arc()
.innerRadius(level2InnerRadius)
.outerRadius(level2InnerRadius);
/*
calculating position of links
*/
// Map from the name of a node to the data for its arc
const nameToSlice = d3.index(slices, d => d.data.name);
// get the start and end positions for each link
const linkPositions = links.map(({source, target}) => ({
source: level1OuterArc.centroid(nameToSlice.get(source)),
target: level2InnerArc.centroid(nameToSlice.get(target)),
}));
/*
drawing
*/
// nodes
g.append('g')
.selectAll('path')
.data(slices)
.join('path')
.attr('d', d => levelToArc.get(d.data.level)(d))
.attr('fill', d => d.data.color);
// node labels
const labelsGroup = g.append('g')
.attr('font-family', 'sans-serif')
.attr('font-weight', 'bold')
.attr('font-size', 30)
.selectAll('text')
.data(slices)
.join('text')
.attr('dominant-baseline', 'middle')
.attr('text-anchor', 'middle')
.attr('transform', d => `translate(${levelToArc.get(d.data.level).centroid(d)})`)
.text(d => d.data.name);
// links
g.append('g')
.selectAll('path')
.data(linkPositions)
.join('path')
.attr('d', d3.linkVertical())
.attr('fill', 'none')
.attr('stroke', 'DarkBlue')
.attr('stroke-width', 2);
// circles at the end of links
g.append('g')
.selectAll('circle')
.data(linkPositions.map(({source, target}) => [source, target]).flat())
.join('circle')
.attr('r', 5)
.attr('fill', 'DarkBlue')
.attr('transform', d => `translate(${d})`);
</script>
</body>
</html>
This question builds on this question.
Using d3.js/dc.js, I have three (or more) charts. All have the same x-axis (a date series), so the nth datapoint on any chart will correspond exactly to the nth datapoint on the x-axis of the other charts.
When the user clicks on a dot point in one chart, I need to get the "y" data from the same point on the other 2+ charts and return an array or object or string with the chartID/y-datum from the other charts, something like this:
{"chart1":"30","chart2":"50","chart3":"10"}
Here is an example borrowed from Gerardo Furtado's answer to the above-referenced question. How would I modify Gerardo's example to return the datapoints from each chart?
var data = [{x:20, y:30},
{x:30, y:60},
{x:40, y:40},
{x:50, y:90},
{x:60, y:20},
{x:70, y:90},
{x:80, y:90},
{x:90, y:10}];
draw("#svg1");
draw("#svg2");
draw("#svg3");
function draw(selector){
var width = 250,
height = 250;
var svg = d3.select(selector)
.append("svg")
.attr("width", width)
.attr("height", height);
var xScale = d3.scaleLinear()
.domain([0, 100])
.range([30, width - 10]);
var yScale = d3.scaleLinear()
.domain([0,100])
.range([height - 30, 10]);
var circles = svg.selectAll("foo")
.data(data)
.enter()
.append("circle");
circles.attr("r", 10)
.attr("fill", "teal")
.attr("cx", d=>xScale(d.x))
.attr("cy", d=>yScale(d.y));
var xAxis = d3.axisBottom(xScale);
var yAxis = d3.axisLeft(yScale);
svg.append("g").attr("transform", "translate(0,220)")
.attr("class", "xAxis")
.call(xAxis);
svg.append("g")
.attr("transform", "translate(30,0)")
.attr("class", "yAxis")
.call(yAxis);
}
d3.selectAll("circle").on("mouseover", function(){
var thisDatum = d3.select(this).datum();
d3.selectAll("circle").filter(d=>d.x == thisDatum.x && d.y == thisDatum.y).attr("fill", "firebrick");
}).on("mouseout", function(){
d3.selectAll("circle").attr("fill", "teal")
})
#svg1 {
float: left;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<div id="svg1"></div>
<div id="svg2"></div>
<div id="svg3"></div>
As you have several different data sets, I'll modify the answer I wrote in your previous question so we can have different y values.
First, let't put all data in an object. That way, we can access the different data sets later:
var dataObject = {
data1: [{
x: 10,
y: 30
}, ...
}],
data2: [{
x: 10,
y: 70
}, ...
}],
data3: [{
x: 10,
y: 10
}, ...
}]
};
Then, we call the draw function:
draw("#svg1", dataObject.data1);
draw("#svg2", dataObject.data2);
draw("#svg3", dataObject.data3);
So, to get what you want, in the mouseover...
d3.selectAll("circle").on("mouseover", function() {
var thisDatum = d3.select(this).datum();
findPoints(thisDatum);
})
We call this function:
function findPoints(datum) {
var myObject = {};
for (var i = 1; i < 4; i++) {
myObject["chart" + i] = dataObject["data" + i].filter(e => e.x === datum.x)[0].y;
}
console.log(myObject)//use return instead of console.log
}
Here is the demo:
var dataObject = {
data1: [{
x: 10,
y: 30
}, {
x: 20,
y: 60
}, {
x: 30,
y: 40
}, {
x: 40,
y: 90
}, {
x: 50,
y: 20
}, {
x: 60,
y: 90
}, {
x: 70,
y: 90
}, {
x: 80,
y: 10
}],
data2: [{
x: 10,
y: 70
}, {
x: 20,
y: 60
}, {
x: 30,
y: 80
}, {
x: 40,
y: 10
}, {
x: 50,
y: 10
}, {
x: 60,
y: 20
}, {
x: 70,
y: 10
}, {
x: 80,
y: 90
}],
data3: [{
x: 10,
y: 10
}, {
x: 20,
y: 20
}, {
x: 30,
y: 40
}, {
x: 40,
y: 90
}, {
x: 50,
y: 80
}, {
x: 60,
y: 70
}, {
x: 70,
y: 50
}, {
x: 80,
y: 50
}]
};
draw("#svg1", dataObject.data1);
draw("#svg2", dataObject.data2);
draw("#svg3", dataObject.data3);
function draw(selector, data) {
var width = 200,
height = 100;
var svg = d3.select(selector)
.append("svg")
.attr("width", width)
.attr("height", height);
var xScale = d3.scaleLinear()
.domain([0, 100])
.range([30, width - 10]);
var yScale = d3.scaleLinear()
.domain([0, 100])
.range([height - 30, 10]);
var circles = svg.selectAll("foo")
.data(data)
.enter()
.append("circle");
circles.attr("r", 5)
.attr("fill", "palegreen")
.attr("cx", d => xScale(d.x))
.attr("cy", d => yScale(d.y));
var xAxis = d3.axisBottom(xScale);
var yAxis = d3.axisLeft(yScale).ticks(2);
svg.append("g").attr("transform", "translate(0,70)")
.attr("class", "xAxis")
.call(xAxis);
svg.append("g")
.attr("transform", "translate(30,0)")
.attr("class", "yAxis")
.call(yAxis);
}
d3.selectAll("circle").on("mouseover", function() {
var thisDatum = d3.select(this).datum();
findPoints(thisDatum);
d3.selectAll("circle").filter(d => d.x == thisDatum.x).attr("fill", "firebrick");
}).on("mouseout", function() {
d3.selectAll("circle").attr("fill", "palegreen")
})
function findPoints(datum) {
var myObject = {};
for (var i = 1; i < 4; i++) {
myObject["chart" + i] = dataObject["data" + i].filter(e => e.x === datum.x)[0].y;
}
console.log(JSON.stringify(myObject))
}
#svg1, #svg2 {
float: left;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<div id="svg1"></div>
<div id="svg2"></div>
<div id="svg3"></div>
This question already has an answer here:
Using d3 to shade area between two lines
(1 answer)
Closed 6 years ago.
I want to fill the lines between two area graphs defined below. I am hitting a bit of a wall -- the issue I seem to have that each path I created does NOT have the other value to compare with, and my efforts to find a work around seem to have hit a bit of a wall.
Any tips?
jQuery(document).ready(function ($) {
var margin = {top: 20, right: 30, bottom: 40, left: 24},
width = 430 - margin.left - margin.right,
height = 225 - margin.top - margin.bottom,
dotRadius = function() { return 3 };
var x = d3.time.scale()
.range([0, width]);
var y = d3.scale.linear()
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient('bottom')
.tickFormat(d3.time.format('%b'));
var yAxis = d3.svg.axis()
.scale(y)
.orient('left');
// This is a function that determines the colours of the lines drawn, up to 10.
var color = d3.scale.category10();
// This is used to format the time for our data.
var formatTime = d3.time.format("%Y-%m-%d");
var line = d3.svg.line()
.x(function(d) { return x(d.Period); })
.y(function(d) { return y(d.Value); })
var areaBetweenGraphs = d3.svg.area()
.x(function(d) {
console.log('ABG x is: ', d);
return x(formatTime.parse(d.Time));
})
.y0(function(d) {
console.log('ABG y0 is: ', d);
return y(d.Quantity);
})
.y1(function(d) {
console.log('ABG y1 is: ', d);
return y(d.Amount);
});
var svg = d3.select("#pipeline-chart-render")
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
// This separates the data into the lines we want, although the data is stored
// In the same original object.
color.domain(d3.keys(data[0].values[0]).filter(function(key) {
if (key === 'Amount'
|| key === 'Quantity') {
return key
}
}));
// This returns the data into two separate objects which can be graphed.
// In this case, Amount and Quantity.
var datasets = color.domain().map(function(name) {
return {
name: name,
values: data.map(function(d) {
return {
Period: formatTime.parse(d.values[0].Time),
Value: +d.values[0][name]};
})
};
});
console.log('datasets is: ', datasets);
// set the minYDomainValue to zero instead of letting it be a lingering magic number.
var minDomainValue = 0
var minDate = d3.min(datasets, function(d0){
return d3.min(d0.values, function(d1){
return d1.Period;
})
}),
maxDate = d3.max(datasets, function(d0){
return d3.max(d0.values, function(d1){
return d1.Period;
})
});
x.domain([minDate, maxDate]);
y.domain([
minDomainValue,
d3.max(datasets, function(c) { return d3.max(c.values, function(v) { return v.Value; }); })
])
// Append the x-axis class and move axis around.
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
// Append the y-axis class.
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
svg.append('g')
var pipeline = svg.selectAll('.pipeline')
.data(datasets);
pipeline.enter()
.append('g')
.attr('class', 'pipeline');
pipeline.append('path')
.attr('class', 'line')
.attr('id', function(d, i) {
return 'pipeline-'+(i+1);
})
.attr('d', function(d) { console.log('line d is: ', d); return line(d.values); })
.attr("data-legend",function(d) { return d.name})
.style("stroke", function(d) { return color(d.name); })
pipeline.exit().remove()
// Rendering the points on the graph.
var points = svg.selectAll('.pipelinePoint')
.data(datasets);
points
.enter()
.append('g')
.attr('class', 'pipelinePoint');
points.selectAll('.point')
.data(function(d) {
return d.values;
})
.enter()
.append('circle')
.attr('circleId', function(d, i) {
return 'circleId-'+i;
})
.attr('cx', function(d) {
return x(d.Period);
})
.attr('cy', function(d) {
return y(d.Value);
})
.attr('r', function(d) {
return dotRadius()
});
});
var data = [
{
key: 1,
values: [
{
Amount: 33,
Quantity: 22,
Time: '2015-01-01'
}
]
},
{
key: 2,
values: [
{
Amount: 52,
Quantity: 20,
Time: '2015-02-01'
}
]
},
{
key: 3,
values: [
{
Amount: 63,
Quantity: 30,
Time: '2015-03-01'
}
]
},
{
key: 4,
values: [
{
Amount: 92,
Quantity: 60,
Time: '2015-04-01'
}
]
},
{
key: 5,
values: [
{
Amount: 50,
Quantity: 29,
Time: '2015-05-01'
}
]
},
{
key: 6,
values: [
{
Amount: 53,
Quantity: 25,
Time: '2015-06-01'
}
]
},
{
key: 7,
values: [
{
Amount: 46,
Quantity: 12,
Time: '2015-07-01'
}
]
},
{
key: 8,
values: [
{
Amount: 52,
Quantity: 15,
Time: '2015-08-01'
}
]
},
{
key: 9,
values: [
{
Amount: 55,
Quantity: 20,
Time: '2015-09-01'
}
]
},
{
key: 10,
values: [
{
Amount: 35,
Quantity: 17,
Time: '2015-10-01'
}
]
},
{
key: 11,
values: [
{
Amount: 80,
Quantity: 45,
Time: '2015-11-01'
}
]
},
{
key: 12,
values: [
{
Amount: 64,
Quantity: 24,
Time: '2015-12-01'
}
]
}
]
CSS if you want it to be a less ugly render:
/* Line Chart CSS */
.axis path,
.axis line {
fill: none;
stroke: #000;
stroke-width: 3px;
shape-rendering: crispEdges;
}
#pipeline-1,
#pipeline-2 {
fill: none;
stroke-width: 1.5px;
stroke-linecap: round;
transition: stroke-width 250ms linear;
-moz-transition: stroke-width 250ms linear;
-webkit-transition: stroke-width 250ms linear;
transition-delay: 250ms
-moz-transition-delay: 250ms;
-webkit-transition-delay: 250ms;
}
.x.axis path {
/* Uncomment below if I want to remove x-axis line */
/* display: none;*/
}
.line.hover path {
stroke-width: 6px;
}
#pipeline-chart-render {
padding-left: -50px;
}
.area {
fill: steelblue;
}
This ended up working.
// The following is for defining the area BETWEEN graphs.
var areaAboveQuantity = d3.svg.area()
.x(line.x())
.y0(line.y())
.y1(0);
var areaBelowQuantity = d3.svg.area()
.x(line.x())
.y0(line.y())
.y1(height);
var areaAboveAmount = d3.svg.area()
.x(line.x())
.y0(line.y())
.y1(0);
var areaBelowAmount = d3.svg.area()
.x(line.x())
.y0(line.y())
.y1(height);
var defs = svg.append('defs');
defs.append('clipPath')
.attr('id', 'clip-quantity')
.append('path')
.datum(datasets)
.attr('d', function(d) {
return areaAboveQuantity(d[1].values);
});
defs.append('clipPath')
.attr('id', 'clip-amount')
.append('path')
.datum(datasets)
.attr('d', function(d) {
return areaAboveAmount(d[0].values);
});
svg.append('path')
.datum(datasets)
.attr('class', 'area')
.attr('d', function(d) {
return areaBelowQuantity(d[1].values)
});
// Quantity IS ABOVE Amount
svg.append('path')
.datum(datasets)
.attr('d', function(d) {
areaBelowQuantity(d[1].values);
})
.attr('clip-path', 'url(#clip-amount)')
.style('fill', 'steelblue')
.style('opacity', '0.2');
// Amount IS ABOVE Quanity
svg.append('path')
.datum(datasets)
.attr('d', function(d) {
return areaBelowAmount(d[0].values);
})
.attr('clip-path', 'url(#clip-quantity)')
.style('fill', 'steelblue')
.style('opacity', '0.2');
Trying to create a custom line chart in which there is only one simple line, with a gradient background - the background of every part of the line is determined according to the y-value at that point (changes in values are guaranteed to be mild).
I'm having trouble with the basic configuration. This is my code:
js:
// General definitions
var HEIGHT, MARGINS, WIDTH, formatDay, lineFunc, graph, graph_data, weekdays, x, xAxis, y, yAxis;
WIDTH = 360;
HEIGHT = 130;
MARGINS = {
top: 20,
right: 30,
bottom: 20,
left: 20
};
graph = d3.select("#graph");
// Define Axes
weekdays = ["MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"];
formatDay = function(d) {
return weekdays[d % 6];
};
x = d3.scale.linear().range([MARGINS.left, WIDTH - MARGINS.right]).domain([
d3.min(graph_data, function(d) {
return d.x;
}), d3.max(graph_data, function(d) {
return d.x + 1;
})
]);
y = d3.scale.linear().range([HEIGHT - MARGINS.top, MARGINS.bottom]).domain([
d3.min(graph_data, function(d) {
return d.y;
}), d3.max(graph_data, function(d) {
return d.y;
})
]);
xAxis = d3.svg.axis().scale(x).orient("bottom").tickFormat(formatDay);
yAxis = d3.svg.axis().scale(y).tickSize(10).orient("left");
// Line Function
lineFunc = d3.svg.line().x(function(d) {
return x(d.x);
}).y(function(d) {
return y(d.y);
}).interpolate("basis");
// Define Line Gradient
graph.append("linearGradient").attr("id", "line-gradient").attr("gradientUnits", "userSpaceOnUse").attr("x1", 0).attr("y1", y(0)).attr("x2", 0).attr("y2", y(200)).selectAll("stop").data([
{
offset: "0%",
color: "#F0A794"
}, {
offset: "20%",
color: "#F0A794"
}, {
offset: "20%",
color: "#E6A36A"
}, {
offset: "40%",
color: "#E6A36A"
}, {
offset: "40%",
color: "#CE9BD2"
}, {
offset: "62%",
color: "#CE9BD2"
}, {
offset: "62%",
color: "#AA96EE"
}, {
offset: "82%",
color: "#AA96EE"
}, {
offset: "82%",
color: "#689BE7"
}, {
offset: "90%",
color: "#689BE7"
}, {
offset: "90%",
color: "1AA1DF"
}, {
offset: "100%",
color: "1AA1DF"
}
]).enter().append("stop").attr("offset", function(d) {
return d.offset;
}).attr("stop-color", function(d) {
return d.color;
});
// Draw Line
graph.append("svg:path").attr("d", lineFunc(graph_data));
// Draw Axes
graph.append("svg:g").attr("class", "x axis").attr("transform", "translate(0," + (HEIGHT - MARGINS.bottom) + ")").call(xAxis);
graph.append("svg:g").attr("class", "y axis").attr("transform", "translate(" + MARGINS.left + ",0)").call(yAxis);
style
#line-gradient {
fill: none;
stroke: url(#line-gradient);
stroke-width: 7px;
stroke-linejoin: "round";
}
Sample data
graph_data = [{
x: 1,
y: 22
}, {
x: 2,
y: 20
}, {
x: 3,
y: 10
}, {
x: 4,
y: 40
}, {
x: 5,
y: 5
}, {
x: 6,
y: 30
}, {
x: 7,
y: 60
}]
What i'm getting looks like this:
Can any of you D3.js experts tell me what I'm doing wrong, and what needs to change in order for my line to be a line rather than an area, having the line background gradient explained above, and round edges?
Many thanks in advance!
Here's a fiddle: http://jsfiddle.net/henbox/gu4y7fk8/
You should give the path a class name, like this:
graph.append("svg:path")
.attr("class","chartpath")
.attr("d", lineFunc(graph_data));
And then the CSS styling you have should be on that path element rather than the lineargradient element
.chartpath { /*note: not #line-gradient*/
fill: none;
stroke: url(#line-gradient);
stroke-width: 7px;
stroke-linejoin: "round";
}
I also fixed up a couple of other things:
Missing # on a couple of the color codes, so changed (color: "1AA1DF" to color: "#1AA1DF"
I changed the max y value for the gradient from 200 to 60, so that the changing color gradient of the line is more visible in the example (.attr("y2", y(200)) to .attr("y2", y(60)))