Add linebreak in column titles in sankey diagram in R - javascript

I would like to ask whether can I add line break to the title column in sankey diagram generated by sankeyNetwork()? The title added by using htmlwidgets::onRender.
Please find my data and code as below:
Data used to links:
sali1 <- structure(list(source = structure(c(1L, 1L, 1L, 1L, 1L, 1L, 2L,
2L, 2L, 2L, 2L, 2L, 3L, 3L, 3L, 3L, 3L, 3L, 4L, 4L, 4L, 4L, 4L
), levels = c("Yes", "I have heard something about it", "No",
"I don't no/No answer"), class = "factor"), target = c("Strongly Disagree ",
"Disagree ", "Neither agree, nor disagree ", "Agree ", "Strongly Agree ",
"I don't know/No answer ", "Strongly Disagree ", "Disagree ",
"Neither agree, nor disagree ", "Agree ", "Strongly Agree ",
"I don't know/No answer ", "Strongly Disagree ", "Disagree ",
"Neither agree, nor disagree ", "Agree ", "Strongly Agree ",
"I don't know/No answer ", "Disagree ", "Neither agree, nor disagree ",
"Agree ", "Strongly Agree ", "I don't know/No answer "), value = c(32L,
84L, 101L, 162L, 31L, 2L, 20L, 83L, 419L, 479L, 60L, 20L, 9L,
16L, 134L, 152L, 31L, 151L, 2L, 7L, 2L, 3L, 12L), group = c("Yes",
"Yes", "Yes", "Yes", "Yes", "Yes", "I-have-heard-something-about-it",
"I-have-heard-something-about-it", "I-have-heard-something-about-it",
"I-have-heard-something-about-it", "I-have-heard-something-about-it",
"I-have-heard-something-about-it", "No", "No", "No", "No", "No",
"No", "I-dont-no/No-answer", "I-dont-no/No-answer", "I-dont-no/No-answer",
"I-dont-no/No-answer", "I-dont-no/No-answer"), IDsource = c(0,
0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3,
3), IDtarget = c(4, 5, 6, 7, 8, 9, 4, 5, 6, 7, 8, 9, 4, 5, 6,
7, 8, 9, 5, 6, 7, 8, 9)), row.names = c(NA, -23L), class = c("grouped_df",
"tbl_df", "tbl", "data.frame"), groups = structure(list(source = structure(1:4, levels = c("Yes",
"I have heard something about it", "No", "I don't no/No answer"
), class = "factor"), .rows = structure(list(1:6, 7:12, 13:18,
19:23), ptype = integer(0), class = c("vctrs_list_of", "vctrs_vctr",
"list"))), class = c("tbl_df", "tbl", "data.frame"), row.names = c(NA,
-4L), .drop = TRUE))
Data for nodes
nodes <- structure(list(name = c("Yes", "I have heard something about it",
"No", "I don't no/No answer", "Strongly Disagree ", "Disagree ",
"Neither agree, nor disagree ", "Agree ", "Strongly Agree ",
"I don't know/No answer "), group = c("Yes", "I-have-heard-something-about-it",
"No", "I-dont-no/No-answer", "Strongly-Disagree-", "Disagree-",
"Neither-agree,-nor-disagree-", "Agree-", "Strongly-Agree-",
"I-dont-know/No-answer-")), row.names = c(NA, -10L), class = "data.frame")
Code for plotting sankey:
# Manually change color
color_scale <- "d3.scaleOrdinal() .domain(['Yes', 'I-have-heard-something-about-it', 'No','I-dont-no/No-answer', 'Strongly-Disagree-','Disagree-', 'Neither-agree,-nor-disagree-', 'Agree-', 'Strongly-Agree-', 'I-dont-know/No-answer-']) .range(['#28C4A9', '#A0C982', '#857A6E', '#bebebe', '#857A6E', '#D5B252', '#00487F', '#A0C982', '#28C4A9', '#bebebe']); "
# Plot sankey
sankey_sali1 <- sankeyNetwork(Links = sali1, Nodes = nodes,
Source = "IDsource", Target = "IDtarget",
Value = "value", NodeID = "name",
sinksRight = FALSE,
colourScale = color_scale,
NodeGroup = "group",
LinkGroup = "group",
nodeWidth = 40, fontSize = 20,
fontFamily = "Arial",
nodePadding = 20,
iterations = 0,
margin = list(left = 1, right = 1, top = 40, bottom = 10))
# Apply the manual var labels and change font of the labels
sankey_sali1 <- htmlwidgets::onRender(sankey_sali1, '
function(el, x) {
var cols_x = this.sankey.nodes()
.map(d => d.x)
.filter((v, i, a) => a
.indexOf(v) === i)
.sort(function(a, b){return a - b});
var labels = ["Do you know anything about radon?", "Radon may be a problem, but I have not paid much attention to it"];
cols_x.forEach((d, i) => {
d3.select(el)
.select("svg")
.append("text")
.attr("x", d)
.attr("y", 25)
.text(labels[i])
.style("font-family", "Arial")
.style("font-size", "23");
})
}
')
sankey_sali1
Current output:
As you can see, the label for the right column "Radon may be a problem, but I have not paid much attention to it" has overflowed from the picture. I would like to add a line break to it, after "problem, " in order for it to stay in the frame

For an SVG, multi-line text needs to be split up and put into separate <tspan> tags. This is not elegant, but demonstrates a solution...
sankey_sali1 <- htmlwidgets::onRender(sankey_sali1, '
function(el, x) {
var cols_x = this.sankey.nodes()
.map(d => d.x)
.filter((v, i, a) => a
.indexOf(v) === i)
.sort(function(a, b){return a - b});
var labels = ["Do you know anything \\nabout radon?", "Radon may be a problem, but I \\nhave not paid much attention to it"];
cols_x.forEach((d, i) => {
var textnode = d3.select(el)
.select("svg")
.append("text")
.attr("x", d)
.attr("y", 25)
.style("font-family", "Arial")
.style("font-size", "23");
var lines = labels[i].split(/\\r?\\n/);
textnode.append("tspan").text(lines[0]);
textnode.append("tspan").text(lines[1]).attr("x", d).attr("dy", "1em");
})
}
')
sankey_sali1

Related

Override default networkD3 graph properties

I am trying to set a custom color scheme using sankeyNetwork() from the networkd3 package in r. This is the custom color code I set for the colourScale argument of sankeyNetwork()...
# Give a color for each group:
my_color <-
'd3.scaleOrdinal()
.domain([
"Piped Water",
"Dug well protected",
"Tube well, borehole",
"Spring protected",
"Rainwater",
"Dug well unprotected",
"Spring unprotected",
"Tanker truck / cart",
"Surface water",
"Other",
"node_color"
])
.range([
"#2D20E1",
"#ABA7ED",
"#8726BE",
"#12B3F3",
"#6178e8",
"#F6F614",
"#BDBD07",
"#F4710E",
"#EEB56E",
"#6F3609"
])'
However the custom colors set in this code do not appear as I expect in the output sankey plot.
It's possible you're running into a problem with D3/JavaScript comparing only the first word of your LinkGroup values, i.e. ignoring everything after the first space. You can work around that by replacing the spaces with something else, like a "_", in both the Links data frame and the JavaScript in my_color.
Here's a full reproducible example...
library(networkD3)
nodes <- read.table(header = TRUE, text = '
name node_group
"Node 0" "node_color"
"Node 1" "node_color"
"Node 2" "node_color"
"Node 3" "node_color"
"Node 4" "node_color"
"Node 5" "node_color"
"Node 6" "node_color"
"Node 7" "node_color"
"Node 8" "node_color"
"Node 9" "node_color"
"Node 10" "node_color"
')
links <- read.table(header = TRUE, text = '
source target value link_group
0 10 1 "Piped Water"
1 10 1 "Dug well protected"
2 10 1 "Tube well, borehole"
3 10 1 "Spring protected"
4 10 1 "Rainwater"
5 10 1 "Dug well unprotected"
6 10 1 "Spring unprotected"
7 10 1 "Tanker truck / cart"
8 10 1 "Surface water"
9 10 1 "Other"
')
links$link_group <- gsub(" ", "_", links$link_group)
my_color <- paste0(
'd3.scaleOrdinal()
.domain(["',
paste0(links$link_group, collapse = '", "'),
'"])
.range([
"#F00", // red
"#0F0", // green
"#00F", // blue
"#FF0", // yellow
"#71A", // purple
"#EEE", // off-white
"#666", // grey
"#000", // black
"#520", // brown
"#FA0", // orange
"#AAA"
])')
sankeyNetwork(
Links = links,
Nodes = nodes,
Source = "source",
Target = "target",
Value = "value",
NodeID = "name",
NodeGroup = "node_group",
LinkGroup = "link_group",
colourScale = my_color
)

Handling Objects With Javascript Reduce

This is my object:
let data = [
{acnumber: 1, acname: "Mr X", acterm: "shorterm", acbalance: 150},
{acnumber: 2, acname: "Mr Y", acterm: "longterm", acbalance: 140},
{acnumber: 3, acname: "Mr Z", acterm: "shorterm", acbalance: 155}
]
Because I need information about number of accounts and account balance for 'shortterm' loans only, I can filter this data this way:
let shortTerm = data.filter((item)=>{
if (item.acterm === "shorterm") {
return item;
}
})
Then I can find number of shortterm loan accounts using this:
let numberOfAccounts = shortTerm.length;
Also, I can find sum of account balance using this:
let accountBalance = shortTerm.reduce((acc, item)=>{
return acc+item.acbalance;
},0)
I was wondering if there is any better or more precise ways of getting the same result. I will highly appreciate any assistance.
A single .reduce will work - add the property to the accumulator only if the object being iterated over is shortterm. Use concise return if you want.
let data = [
{acnumber: 1, acname: "Mr X", acterm: "shorterm", acbalance: 150},
{acnumber: 2, acname: "Mr Y", acterm: "longterm", acbalance: 140},
{acnumber: 3, acname: "Mr Z", acterm: "shorterm", acbalance: 155}
]
const balance = data.reduce(
(a, obj) => a + (obj.acterm === 'shorterm' ? obj.acbalance : 0),
0
);
console.log(balance);

ES6 - Reorder an Array with respect to an Object

I have an array which is like :
const my_array = [
"S T"
"O P"
"Lend"
"GT"
"CT"
"AC"
];
and an object according to which this array is being reordered:
const AN_ORDER = {
'Lend': 1,
'C T': 2,
'S T': 3,
'AC': 4,
'O P': 5
};
for which there is already a solution done as
//keys and sortBy from Lodash
const new_array = keys(my_array);
return sortBy(new_array, ar => AN_ORDER[ar] || Number.MAX_SAFE_INTEGER);
this yields me new array as:
["Lend", "AC", "O P", "S T", "GT", "C T"]
I tried looking into documentation of this usage, but I do not understand the idea of second argument being used here in sortBy.
Now my need is to reorder the my_array in below sequence:
['Lend', 'C T', 'S T', 'GT', 'AC', 'O P']
Please help me understand the implementation and suggest a way I can reorder in the same logic.
Your key 'G T' is not in object 'AN_ORDER' so my output will look different than yours. but it's working right.
const myArray = [
'S T',
'O P',
'Lend',
'GT',
'C T',
'AC'
];
const AN_ORDER = {
'Lend': 1,
'C T': 2,
'S T': 3,
'AC': 4,
'O P': 5
};
const sortedCollection = _.sortBy(myArray, item => {
return AN_ORDER[item];
});
You have add array in second argument, here you can mention callback function or property name.
Here is the working solution
const my_array = [
"S T",
"O P",
"Lend",
"GT",
"C T",
"AC"
];
var AN_ORDER = {
"Lend": 1,
"C T": 2,
"S T": 3,
"AC": 4,
"O P": 5
};
console.log(_.sortBy(my_array, [a => {
return (AN_ORDER[a] || Number.MAX_SAFE_INTEGER);
}]));
<script src="https://cdn.jsdelivr.net/npm/lodash#4.17.15/lodash.min.js"></script>
Not sure what AN_ORDER represents, as there isn't enough information on what it represents exactly.
Uses the keys in AN_ORDER in the final array, because the specified output above uses that. Strips spaces before searching for matching keys, using a Set to search if they exist, and using an offset of 0.5 to insert non-existing keys into AN_ORDER for sorting.
const my_array = [
"S T",
"O P",
"Lend",
"GT",
"CT",
"AC"
];
const AN_ORDER = {
'Lend': 1,
'C T': 2,
'S T': 3,
'AC': 4,
'O P': 5
};
const keys = new Set(Object.keys(AN_ORDER).map(x=>x.replace(/\s/g,'')))
my_array.forEach( (x,i)=>!keys.has(x.replace(/\s/g,'')) && (AN_ORDER[x]=i+0.5) )
my_obj = _.sortBy(Object.entries(AN_ORDER), ([,x])=>x).map(([x,])=>x)
console.log(my_obj);
<script src="https://cdn.jsdelivr.net/npm/lodash/lodash.min.js"></script>

How to sort this array of objects, since it has x and y coordinates in JS? [closed]

Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 2 years ago.
Improve this question
I can't sort my array of objects "datoa" so that if I sort x values, corresponding y values change too.
The variable myJSON is structured like:
var myJSON = [
{
"": 0,
"Comune": "BONDENO",
"PUNTEGGIOSCUOLA1516": 4.25,
"Value 1": 63,
"Value 2": 8,
"Value 3": 17,
"DANNO": 6,
"Somma valori": 88,
"numero_di_scuole": 4,
},
{
"": 1,
"Comune": "CAVEZZO",
"PUNTEGGIOSCUOLA1516": 3.75,
"Value 1": 23,
"Value 2": 2,
"Value 3": 9,
"DANNO": 8,
"Somma valori": 34,
"numero_di_scuole": 4,
}, etc.
The code to make some values into coordinates is:
const xData= myJSON.map(itm=>(itm['Value 1']+itm['Value 2'])/itm['numero_di_scuole']);
const yData = myJSON.map(itm=>itm['PUNTEGGIOSCUOLA1516']);
const datoa = xData.map((x, i) => {
return {
x: x,
y: yData[i]
};
});
However, if I do sort like:
datoa.sort(function(a,b) {
if( a.x == b.x) return a.y-b.y;
return a.x-b.x;
});
My goal is that if I have datoa = [{x: 11, y:2}, {x:5, y:6}]
with sorting datoa becomes datoa = [{x: 5, y:6}, {x:11, y:2}]
It won't work. How can I do it?
You could sort by the "square root of the sum of squares" with Math.hypot, which moves smaller x and y elements to top.
var data = [{ "": 0, Comune: "BONDENO", PUNTEGGIOSCUOLA1516: 4.25, "Value 1": 63, "Value 2": 8, "Value 3": 17, DANNO: 6, "Somma valori": 88, numero_di_scuole: 4 }, { "": 1, Comune: "CAVEZZO", PUNTEGGIOSCUOLA1516: 3.75, "Value 1": 23, "Value 2": 2, "Value 3": 9, DANNO: 8, "Somma valori": 34, numero_di_scuole: 4 }],
datoa = data.map(itm => ({
x: (itm['Value 1'] + itm['Value 2']) / itm['numero_di_scuole'],
y: itm.PUNTEGGIOSCUOLA1516
}));
datoa.sort((a, b) => Math.hypot(a.x, a.y) - Math.hypot(b.x, b.y));
console.log(datoa);
I believe your issue is in the syntax if statement of your sort function. Add curly braces {} around your first return statement, and it should work.
datoa.sort(function(a, b) {
if(a.x == b.x) {
return a.y - b.y
}
return a.x - b.x;
})
Alternately, you could use a ternary function to clean it up just a bit:
datoa.sort(function(a, b) {
return a.x == b.x ? a.y - b.y : a.x - b.x;
})
You only need a single map over the source data, then the sort can keep everything together:
const datoa = myJSON
.map(itm => ({
x: (itm['Value 1'] + itm['Value 2']) / itm['numero_di_scuole'],
y: itm['PUNTEGGIOSCUOLA1516']
}))
.sort((a, b) => (a.x === b.x) ? a.y - b.y : a.x - b.x));

dc.js table on aggregated data

I've been trying to get my head around dc.js by duplicating the code from here, https://github.com/dc-js/dc.js/blob/develop/web/examples/table-on-aggregated-data.html, with my own data.
[
{"grade": 8, "category": "Math", "rating": 4},
{"grade": 8, "category": "English", "rating": 3},
{"grade": 8, "category": "Math", "rating": 1},
{"grade": 8, "category": "Math", "rating": 3},
{"grade": 8, "category": "Science", "rating": 1},
{"grade": 9, "category": "Science", "rating": 2},
{"grade": 9, "category": "Science", "rating": 5},
{"grade": 9, "category": "English", "rating": 5}
]
with the result being a table like this:
8 English 3
8 Math 2.67
8 Science 1
9 English 5
9 Science 3.5
I've put this together
var chart = dc.dataTable("#test");
d3.json("test.json", function(error, experiments) {
console.log(experiments);
var ndx = crossfilter(experiments),
exptDimension = ndx.dimension(function(d) {return +d.grade;}),
groupedDimension = exptDimension.group().reduce(
function (p, v) {
++p.number;
p.total += +v.rating;
p.category = v.category;
p.grade = v.grade;
p.avg = (p.total / p.number).toFixed(2);
console.log(p);
return p;
},
function (p, v) {
--p.number;
p.total -= +v.rating;
p.category = v.category;
p.grade = v.grade;
p.avg = (p.number == 0) ? 0 : Math.round(p.total / p.number).toFixed(2);
return p;
},
function () {
return {number: 0, total: 0, avg: 0, category:"", grade:"", }
}),
rank = function (p) { return "rank" };
chart
.width(768)
.height(480)
.dimension(groupedDimension)
.group(rank)
.columns([function (d) { console.log(d); return d.value.grade },
function (d) { return d.value.category },
function (d) { return d.value.avg }])
.sortBy(function (d) { return d.value.avg })
.order(d3.descending)
chart.render();
});
with this result:
9 English 4.00
8 Science 2.40
I feel like i'm missing something really simple. Does anybody have a suggestion on how to proceed?
Thanks
Yeah, it is simple - or mostly simple. There's an implicit requirement that you want to group by both grade and category, whereas you were grouping by grade only. Your reduce functions wouldn't make a lot of sense that way, because
p.category = v.category;
assumes there's only one category per grade. But if you ignore that, the answer was sort of "correct", in that the average rating for 9th graders is 4, and the average rating for 8th graders is 2.4.
To group by both grade and category, you can specify a dimension key with both values in it:
exptDimension = ndx.dimension(function(d) {return d.category+'/'+d.grade;}),
This constructs a string with both values in it. Some people just make an array key here, but I don't like that because it will do some implicit conversions to string, which is a bit slower.
So that was easy but it only orders by average rating:
9 English 5.00
9 Science 3.50
8 English 3.00
8 Math 2.67
8 Science 1.00
http://jsfiddle.net/gordonwoodhull/5sbdvtqv/2/
Another implicit requirement is that you want to order by grade in ascending order, and average rating in descending order. This is a bit tougher, because it means you need to specify a single key that encodes two opposite orders.
Here's one way to do it:
.sortBy(function (d) { return (20-d.value.grade)*1000 + d.value.avg; })
Yeah. Wow. That's going to be fun to maintain, but I don't know a better way to do it. It creates a sort key where grade is highly negative-weighted, and avg is weakly positive-weighted, which when combined with d3.descending, will do what you want.
End result:
8 English 3.00
8 Math 2.67
8 Science 1.00
9 English 5.00
9 Science 3.50
http://jsfiddle.net/gordonwoodhull/5sbdvtqv/9/

Categories