How do I dynamically update the coordinates for nodes in a D3 forceSimulation? - d3.js

I am trying to update the position of a node, based on a click event.
I can see that the data is updated in console, however the x,y coordinates aren't updating. I was expecting the forceSimulation to update the x,y coordinates as soon as the data changed.
Obviously, I have misunderstood how this works. But if you could state where exactly I was wrong in my intuition, that would really help me out. My thought process was that the force simulation would update the the forceX and forceY coordinates based on the foci_dict.
In console : {name: "b", age: 27, index: 0, x: 45.46420808466252, y: 54.94672336907456, …}
The object was updated, however the coordinates didn't update.
<html>
<body>
<canvas id="bubbles" width="1500" height="500"></canvas>
<button onClick="changeMe()"> clicck me </button>
</body>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
var height = 500;
var width = 1500;
var canvas = d3.select("#bubbles");
var ctx = canvas.node().getContext("2d");
var r = 5;
var data = [
{ name: "a", age: 27 },
{ name: "a", age: 21 },
{ name: "a", age: 37 },
{ name: "b", age: 23 },
{ name: "b", age: 33 },
{ name: "c", age: 23 },
{ name: "d", age: 33 }
];
var foci_points = {
'a': { x: 50, y: 50 },
'b': { x: 150, y: 150 },
'c': { x: 250, y: 250 },
'd': { x: 350, y: 350 }
};
var simulation = d3.forceSimulation()
.force("x", d3.forceX(function(d){
return foci_points[d.name].x;
}))
.force("y", d3.forceY(function(d){
return foci_points[d.name].y;
})
)
.force("collide", d3.forceCollide(r+1))
.force("charge", d3.forceManyBody().strength(10))
.on("tick", update);
simulation.nodes(data);
function update() {
ctx.clearRect(0, 0, width, height);
ctx.beginPath();
data.forEach(drawNode);
ctx.fill();
}
function changeMe(){
data[0].name="b";
console.log(data);
}
function drawNode(d) {
ctx.moveTo(d.x, d.y);
ctx.arc(d.x, d.y, r, 0, 2 * Math.PI);
}
update();
</script>
</html>

There are two issues you have to address:
The forces will not update automatically just because the underlying data has changed. You have to programmatically re-initialize the forces by calling force.initialize(nodes). Given your code this could be somewhat along the following lines:
simulation.force("x").initialize(data); // Get the x force and re-initialize.
simulation.force("y").initialize(data); // Get the y force and re-initialize.
By the time you click the button the simulation will have cooled down or even stopped. To get it going again you have to reheat it by setting alpha to some value greater than alphaMin and by restarting the simulation run.
simulation.alpha(1).restart(); // Reheat and restart the simulation.
These actions can best be done in your changeMe() function.
Have a look at the following snippet for a running demo:
var height = 500;
var width = 1500;
var canvas = d3.select("#bubbles");
var ctx = canvas.node().getContext("2d");
var r = 5;
var data = [{
name: "a",
age: 27
},
{
name: "a",
age: 21
},
{
name: "a",
age: 37
},
{
name: "b",
age: 23
},
{
name: "b",
age: 33
},
{
name: "c",
age: 23
},
{
name: "d",
age: 33
}
];
var foci_points = {
'a': {
x: 50,
y: 50
},
'b': {
x: 150,
y: 150
},
'c': {
x: 250,
y: 250
},
'd': {
x: 350,
y: 350
}
};
var simulation = d3.forceSimulation()
.force("x", d3.forceX(function(d) {
return foci_points[d.name].x;
}))
.force("y", d3.forceY(function(d) {
return foci_points[d.name].y;
}))
.force("collide", d3.forceCollide(r + 1))
.force("charge", d3.forceManyBody().strength(10))
.on("tick", update);
simulation.nodes(data);
function update() {
ctx.clearRect(0, 0, width, height);
ctx.beginPath();
data.forEach(drawNode);
ctx.fill();
}
function changeMe() {
data[0].name = "b";
simulation.force("x").initialize(data);
simulation.force("y").initialize(data);
simulation.alpha(1).restart();
}
function drawNode(d) {
ctx.moveTo(d.x, d.y);
ctx.arc(d.x, d.y, r, 0, 2 * Math.PI);
}
update();
<script src="https://d3js.org/d3.v4.min.js"></script>
<body>
<button onClick="changeMe()"> clicck me </button>
<canvas id="bubbles" width="1500" height="500"></canvas>
</body>

