Related
I am trying to create small multiple bar charts that have different y-axis scales using d3 v6. There are a few examples out there (https://flowingdata.com/2014/10/15/linked-small-multiples/) of small multiples for previous versions of d3, but not v6, which seems to have a good number of changes implemented.
I don't have much experience with d3, so I am probably missing something obvious, but I can't get the bars to properly generate, the axes are generating (though I think I am generating them too many times on top of each other).
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Small multiple bar charts</title>
<script src="https://d3js.org/d3.v6.min.js"></script>
</head>
<body>
<div id='vis'></div>
<script type="text/javascript">
// Set the sizing metrics
var width = 150;
var height = 120;
var margin = {top: 15, right: 10, bottom: 40, left: 35};
// Create the axes
var xScale = d3.scaleBand()
.range([0, width])
.padding(0.1);
var yScale = d3.scaleLinear()
.range([height, 0]);
var xAxis = d3.axisBottom()
.scale(xScale);
// Load the data
d3.csv('data.csv').then(function(data) {
data.forEach(function(d) {
d.segment = d.segment;
d.parameter = d.parameter;
d.the_value = +d.the_value;
});
// set the x domain
xScale.domain(data.map(function(d) { return d.segment; }));
// group the data
metrics = Array.from(
d3.group(data, d => d.parameter), ([key, value]) => ({key, value})
);
// create a separate SVG object for each group
var svg = d3.select('#vis').selectAll('svg')
.data(metrics)
.enter()
.append('svg');
// loop over the data and create the bars
metrics.forEach(function(d, i) {
console.log(d);
console.log(metrics);
yScale.domain([0, d3.max(metrics, function(c) { return c.the_value; })]);
svg.selectAll('.bar')
.data(d)
.enter().append('rect')
.attr('class', 'bar')
.attr('x', function(c) { return xScale(c.segment); })
.attr('width', xScale.bandwidth())
.attr('y', function(c) { return yScale(c.the_value); })
.attr('height', function(c) { return height - yScale(c.the_value); })
.attr('fill', 'teal');
svg.append('g')
.attr('transform', 'translate(0,' + height + ')')
.call(xAxis)
});
});
</script>
</body>
</html>
Here is the data file:
segment,parameter,the_value
A,one,33
A,two,537723
A,three,14
A,four,5
A,five,0.093430759
B,one,76
B,two,137110
B,three,16
B,four,20
B,five,0.893868331
C,one,74
C,two,62020
C,three,25
C,four,14
C,five,0.862952872
Eventually I would also like to get the charts linked so that when series A is hovered on the first graph the value will display for each series on all of the graphs, but the first step is to get the visuals properly working.
There's a few small changes to get it working:
When you set the domain on the x scale, you just need the unique segments e.g. A, B, C and not the full list of segments from the data.
When you create the 5 SVGs you can class them so that you can refer to each separately when you loop through the values of the metrics. So the first small multiple has a class of one, the second small multiple has a class of two etc
Reset the y domain using the set of the_values from the metrics you're charting - i.e. use d not metrics
When you loop metrics first select the small multiple for that metric and then selectAll('.bar')
Pass d.value to data as this makes the references to c.the_value etc work properly
To prevent adding the x axis multiple times, again select the SVG for the specific small multiple before call(xAxis) otherwise you add as many axes as there are parameters to each small multiple.
I faked up your data to include random data.
See the example below - maybe there's a smarter way to do it:
// fake data
var data = ["A", "B", "C"].map(seg => {
return ["one", "two", "three", "four", "five"].map((par, ix) => {
return {
"segment": seg,
"parameter": par,
"the_value": (Math.floor(Math.random() * 10) + 1) * (Math.floor(Math.random() * 10 * ix) + 1)
}
});
}).flat();
// Set the sizing metrics
var width = 150;
var height = 120;
var margin = {top: 15, right: 10, bottom: 40, left: 35};
// Create the axes
var xScale = d3.scaleBand()
.range([0, width])
.padding(0.1);
var yScale = d3.scaleLinear()
.range([height, 0]);
var xAxis = d3.axisBottom()
.scale(xScale);
// set the x domain
// put unique segments into the domain e.g. A, B, C
var uniqueSegments = Array.from(new Set(data.map(function(d) {return d.segment;} )));
xScale.domain(uniqueSegments);
// group the data
var metrics = Array.from(
d3.group(data, d => d.parameter), ([key, value]) => ({key, value})
);
// create a separate SVG object for each group
// class each SVG with parameter from metrics
var svg = d3.select('#vis').selectAll('svg')
.data(metrics)
.enter()
.append('svg')
.attr("class", function(d) { return d.value[0].parameter;});
// loop over the data and create the bars
metrics.forEach(function(d, i) {
//console.log(d);
//console.log(metrics);
// reset yScale domain based on the set of the_value's for these metrics
yScale.domain([0, d3.max(d.value, function(c) { return c.the_value; })]);
// select the right svg for this set of metrics
d3.select("svg." + d.value[0].parameter)
.selectAll('.bar')
.data(d.value) // use d.value to get to the the_value
.enter()
.append('rect')
.attr('class', 'bar')
.attr('x', function(c) { return xScale(c.segment); })
.attr('width', xScale.bandwidth())
.attr('y', function(c) { return yScale(c.the_value); })
.attr('height', function(c) { return height - yScale(c.the_value); })
.attr('fill', 'teal');
// call axis just on this SVG
// otherwise calling it 5 times for 5 metrics...
d3.select("svg." + d.value[0].parameter)
.append('g')
.attr('transform', 'translate(0,' + height + ')')
.call(xAxis)
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.5.0/d3.min.js"></script>
<div id='vis'></div>
I'd like to be able to animate multiple (based on the data) arc charts from one percent (angle) to another in D3.js and can draw them fine initially.
However, after much hunting around, I'm stuck with the animation. Below is the code that does the original drawing and then two options for animation to subsequent values. I'm using groups for each Chart Node as I will be adding multiple elements to each.
Option 1 uses standard interpolation which I know doesn't work
properly as the shape is too complex. So the animation doesn't
follow the correct steps and also errors are reported to the console.
Option 2 uses the Arc Tween method, but this just reports errors.
To see each option working, comment out the other one.
Ideally, I'd like to be able to create an arc function to which I can pass the innerRadius, outerRadius and then the endAngle. For at least the endAngle, I want to be able to choose to pass a constant (e.g. 0) or Bound Data (e.g. d.pct).
index.html
<html lang="en">
<head>
<script src="https://d3js.org/d3.v6.min.js"></script>
</head>
<body>
<div id="vis">
</div>
<script src = 'SOarc.js'></script>
</body>
</html>
SOarc.js
data = [
{x:50, y: 250, pct: 0.25},
{x:200, y: 250, pct: 0.50},
{x:350, y: 250, pct: 0.75}]
radialScale = d3.scaleLinear()
.domain([0, 1])
.range([0, 2 * Math.PI]);
svg = d3.select("#vis")
.append('svg')
.attr('width', 500)
.attr('height', 500)
.attr('opacity', 1)
// Join to the data and create a group for each data point so that various chart items (e.g. multiple arcs) can be added
chartNodes = svg
.selectAll('g.chartGroup')
.data(data)
// Position each using transform/ translate with coordinates specified in data
chartNodesEnter = chartNodes
.enter()
.append('g')
.attr("class", "chartGroup")
.attr('transform', (d) => 'translate('+d.x+','+d.y+')');
// Add arcs to as per data
chartNodesEnter.append('path')
.attr("class", "chart1")
.attr('fill', "red")
.attr('d', d3.arc()
.startAngle(0)
.endAngle((d) => radialScale(d.pct))
.innerRadius(50+2) // This is the size of the donut hole
.outerRadius(50+8));
// Now animate to a different endAngle (90% in this example)
// Option 1 - Standard Interpolation - doesn't work with complex shapes
// --------------------------------------------------------------------
// Animate all arcs to 90% - doesn't animate properly as interpolation not correct for this complex shape
// and also throws Error: <path> attribute d: Expected arc flag ('0' or '1') errors for the same reason
svg.selectAll('.chart1')
.transition().duration(3000).delay(0)
.attr('d', d3.arc()
.startAngle(0)
.endAngle(function(d){ return radialScale(0.9)})
.innerRadius(50+2) // This is the size of the donut hole
.outerRadius(50+8)
)
// Option 2 - Tween Interpolation - Produces error
// -----------------------------------------------
// Code from from Mike Bostock's Arc Tween http://bl.ocks.org/mbostock/5100636
// Errors with <path> attribute d: Expected moveto path command ('M' or 'm'), "function(t) {\n …".
var arc = d3.arc()
.innerRadius(50+2)
.outerRadius(50+8)
.startAngle(0);
// Returns a tween for a transition’s "d" attribute, transitioning any selected
// arcs from their current angle to the specified new angle.
function arcTween(newAngle) {
return function(d) {
var interpolate = d3.interpolate(d.endAngle, newAngle);
return function(t) {
d.endAngle = interpolate(t);
return arc(d);
};
};
}
// Animate to 90%
svg.selectAll('.chart1')
.transition().duration(3000).delay(0)
.attrTween("d", d => arcTween(radialScale(0.9)) );
Error: <path> attribute d: Expected moveto path command ('M' or 'm'), "function(t) {\n …". # SOarc.js:68
Option 2 is the right way to do this but Mr. Bostock's example is a little much for your simpler use case.
Let's examine the simplest code which achieves your goal:
// create a arc generator with start angle of 0
var arc = d3
.arc()
.innerRadius(50 + 2)
.outerRadius(50 + 8)
.startAngle(0)
.endAngle(0);
svg
.selectAll('.chart1')
.transition()
.duration(3000)
.delay(0)
.attrTween('d', function(d,i) {
// for each chart
// create an interpolator between start angle 0
// and end angle of d.pct
var interpolate = d3.interpolate(0, radialScale(d.pct));
// attrTween is expecting a function to call for every iteration of t
// so let's return such a function
return function(t) {
// assign end angle to interpolated value for t
arc.endAngle(interpolate(t));
// call arc and return intermediate `d` value
return arc();
};
});
Here it is running:
<html lang="en">
<head>
<script src="https://d3js.org/d3.v6.min.js"></script>
</head>
<body>
<div id="vis"></div>
<script>
data = [
{ x: 50, y: 250, pct: 0.25 },
{ x: 200, y: 250, pct: 0.5 },
{ x: 350, y: 250, pct: 0.75 },
];
radialScale = d3
.scaleLinear()
.domain([0, 1])
.range([0, 2 * Math.PI]);
svg = d3
.select('#vis')
.append('svg')
.attr('width', 500)
.attr('height', 500)
.attr('opacity', 1);
// Join to the data and create a group for each data point so that various chart items (e.g. multiple arcs) can be added
chartNodes = svg.selectAll('g.chartGroup').data(data);
// Position each using transform/ translate with coordinates specified in data
chartNodesEnter = chartNodes
.enter()
.append('g')
.attr('class', 'chartGroup')
.attr('transform', (d) => 'translate(' + d.x + ',' + d.y + ')');
// Add arcs to as per data
chartNodesEnter
.append('path')
.attr('class', 'chart1')
.attr('fill', 'red')
.attr(
'd',
d3
.arc()
.startAngle(0)
.endAngle((d) => radialScale(d.pct))
.innerRadius(50 + 2) // This is the size of the donut hole
.outerRadius(50 + 8)
);
// Now animate to a different endAngle (90% in this example)
// Option 1 - Standard Interpolation - doesn't work with complex shapes
// --------------------------------------------------------------------
// Animate all arcs to 90% - doesn't animate properly as interpolation not correct for this complex shape
// and also throws Error: <path> attribute d: Expected arc flag ('0' or '1') errors for the same reason
/*
svg
.selectAll('.chart1')
.transition()
.duration(3000)
.delay(0)
.attr(
'd',
d3
.arc()
.startAngle(0)
.endAngle(function (d) {
return radialScale(0.9);
})
.innerRadius(50 + 2) // This is the size of the donut hole
.outerRadius(50 + 8)
);
*/
// Option 2 - Tween Interpolation - Produces error
// -----------------------------------------------
// Code from from Mike Bostock's Arc Tween http://bl.ocks.org/mbostock/5100636
// Errors with <path> attribute d: Expected moveto path command ('M' or 'm'), "function(t) {\n …".
var arc = d3
.arc()
.innerRadius(50 + 2)
.outerRadius(50 + 8)
.startAngle(0)
.endAngle(0);
// Animate to end angle
svg
.selectAll('.chart1')
.transition()
.duration(3000)
.delay(0)
.attrTween('d', function(d,i) {
var interpolate = d3.interpolate(0, radialScale(d.pct));
return function(t) {
arc.endAngle(interpolate(t));
return arc();
};
});
</script>
</body>
</html>
New snippet for comments
Lots of options for variable arcs. The first thing that jumped into my head was to add your radiuses into your data binding and create the arcs like in this snippet.
<html lang="en">
<head>
<script src="https://d3js.org/d3.v6.min.js"></script>
</head>
<body>
<div id="vis"></div>
<script>
data = [
{ x: 50, y: 250, pct: 0.25, inner: 52, outer: 58 },
{ x: 200, y: 250, pct: 0.5, inner: 22, outer: 28 },
{ x: 350, y: 250, pct: 0.75, inner: 82, outer: 88 },
];
radialScale = d3
.scaleLinear()
.domain([0, 1])
.range([0, 2 * Math.PI]);
svg = d3
.select('#vis')
.append('svg')
.attr('width', 500)
.attr('height', 500)
.attr('opacity', 1);
// Join to the data and create a group for each data point so that various chart items (e.g. multiple arcs) can be added
chartNodes = svg.selectAll('g.chartGroup').data(data);
// Position each using transform/ translate with coordinates specified in data
chartNodesEnter = chartNodes
.enter()
.append('g')
.attr('class', 'chartGroup')
.attr('transform', (d) => 'translate(' + d.x + ',' + d.y + ')');
// Add arcs to as per data
chartNodesEnter
.append('path')
.attr('class', 'chart1')
.attr('fill', 'red')
.attr(
'd',
d3
.arc()
.startAngle(0)
.endAngle((d) => radialScale(d.pct))
.innerRadius(50 + 2) // This is the size of the donut hole
.outerRadius(50 + 8)
);
// Now animate to a different endAngle (90% in this example)
// Option 1 - Standard Interpolation - doesn't work with complex shapes
// --------------------------------------------------------------------
// Animate all arcs to 90% - doesn't animate properly as interpolation not correct for this complex shape
// and also throws Error: <path> attribute d: Expected arc flag ('0' or '1') errors for the same reason
/*
svg
.selectAll('.chart1')
.transition()
.duration(3000)
.delay(0)
.attr(
'd',
d3
.arc()
.startAngle(0)
.endAngle(function (d) {
return radialScale(0.9);
})
.innerRadius(50 + 2) // This is the size of the donut hole
.outerRadius(50 + 8)
);
*/
// Option 2 - Tween Interpolation - Produces error
// -----------------------------------------------
// Code from from Mike Bostock's Arc Tween http://bl.ocks.org/mbostock/5100636
// Errors with <path> attribute d: Expected moveto path command ('M' or 'm'), "function(t) {\n …".
// Animate to end angle
svg
.selectAll('.chart1')
.transition()
.duration(3000)
.delay(0)
.attrTween('d', function(d,i) {
var interpolate = d3.interpolate(0, radialScale(d.pct));
var arc = d3
.arc()
.innerRadius(d.inner)
.outerRadius(d.outer)
.startAngle(0)
.endAngle(0);
return function(t) {
arc.endAngle(interpolate(t));
return arc();
};
});
</script>
</body>
</html>
I tried a lot of datasets and I don't know why, I have a issue with data_histo.csv. The x axis seems reverse and then, bars can't be displayed. With data_histo2.csv or data_histo3.csv, all is good.
An explanation could be nice!
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/d3#5.0.0/dist/d3.min.js"></script>
</head>
<body>
<svg class="histogramme" width="960" height="600"></svg>
<script>
//select
let svgHisto = d3.select(".histogramme")
//dimension
let margin = {top: 20, right: 10, bottom: 20, left: 80}
let width = +svgHisto.attr("width") - margin.left - margin.right
let height = +svgHisto.attr("height") - margin.top - margin.bottom;
let g1 = svgHisto.append("g")
.attr("transform",`translate(${margin.left}, ${margin.top})`);
//data
d3.csv("data_histo.csv").then(function(data) {
console.log(data);
//define x and y axis
let x = d3.scaleLinear();
let y = d3.scaleBand();
x.domain(d3.extent(data, function(d) { return d.value0; })).nice()
.rangeRound([0, width]);
y.domain(data.map(function(d) { return d.libreg; }))
.rangeRound([0, height]).padding(0.1);
//add x axis
g1.append("g")
.attr("class", "axis x_axis")
.attr("transform",`translate(0,${height})`)
.call(d3.axisBottom(x));
//add y axis
g1.append("g")
.attr("class", "axis y_axis")
.call(d3.axisLeft(y));
//bar chart
g1.selectAll(".bar1")
.data(data)
.enter().append("rect")
.attr("class", "bar bar1")
.attr("x", function(d) {return x(Math.min(0,d.value0)); })
.attr("y", function(d) { return y(d.libreg) + 10; })
.attr("width", 0)
.attr("height", y.bandwidth() - 20);
//animate
g1.selectAll(".bar1")
.transition()
.duration(1000)
.delay(function(d,i){return i*100})
.attr("width", function(d) { return x(d.value0); });
});
</script>
</body>
</html>
With data_histo.csv
"codreg","libreg","year0","value0","year1","value1"
"03","Guyane",2009,4,2014,4.6
"04","La Réunion",2009,8.2,2014,9.8
"11","Île-de-France",2009,12.6,2014,13.9
"01","Guadeloupe",2009,13.3,2014,15.8
"32","Hauts-de-France",2009,14.7,2014,16.1
"02","Martinique",2009,14.7,2014,17.6
"44","Grand Est",2009,16.5,2014,18
"84","Auvergne-Rhône-Alpes",2009,16.8,2014,18.3
"52","Pays de la Loire",2009,17.1,2014,18.6
"28","Normandie",2009,17.2,2014,19
"53","Bretagne",2009,18.5,2014,20.2
"24","Centre-Val de Loire",2009,18.7,2014,20.4
"27","Bourgogne-Franche-Comté",2009,18.8,2014,20.7
"76","Occitanie",2009,19.3,2014,20.8
"93","Provence-Alpes-Côte d''Azur",2009,19.5,2014,21.3
"94","Corse",2009,20.2,2014,21.5
"75","Nouvelle-Aquitaine",2009,20.2,2014,21.8
With data_histo2.csv
"codreg","libreg","year0","value0","year1","value1"
"84","Auvergne-Rhône-Alpes",2013,39.1,2017,41.7
"27","Bourgogne-Franche-Comté",2013,42.3,2017,44.4
"53","Bretagne",2013,39.6,2017,44.7
"24","Centre-Val de Loire",2013,40.5,2017,46.8
"94","Corse",2013,24.2,2017,30.8
"44","Grand Est",2013,41.3,2017,45.4
"01","Guadeloupe",2013,55.5,2017,56.5
"03","Guyane",2013,33.1,2017,33.2
"32","Hauts-de-France",2013,45.8,2017,47.3
"11","Île-de-France",2013,40.1,2017,42.6
"02","Martinique",2013,52.5,2017,50.2
"28","Normandie",2013,42.6,2017,46.2
"75","Nouvelle-Aquitaine",2013,40,2017,44.4
"76","Occitanie",2013,40.3,2017,43.7
"52","Pays de la Loire",2013,40.6,2017,45.8
"93","Provence-Alpes-Côte d''Azur",2013,38.5,2017,42.6
"04","La Réunion",2013,54.2,2017,54.6
"06","Mayotte",2013,,2017,
Here is my code : https://plnkr.co/edit/B8qEQ4dlUdRHhkQvzjZx?p=preview
There are two issues with your code:
D3 parses csv/tsv/dsv entries as text. So when you load your csv, you get rows that look like this:
{
"codreg": "03",
"libreg": "Guyane",
"year0": "2009",
"value0": "4",
"year1": "2014",
"value1": "4.6"
}
When you set your scale with extent, you aren't using the numerical extent. You could coerce your data to a number like so:
data.forEach(function(d) {
d.value0 = +d.value0;
})
Secondly, if you do this you'll notice some peculiar behavior in the placement of the bars:
You can see that the bars start to the left of the plot area. The reason is that you are using a linear scale, and plot the start of the bars like so:
.attr("x", function(d) {return x(Math.min(0,d.value0)); })
You want your bars to start at x(4) - which is where the x value that marks the interception with the y axis. Instead you are using x(0), which will naturally be to the left of where you want. This works in your second example, because x(0) also happens to be the x value that intercepts the y axis. Instead, you can simply use:
.attr("x",0)
This marks the left edge of the plot area, which is likely where you want all your bars to be anchored to.
Here's a forked plunkr.
One thing to note, is that your shortest bar will always be non-visible: the start and end points will be the same. This is because the extent of the scale goes from the smallest value to the largest value - and the smallest value, marking the left boundary of the plot area, is the value of the shortest bar. To change this you can modify the scale's domain, perhaps using 0 as the first value and then using d3.max to find the uppermost value.
I just started learning javascript and d3.js by taking a couple of lynda.com courses. My objective is to create a function that takes an array of numbers and a cutoff and produces a plot like this one:
I was able to write javascript code that generates this:
Alas, I'm having troubles figuring out a way to tell d3.js that the area to the left of -opts.threshold should be read, the area in between -opts.threshold and opts.threshold blue, and the rest green.
This is my javascript code:
HTMLWidgets.widget({
name: 'IMposterior',
type: 'output',
factory: function(el, width, height) {
// TODO: define shared variables for this instance
return {
renderValue: function(opts) {
console.log("MME: ", opts.MME);
console.log("threshold: ", opts.threshold);
console.log("prob: ", opts.prob);
console.log("colors: ", opts.colors);
var margin = {left:50,right:50,top:40,bottom:0};
var xMax = opts.x.reduce(function(a, b) {
return Math.max(a, b);
});
var yMax = opts.y.reduce(function(a, b) {
return Math.max(a, b);
});
var xMin = opts.x.reduce(function(a, b) {
return Math.min(a, b);
});
var yMin = opts.y.reduce(function(a, b) {
return Math.min(a, b);
});
var y = d3.scaleLinear()
.domain([0,yMax])
.range([height,0]);
var x = d3.scaleLinear()
.domain([xMin,xMax])
.range([0,width]);
var yAxis = d3.axisLeft(y);
var xAxis = d3.axisBottom(x);
var area = d3.area()
.x(function(d,i){ return x(opts.x[i]) ;})
.y0(height)
.y1(function(d){ return y(d); });
var svg = d3.select(el).append('svg').attr("height","100%").attr("width","100%");
var chartGroup = svg.append("g").attr("transform","translate("+margin.left+","+margin.top+")");
chartGroup.append("path")
.attr("d", area(opts.y));
chartGroup.append("g")
.attr("class","axis x")
.attr("transform","translate(0,"+height+")")
.call(xAxis);
},
resize: function(width, height) {
// TODO: code to re-render the widget with a new size
}
};
}
});
In case this is helpful, I saved all my code on a public github repo.
There are two proposed solutions in this answer, using gradients or using multiple areas. I will propose an alternate solution: Use the area as a clip path for three rectangles that together cover the entire plot area.
Make rectangles by creating a data array that holds the left and right edges of each rectangle. Rectangle height and y attributes can be set to svg height and zero respectively when appending rectangles, and therefore do not need to be included in the array.
The first rectangle will have a left edge at xScale.range()[0], the last rectangle will have an right edge of xScale.range()[1]. Intermediate coordinates can be placed with xScale(1), xScale(-1) etc.
Such an array might look like (using your proposed configuration and x scale name):
var rects = [
[x.range()[0],x(-1)],
[x(-1),x(1)],
[x(1),x.range()[1]]
]
Then place them:
.enter()
.append("rect")
.attr("x", function(d) { return d[0]; })
.attr("width", function(d) { return d[1] - d[0]; })
.attr("y", 0)
.attr("height",height)
Don't forget to set a clip-path attribute for the rectangles:
.attr("clip-path","url(#areaID)"), and to set fill to three different colors.
Now all you have to do is set your area's fill and stroke to none, and append your area to a clip path with the specified id:
svg.append("clipPath)
.attr("id","area")
.append("path")
.attr( // area attributes
...
Here's the concept in action (albeit using v3, which shouldn't affect the rectangles or text paths.
Thanks to #andrew-reid suggestion, I was able to implement the solution that uses multiple areas.
HTMLWidgets.widget({
name: 'IMposterior',
type: 'output',
factory: function(el, width, height) {
// TODO: define shared variables for this instance
return {
renderValue: function(opts) {
console.log("MME: ", opts.MME);
console.log("threshold: ", opts.threshold);
console.log("prob: ", opts.prob);
console.log("colors: ", opts.colors);
console.log("data: ", opts.data);
var margin = {left:50,right:50,top:40,bottom:0};
xMax = d3.max(opts.data, function(d) { return d.x ; });
yMax = d3.max(opts.data, function(d) { return d.y ; });
xMin = d3.min(opts.data, function(d) { return d.x ; });
yMin = d3.min(opts.data, function(d) { return d.y ; });
var y = d3.scaleLinear()
.domain([0,yMax])
.range([height,0]);
var x = d3.scaleLinear()
.domain([xMin,xMax])
.range([0,width]);
var yAxis = d3.axisLeft(y);
var xAxis = d3.axisBottom(x);
var area = d3.area()
.x(function(d){ return x(d.x) ;})
.y0(height)
.y1(function(d){ return y(d.y); });
var svg = d3.select(el).append('svg').attr("height","100%").attr("width","100%");
var chartGroup = svg.append("g").attr("transform","translate("+margin.left+","+margin.top+")");
chartGroup.append("path")
.attr("d", area(opts.data.filter(function(d){ return d.x< -opts.MME ;})))
.style("fill", opts.colors[0]);
chartGroup.append("path")
.attr("d", area(opts.data.filter(function(d){ return d.x > opts.MME ;})))
.style("fill", opts.colors[2]);
if(opts.MME !==0){
chartGroup.append("path")
.attr("d", area(opts.data.filter(function(d){ return (d.x < opts.MME & d.x > -opts.MME) ;})))
.style("fill", opts.colors[1]);
}
chartGroup.append("g")
.attr("class","axis x")
.attr("transform","translate(0,"+height+")")
.call(xAxis);
},
resize: function(width, height) {
// TODO: code to re-render the widget with a new size
}
};
}
});
I'm trying to draw what I think amounts to a force graph in d3, but in a single flat line. I would like around 4-5 points of varying size depending on their magnitude, spaced evenly between them (not by center, but the distance between the sides of the circles should be constant), and lines to join them. So in ASCII format, something like:
o---O---o---O
I was trying to avoid a complicated calculation to figure out the center coordinates and start and end of each line, so it seemed like the force layout might do the trick. Unfortunately, when I put it together, I can't seem to get it to work very well. Often times points end up behind other points, so for a 4 node graph like above, it comes out looking something more like:
O---O
Is there any way to get the force layout to work in 1 dimension instead of 2? Or am I stuck doing all of the spacing calculations myself? The code I'm working with is below:
var width = 500;
var height = 200;
var svg = d3.select($el[0])
.append('svg')
.attr('width', width)
.attr('height', height);
var data_nodes = [
{ x: width / 2, y: height / 2, count: 5 },
{ x: width / 2, y: height / 2, count: 0 },
{ x: width / 2, y: height / 2, count: 1 },
{ x: width / 2, y: height / 2, count: 10 },
];
var data_links = [
{ source: 0, target: 1 },
{ source: 1, target: 2 },
{ source: 2, target: 3 },
];
var force = d3.layout.force()
.nodes(data_nodes)
.links(data_links)
.linkDistance(150)
.linkStrength(0.5)
.gravity(0.7)
.friction(0.3)
.size([width, height])
.charge(-300);
var links = svg.selectAll('line')
.data(data_links)
.enter()
.append('line')
.attr('stroke', '#65759E')
.attr('stroke-width', 4)
.attr('x1', function (d) { return data_nodes[d.source].x; })
.attr('y1', function (d) { return data_nodes[d.source].y; })
.attr('x2', function (d) { return data_nodes[d.target].x; })
.attr('y2', function (d) { return data_nodes[d.target].y; });
var nodes = svg.selectAll('circle')
.data(data_nodes)
.enter()
.append('circle')
.attr('fill', '#65759E')
.attr('r', function (d) { return 10 + Math.sqrt(d.count) * 4; })
.attr('cx', function (d, i) { return d.x + i * 10; })
.attr('cy', function (d, i) { return d.y; });
force.on('tick', function () {
nodes.attr('cx', function (d) { return d.x; });
links.attr('x1', function (d) { return d.source.x; })
.attr('x2', function (d) { return d.target.x; });
});
force.start();
The difficulty you're having stems from the fact that there is no way for the nodes to get around each other without leaving the 1D line you're forcing them into. The repulsive forces prevent a node from passing over top of another node to get to the other side, so they become trapped in these sub-optimal arrangements.
By default, d3 force layout initializes nodes in a random position. However, you can initialize them yourself by setting the x and y properties of the data nodes objects before starting the layout. If you initialize the graph with the nodes ordered in a row, according to the order of their connections, then the force layout can handle the spacing for you.