Related

How to pass degree Celsius as symbol to YAxis title

How can I pass degree Celcius symbol for yAxis
I have tried all kinds
1. °
2. U+00B0 - unicode
3. ° - html code
but all of them get printed as given and do not print the degree symbol.
You can use suffix property to add suffix in axis labels to do this.
var chart = new CanvasJS.Chart("chartContainer", {
axisY: {
suffix: " °C"
},
data: [{
type: "spline",
yValueFormatString: "#0.## °C",
dataPoints: [
{ x: new Date(2017,6,24), y: 31 },
{ x: new Date(2017,6,25), y: 31 },
{ x: new Date(2017,6,26), y: 29 },
{ x: new Date(2017,6,27), y: 29 },
{ x: new Date(2017,6,28), y: 31 },
{ x: new Date(2017,6,29), y: 30 },
{ x: new Date(2017,6,30), y: 29 }
]
}]
});
chart.render();
<script src="https://canvasjs.com/assets/script/canvasjs.min.js"></script>
<div id="chartContainer" style="height: 300px; width: 100%;"></div>

Aligning C3 line shapes with Bar charts

We have the following mixed line chart / bar chart in C3:
a bar chart with two groups (light/dark blue is one group, gray is the
other group)
two other data sets represented as line with stroke-width = 0 that represent the limit for group1 and group2.
How can we place the circle shape for line1 aligned with the bar for group1 and the circle shape for line2 aligned with the two bars of group2?
In the following example, we basically would want one of the two circles to be moved slightly to the right so to align with the center of a group and the other one slightly to the left.
var chartSettings = {
padding: {
left: 120,
right: 120
},
bindto: '#chart',
data: {
x: 'Dates',
type: 'bar',
types: {
line1: 'line',
line2: 'line'
},
groups: [
['data2', 'data3'],
],
colors: {
data1: '#f3e274',
data2: '#85bdde',
data3: '#ccebfb'
},
},
bar: {
width: {
ratio: 0.50
}
},
point: {
r: 8
},
axis: {
x: {
type: 'timeseries',
tick: {
format: '%d-%m-%Y'
}
},
y: {
label: { // ADD
text: '',
position: 'outer-middle'
},
},
}
};
var date1 = new Date(2015, 1, 1, 0,0, 0,0);
var date2 = new Date(2015, 3, 1, 0,0, 0,0);
var date3 = new Date(2015, 6, 1, 0,0, 0,0);
var xAxis = ['Dates', date1, date2,date3];
var line1 = ['line1', 50, 60,55];
var line2 = ['line2', 70, 75,60];
var data1 = ['data1', 40, 35,30];
var data2 = ['data2', 5, 10,10];
var data3 = ['data3', 20, 15,30];
chartSettings.data.columns = [xAxis,
line1,
line2,
data1,
data2,
data3];
c3.generate(chartSettings);
#cr-chart .c3-line-accordatoTotale {
stroke-width: 0px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/c3/0.4.11/c3.min.js"></script>
<div id="chart"/>
var chartSettings = {
bindto: '#chart',
data: {
x: 'Dates',
type: 'bar',
types: {
line1: 'line',
line2: 'line'
},
groups: [
['data2', 'data3'],
],
names: {
line1: 'Limit for data1',
line2: 'Limit for data2 + data3',
data1: 'Data1',
data2: 'Data2',
data3: 'Data3'
},
},
bar: {
width: {
ratio: 0.50 // this makes bar width 50% of length between ticks
}
},
point: {
r: 8
},
axis: {
x: {
type: 'timeseries',
tick: {
format: '%d-%m-%Y'
}
},
y: {
label: { // ADD
text: '',
position: 'outer-middle'
}
},
}
};
var date1 = new Date(2016, 1, 1, 0, 0, 0, 0);
var date2 = new Date(2016, 3, 1, 0, 0, 0, 0);
var date3 = new Date(2016, 6, 1, 0, 0, 0, 0);
var xAxis = ['Dates',date1,date2,date3];
var line1 = ['line1', 50, 70,80];
var data1 = ['data1', 30, 40, 60];
var line2 = ['line2', 70, 60,40];
var data2 = ['data2',10,15,20];
var data3 = ['data3',15,30,5];
chartSettings.data.columns = [xAxis,
line1,
line2,
data1,
data2,
data3];
c3.generate(chartSettings);
#chart .c3-line-line1 {
stroke-width: 0px;
}
#chart .c3-line-line2 {
stroke-width: 0px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/c3/0.4.11/c3.min.js"></script>
<div id="chart"/>
It would be nice if you attached some jsfiddle.
But at this point I can say that you probably need to look inside .c3-chart-lines container and find desired line eighter by order:
.c3-chart-line:first-child // or last-child?
or by data name:
.c3-chart-line.c3-target-YOUR-ORANGE-DATA-NAME
Hope this helps.

How to draw Normalize Stack chart using C3 chart?

I want to draw normalize chart using C3 Chart library.
my current code is
var chart = c3.generate({
bindto: '#column-chart-main',
size: {
height: $('.chart-area').height()
},
data: {
rows:
chartFinalData
,
type: 'bar',
labels: {
format: d3.format('%')
},
colors: chartFinalColors,
transition: {
duration: 100
}
},
zoom: {
enabled: true
},
axis: {
y: {
show: false,
max: 1,
min: 0,
padding: {bottom:0}
},
x: {
type: 'category',
categories: chartFinalBrands
},
rotated: setChartType
},
tooltip: {
format: {
value:d3.format('%')
}
},
legend: {
show: $scope.chartTrans.showHideLegends,
position: 'inset',
inset: {
anchor: legendPosition,
x: 10,
y: 10,
step: legendSteps,
}
}
});
Above code generate simple bar stack chart.
but i need normalize bar stack chart
my current chart is - current-chart
i need chart as per - required-chart
Thanks in advance
If you want a normalised stack bar chart then you need to normalise the data first, c3 doesn't have a chart setting itself to work that out
e.g.
var data = [
['data1', 30, 200, 200, 400, 150, 250],
['data2', 130, 100, 100, 200, 150, 50],
['data3', 230, 200, 200, 300, 250, 250]
]
;
// Normalise
var scount = data.length;
for (var n = 1; n < data[0].length; n++) {
var total = 0;
for (var m = 0; m < scount; m++) {
total += data[m][n];
}
var ratio = 1.0 / total;
for (var m = 0; m < scount; m++) {
data[m][n] *= ratio;
}
}
var chart = c3.generate({
data: {
columns: data,
type: 'bar',
groups: [
['data1', 'data2', 'data3']
]
},
tooltip: {
format: {
value:d3.format('%')
}
},
axis : {
y : {
//max: 0.95, // for some reason this shows the last tick y as 100%, while 1.0 makes the last y tick 110%, don't know why
// thanks to a.n.onymous who figured out this worked better
max: 1,
padding: 0,
tick: {
format: d3.format("%")
}
}
}
});
http://jsfiddle.net/697p6hw5/6/

How to pass a value to line generation function in D3

Following code is used to generate lines in D3:
var lineFn = d3.line()
.x((d) => this.base.xAxis.scale(d.x))
.y((d) => this.base.yAxes[0].scale(d.y));
// series is a collection of lines I want to plot
series = [
{
data: [{x: 10, y: 20}, {x: 20, y: 30}],
yAxis: 0, // this indicates which y-axis to use
color: red
},
...
];
_.forEach(series, (line) => {
this.base.chart.append("path")
.datum(line.data)
.attr("class", "line")
.attr("d", lineFn)
.style("stroke", line.color)
});
My chart uses dual y-axes using d3.axisLeft() and d3.axisRight().
Right now, I am hardcoding the value of which y-axis to use in the lineFn.
.y((d) => this.base.yAxes[0].scale(d.y)); // 0-left axis, 1-right axis
What I would like to do is pass that value when I call the line function, something like:
.attr("d", lineFn(line.yAxis))
Is there any way to achieve this?
Thanks.
The easiest way to achieve what you want is simply creating two different line generators.
However, since you asked (not verbatim) "is it possible to define the scale dynamically when calling the line generator?", the answer is: yes, it is possible. Let's see how to do it.
In this example, I'm using an object to store the different scales:
var scales = {
yScaleLeft: d3.scaleLinear()
.domain([0, 100])
.range([170, 30]),
yScaleRight: d3.scaleLinear()
.domain([0, 200])
.range([170, 30])
};
And, in the dataset, defining which scale and color should be used for each line, just as you did:
var data = [{
data: [{
x: 1,
y: 20
}, {
...
}, {
x: 8,
y: 50
}],
yAxis: "yScaleLeft",
color: "red"
}, {
data: [{
x: 3,
y: 120
}, {
...
}, {
x: 9,
y: 180
}],
yAxis: "yScaleRight",
color: "blue"
}];
Then, when calling the line generator, we set a variable (in this case, thisScale) to specify the scale:
var thisScale;
paths.attr("stroke", d => d.color)
.attr("d", d => {
thisScale = scales[d.yAxis]
return line(d.data);
})
.attr("fill", "none");
Here is the demo, the red line uses a scale going from 0 to 100, the blue line uses a scale going from 0 to 200:
var svg = d3.select("body").append("svg")
.attr("width", 500)
.attr("height", 200);
var thisScale;
var line = d3.line()
.x(d => xScale(d.x))
.y(d => thisScale(d.y))
.curve(d3.curveMonotoneX);
var data = [{
data: [{
x: 1,
y: 20
}, {
x: 2,
y: 30
}, {
x: 3,
y: 10
}, {
x: 4,
y: 60
}, {
x: 5,
y: 70
}, {
x: 6,
y: 80
}, {
x: 7,
y: 40
}, {
x: 8,
y: 50
}],
yAxis: "yScaleLeft",
color: "red"
}, {
data: [{
x: 3,
y: 120
}, {
x: 4,
y: 130
}, {
x: 5,
y: 10
}, {
x: 6,
y: 120
}, {
x: 7,
y: 40
}, {
x: 8,
y: 130
}, {
x: 9,
y: 180
}],
yAxis: "yScaleRight",
color: "blue"
}];
var scales = {
yScaleLeft: d3.scaleLinear()
.domain([0, 100])
.range([170, 30]),
yScaleRight: d3.scaleLinear()
.domain([0, 200])
.range([170, 30])
};
var xScale = d3.scalePoint()
.domain(d3.range(11))
.range([30, 470])
var paths = svg.selectAll("foo")
.data(data)
.enter()
.append("path");
paths.attr("stroke", d => d.color)
.attr("d", d => {
thisScale = scales[d.yAxis]
return line(d.data);
})
.attr("fill", "none");
var xAxis = d3.axisBottom(xScale);
var yAxisLeft = d3.axisLeft(scales.yScaleLeft);
var yAxisRight = d3.axisRight(scales.yScaleRight);
var gX = svg.append("g").attr("transform", "translate(0,170)").call(xAxis);
var gY = svg.append("g").attr("transform", "translate(30,0)").call(yAxisLeft);
var gY2 = svg.append("g").attr("transform", "translate(470,0)").call(yAxisRight);
<script src="https://d3js.org/d3.v4.min.js"></script>
And here the same solution, but using an array (instead of an object) to store the scales, as you asked in your question:
yAxis: 0//indicates the left axis
yAxis: 1//indicates the right axis
var svg = d3.select("body").append("svg")
.attr("width", 500)
.attr("height", 200);
var thisScale;
var line = d3.line()
.x(d => xScale(d.x))
.y(d => thisScale(d.y))
.curve(d3.curveMonotoneX);
var data = [{
data: [{
x: 1,
y: 20
}, {
x: 2,
y: 30
}, {
x: 3,
y: 10
}, {
x: 4,
y: 60
}, {
x: 5,
y: 70
}, {
x: 6,
y: 80
}, {
x: 7,
y: 40
}, {
x: 8,
y: 50
}],
yAxis: 0,
color: "red"
}, {
data: [{
x: 3,
y: 120
}, {
x: 4,
y: 130
}, {
x: 5,
y: 10
}, {
x: 6,
y: 120
}, {
x: 7,
y: 40
}, {
x: 8,
y: 130
}, {
x: 9,
y: 180
}],
yAxis: 1,
color: "blue"
}];
var scales = [d3.scaleLinear()
.domain([0, 100])
.range([170, 30]), d3.scaleLinear()
.domain([0, 200])
.range([170, 30])
];
var xScale = d3.scalePoint()
.domain(d3.range(11))
.range([30, 470])
var paths = svg.selectAll("foo")
.data(data)
.enter()
.append("path");
paths.attr("stroke", d => d.color)
.attr("d", d => {
thisScale = scales[d.yAxis]
return line(d.data);
})
.attr("fill", "none");
var xAxis = d3.axisBottom(xScale);
var yAxisLeft = d3.axisLeft(scales[0]);
var yAxisRight = d3.axisRight(scales[1]);
var gX = svg.append("g").attr("transform", "translate(0,170)").call(xAxis);
var gY = svg.append("g").attr("transform", "translate(30,0)").call(yAxisLeft);
var gY2 = svg.append("g").attr("transform", "translate(470,0)").call(yAxisRight);
<script src="https://d3js.org/d3.v4.min.js"></script>

Donut3D.js 3d pie chart change data event not firing

I'm using Donut3D.js library (which is based on D3.js chart library) (http://bl.ocks.org/NPashaP/9994181).
I've created a javascript event listener to listen for changes in a select option Html combo box control. Users select an option from the combo box and based on the selected option, the data for the 3d pie chart is fetched from a SQL Server database and the chart is re-drawn. However, my chart is not rendering, although when I'm in Firebug debug mode, it is re-drawn. I'm using Firefox and Firebug for debugging. My Web app is using an MVC pattern and C# programming language. Following are the code snippets:
In Partial View1:
<select id=hucDdl></select>
In Partial View2:
<script>
$(document).ready(function(){
//Event listener when selection changes
$("#hucDdl").change(function () {
//Get huc value
var huc;
if($("#hucDdl").val() != null){
huc = $("#hucDdl").val();
});
//Call function
ChangeData();
});
function ChangeData(){
<blockquote>var huc = $("#hucDdl").val();
var arr = [];
var lulcData = null;
//get data from SQL Server
$.ajax({<br/></blockquote>
url: "/Home/GetBaseLulcJson/",
type: "GET",
data: {huccode: huc},
dataType: "json",
contentType: "application/json; charset=utf-8",
success: function(result){
arr = result;
},
error: function(data){
}
})
lulcData = [
{ "label": "Cropland", "value": arr[0], "color": "#ffb3ba" },
{ "label": "Forest", "value": arr[1], "color": "#ffdfba" },
{ "label": "Pasture", "value": arr[2], "color": "#ffffba" },
{ "label": "Rangeland", "value": arr[3], "color": "#baffc9" },
{ "label": "Urban", "value": arr[4], "color": "#bae1ff" }
];
//Draw the 3d pie chart
Donut3D.draw("blulcpie", getData(), 90, 50, 90, 40, 30, 0);
function getData(){
<blockquote>return lulcData.map(function (d) {
return { label: d.label, value: +d.value, color: d.color };
});
}
});
</script>
The ChangeData() function is not firing on selection change.
Does anyone know how to get the chart to re-draw when data changes?
The data fetched from SQL Server are correct.
I'm just not sure what's causing the chart not to re-draw.
Solved this issue. I revised my codes as follows:
Index.cshmtl (main view):
<!--d3.js references-->
<script src="http://d3js.org/d3.v3.min.js" type="text/javascript"></script>
<script src="~/myscripts/donut3d.js" type="text/javascript"></script>
<div id="bLulc">
<label>Major Landuse</label>
#Html.Partial("~/Views/Shared/_BaseLanduse.cshtml")
</div>
Partial View1 (holds the select HTML control):
<select id = "hucDdl"></select>
Partial View2 (contains the 3d pie chart) "_BaseLanduse.cshtml":
<script type="text/javascript">
$(document).ready(function () {
//Set default array values for initial display on page load
var def_arr = [0.2, 80.3, 1.9, 16.9, 0.7];
var defData = [
{ "label": "Cropland", "value": def_arr[0], "color": "#ffb3ba" },
{ "label": "Forest", "value": def_arr[1], "color": "#ffdfba" },
{ "label": "Pasture", "value": def_arr[2], "color": "#ffffba" },
{ "label": "Rangeland", "value": def_arr[3], "color": "#baffc9" },
{ "label": "Urban", "value": def_arr[4], "color": "#bae1ff" }
];
//Define chart parameters
var margin = { top: 0, right: 20, bottom: 0, left: 20 }
var width = 180,
height = 230 - margin.bottom;
var svg = d3.select("#bLulc")
.append("svg")
.attr("width", width)
.attr("height", height);
svg.append("g")
.data([defData])
.attr("id", "blulcpie");
//Draw the chart
Donut3D.draw("blulcpie", defData, 90, 50, 90, 40, 30, 0);
//Define legend square size
var legendSpace = 4;
var rectSize = 8;
//Add legend
defData.forEach(function (d, i) {
svg.append("rect")
.attr("transform", "translate(0," + i * (rectSize + legendSpace) + ")")
.attr("class", "rect")
.attr("width", rectSize)
.attr("height", rectSize)
.attr("x", 50) //x-axis of rect
.attr("y", 130) //y-axis of rect
.style("stroke", "#000000")
.style("stroke-width", .25)
.style("fill", defData[i].color);
svg.append("text")
.attr("class", "legend")
.attr("x", rectSize + legendSpace)
.attr("y", (i * legendSpace) + (i * rectSize))
.attr("dx", 50) //x-axis of text
.attr("dy", 138) //y-axis of text
.style("font-size", "10px")
.text(defData[i].label);
});
//Event listener when huccode changes
$("#hucDdl").bind("mousedown mouseup", function () {
debugger;
//Get data from SQL Server via Controller
$.ajax({
url: "/Home/GetBaseLulcJson/",
type: "GET",
data: { huccode: $("#hucDdl").val() },
dataType: "json",
contentType: "application/json; charset=utf-8",
success: function (result) {
arr = result;
//alert(arr);
},
error: function (data) {
//alert(data);
}
})
var currData = [
{ label: "Cropland", value: arr[0], color: "#ffb3ba" },
{ label: "Forest", value: arr[1], color: "#ffdfba" },
{ label: "Pasture", value: arr[2], color: "#ffffba" },
{ label: "Rangeland", value: arr[3], color: "#baffc9" },
{ label: "Urban", value: arr[4], color: "#bae1ff" }
];
Donut3D.transition("blulcpie", currData, 90, 40, 30, 0);
});
});
Controller:
[HttpGet]
public JsonResult GetBaseLulcJson(string huccode)
{
//Returns single row, selected columns
var lulcBase = (from f in db.FractionsLulcs
where f.HUCCODE == huccode
select new
{
f.Cropland,
f.Forest,
f.Pasture,
f.Range,
f.Urban
}).SingleOrDefault();
//Convert to percentage
double?[] lulc = new double?[5];
lulc[0] = Math.Round(Convert.ToDouble(lulcBase.Cropland) * 100, 1);
lulc[1] = Math.Round(Convert.ToDouble(lulcBase.Forest) * 100, 1);
lulc[2] = Math.Round(Convert.ToDouble(lulcBase.Pasture) * 100, 1);
lulc[3] = Math.Round(Convert.ToDouble(lulcBase.Range) * 100, 1);
lulc[4] = Math.Round(Convert.ToDouble(lulcBase.Urban) * 100, 1);
return Json(lulc, JsonRequestBehavior.AllowGet);
}

Resources