Override default networkD3 graph properties - javascript

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
)

Related

Sorting teams in group by 3 variables (wins, loses and seed)

I'm working on a tournament project and was looking for good sorting method to sort teams in group by its wins, loses and seed.
Group object:
[
{
"groupId": 1,
"signature": "A",
"teams": [
"team": {
"id": 45,
"name": "Team A",
"seed": {
"id": 1,
"name": "TOP"
},
"wins": 0,
"loses": 0
},
"team": {
"id": 2,
"name": "Team B",
"seed": {
"id": 2,
"name": "HIGH"
},
"wins": 0,
"loses": 0
} etc.
Seed order looks like this: TOP -> HIGH -> MID -> LOW
I have a group A:
Team A TOP | 0:0
Team B HIGH | 0:0
Team C MID | 0:0
Team D LOW | 0:0
In this case it's easy to sort this group (using seed, obviously), but how do I sort them by wins and loses?
Let's say some matches were played already:
Case 1:
Team B HIGH | 2:0
Team A TOP | 1:1
Team D LOW | 1:1
Team C MID | 0:2
Case 2:
Team A TOP | 2:1
Team B HIGH | 2:1
Team D LOW | 1:2
Team C MID | 0:3
How to sort teams when (case 1) teamAWins = teamDWins and teamALoses = teamDLoses and then sort those two by seed? As well as in case 2, where team A has the same wins and loses amount as teamB?
Is it possible? If not, how to do it using only wins and loses?
Thanks in advice. I really need that help.
You could take an object for getting the right numerical value for seed and sort by
wins ascending,
loses descending and
seeds with the numerical value ascending.
const seeds = { TOP: 1, HIGH: 2, MID: 3, LOW: 4 };
array.sort((a, b) =>
a.wins - b.wins ||
b.loses - a.loses ||
seeds[a.seed.name] - seeds[a.seed.name]
);

How to get grouped boxplots with vertical subplots

I'm trying to create a chart like the one in this image using Plotly.js:
It's a grouped boxplot (by sites, currently only one) with two y axes.
I've managed to create two versions, both of which don't work:
Create 5 traces (1 for each box) so that you can define the correct y axis for each box. This then puts them all next to each other, because they're different traces.
Create 3 traces to represent A, B and C. But then (afaik) I have to pick one y axis for each, which means that I can't have the same trace on two y axes.
Here's the code from approach 1 (https://codepen.io/wacmemphis/pen/gJQJeO?editors=0010)
var data =[
{
"x":[
"Site 1",
"Site 1",
"Site 1",
"Site 1",
"Site 1",
"Site 1"
],
"xaxis":"x",
"yaxis":"y",
"name":"A",
"type":"box",
"boxpoints":false,
"y":[
"3.81",
"3.74",
"3.62",
"3.50",
"3.50",
"3.54"
]
},
{
"x":[
"Site 1",
"Site 1",
"Site 1",
"Site 1",
"Site 1",
"Site 1"
],
"xaxis":"x",
"yaxis":"y",
"name":"B",
"type":"box",
"boxpoints":false,
"y":[
"1.54",
"1.54",
"1.60",
"1.41",
"1.65",
"1.47"
]
},
{
"x":[
"Site 1",
"Site 1",
"Site 1",
"Site 1",
"Site 1",
"Site 1"
],
"xaxis":"x",
"yaxis":"y",
"name":"C",
"type":"box",
"boxpoints":false,
"y":[
"3.31",
"3.81",
"3.74",
"3.63",
"3.76",
"3.68"
]
},
{
"x":[
"Site 1",
"Site 1",
"Site 1",
"Site 1",
"Site 1",
"Site 1"
],
"xaxis":"x2",
"yaxis":"y2",
"name":"A",
"type":"box",
"boxpoints":false,
"y":[
"3.81",
"3.74",
"3.62",
"3.50",
"3.50",
"3.54"
]
},
{
"x":[
"Site 1",
"Site 1",
"Site 1",
"Site 1",
"Site 1",
"Site 1"
],
"xaxis":"x2",
"yaxis":"y2",
"name":"C",
"type":"box",
"boxpoints":false,
"y":[
"3.31",
"3.81",
"3.74",
"3.63",
"3.76",
"3.68"
]
}
];
var layout = {
yaxis: {
domain: [0, 0.5],
title: 'axis 1',
},
yaxis2: {
domain: [0.5, 1],
title: 'axis2',
},
boxmode: 'group'
};
Plotly.newPlot('myDiv', data, layout);
Does anyone have any ideas?
Disclaimer
First of all I would like to emphasize that this is rather a workaraound, because Plotly currently does not support to distribute a single data source to multiple axis without interpreting them as new trace-instances (although it would be great to just set an array of target axis like { yaxis: [ "y", "y2" ] }).
However, Plotly is very deterministic in the way it handles ordering and grouping of traces, which can be taken to our advantage.
The following workaround approaches the problem in the following way:
Use two charts with one xaxis/yaxis instead of two axes
Use a single source of data for each trace (A, B, C)
Add traces to each (or both) of the plots dynamically, based on external decision
Use one of the following tactics to insert ghost objects and thus keep traces of both plots on the same x-axis positions:
a) use opacity
b) use a minimal width
c) use a threshold
1. Use two charts instead of two axes
Let's assume we can use two charts with the same layout:
<head>
<!-- Plotly.js -->
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
</head>
<body>
<!-- render the upper axis 2 chart -->
<div id="myDiv_upper"></div>
<!-- render the lower axis 1 chart -->
<div id="myDiv_lower"></div>
<script>
/* JAVASCRIPT CODE GOES HERE */
</script>
</body>
With the accompanying js code to create two inital empty charts with the given layouts:
const myDiv = document.getElementById("myDiv_lower");
const myDiv2 = document.getElementById("myDiv_upper");
const layout = {
yaxis: {
domain: [0, 0.5],
title: "axis 1",
constrain: "range"
},
margin: {
t: 0,
b: 0,
pad: 0
},
showlegend: false,
boxmode: "group"
};
const layout2 = {
yaxis: {
domain: [ 0.5, 1 ],
title: "axis 2",
},
xaxis: {
domain: [ 0, 1 ]
},
margin: {
t: 0,
b: 0,
pad: 0
},
boxmode: "group"
};
Plotly.newPlot(myDiv, [], layout);
Plotly.newPlot(myDiv2, [], layout2);
The resulting empty plot will look like this, if no further data is added:
2. Use a single source of data for each trace (A, B, C)
We can then split the data into three main source-objects:
const A = {
x: ["Site 1", "Site 1", "Site 1", "Site 1", "Site 1", "Site 1"],
xaxis: "x",
yaxis: "y",
name: "A",
legendgroup: "A",
type: "box",
boxpoints: false,
y: ["3.81", "3.74", "3.62", "3.50", "3.50", "3.54"]
};
const B = {
x: ["Site 1", "Site 1", "Site 1", "Site 1", "Site 1", "Site 1"],
xaxis: "x",
yaxis: "y",
name: "B",
legendgroup: "B",
type: "box",
boxpoints: false,
y: ["1.54", "1.54", "1.60", "1.41", "1.65", "1.47"]
};
const C = {
x: ["Site 1", "Site 1", "Site 1", "Site 1", "Site 1", "Site 1"],
xaxis: "x",
yaxis: "y",
name: "C",
legendgroup: "C",
type: "box",
boxpoints: false,
y: ["3.31", "3.81", "3.74", "3.63", "3.76", "3.68"]
}
3. Add traces to each (or both) of the plots dynamically, based on external decision
First of all we create a helper add, that updates the charts, based on new incoming data and one that creates our ghost object helper, named placeholder:
const placeholder = src => {
const copy = Object.assign({}, src)
// use one of the strategies here to make this a ghost object
return copy
}
const add = ({ src, y1, y2 }) => {
let src2
if (y1 && y2) {
Plotly.addTraces(myDiv, [src])
Plotly.addTraces(myDiv2, [src])
} else if (y1 && !y2) {
src2 = placeholder(src)
Plotly.addTraces(myDiv, [src])
Plotly.addTraces(myDiv2, [src2])
} else if (!y1 && y2) {
src2 = placeholder(src)
Plotly.addTraces(myDiv, [src2])
Plotly.addTraces(myDiv2, [src])
} else {
throw new Error('require either y1 or y2 to be true to add data')
}
}
Based on the given images the decisions to add the data to the axis would result in the following calls:
add({ src: A, y1: true, y2: true })
add({ src: B, y1: true, y2: false })
add({ src: C, y1: true, y2: true })
This would create the following (yet not satisfiable) result:
Now we have at least resolved the grouping and color. The next step is to look for possible ways of making B a ghost object, that requires spacing in the upper chart but won't display the data.
4. Use one of the following tactics to insert ghost objects and thus keep traces of both plots on the same x-axis positions
Before we look into the different options, let's see what happens, if we remove the data or null the data.
remove the data
Removing the data would mean, that the placeholder has no x/y values:
const placeholder = src => {
const copy = Object.assign({}, src)
delete copy.x
delete copy.y
return copy
}
The result would still not satisfy the requirements:
null the data
Nulling the data has the nice effect, that the data is added to the legend (which has basically the same effect as visible: 'legendonly':
const placeholder = src => {
const copy = Object.assign({}, src)
copy.x = [null]
copy.y = [null]
return copy
}
The result would still not satisfy the requirements, allthough at least the legend grouping is now correct:
a) use opacity
One option to create a ghost object is to set it's opacity to zero:
const placeholder = src => {
const copy = Object.assign({}, src)
copy.opacity = 0
copy.hoverinfo = "none" // use "name" to show "B"
return copy
}
The result has the advantage, that it pleaces the objects in the right positions. A big disadvantage is, that the legend's opactiy for B is bound to the object's opacity and this shows only the label B but not the colored box.
Another disadvantage is that the data of B still affects the yaxis scaling:
b) use a minimal width
Using a minimal amount greater zero causes the trace to nearly disappear, while a small line remains.
const placeholder = src => {
const copy = Object.assign({}, src)
copy.width = 0.000000001
copy.hoverinfo = "none" // or use "name"
return copy
}
This example keeps the grouping, positioning and legend correct but the scaling is still affected and the remaining line can be misinterpreted (which can be very problematic IMO):
c) use a threshold
Now this is the only solution that satisfies all the requirements with a great caveit: it requires a range to be set on the yaxis:
const layout2 = {
yaxis: {
domain: [ 0.5, 1 ],
title: "axis 2",
range: [3.4, 4] // this is hardcoded for now
},
xaxis: {
domain: [ 0, 1 ]
},
margin: {
t: 0,
b: 0,
pad: 0
},
boxmode: "group"
}
// ...
// with ranges we can safely add
// data to both charts, because they
// get ghosted, based on their fit
// within / outside the range
const add = ({ src }) => {
Plotly.addTraces(myDiv, [src])
Plotly.addTraces(myDiv2, [src])
}
add({ src: A })
add({ src: B })
add({ src: C })
The result will then look like the following:
Now the only question remains, how to determin the range after the new data has been added? Fortunately Plotly provides a function to update the layout, named Plotly.relayout.
For this example we may choose a simple anchor, like the mean. Of course any other method to determine the range is possible.
const add = ({ src }) => {
Plotly.addTraces(myDiv, [src])
Plotly.addTraces(myDiv2, [src])
return src.y
}
// add the data and generate a sum of all values
const avalues = add({ src: A })
const bvalues = add({ src: B })
const cvalues = add({ src: C })
const allValues = [].concat(avalues, bvalues, cvalues)
// some reusable helpers to determine our range
const highest = arr => Math.max.apply( Math, arr )
const mean = arr => arr.reduce((a, b) => Number(a) + Number(b), 0) / arr.length
const upperRange = highest(allValues) // 3.81
const meanRange = mean(allValues) // 2.9361111111111113
// our new values to update the upper layour
const updatedLayout = {
yaxis: {
range: [meanRange, upperRange]
}
}
Plotly.relayout(myDiv2, updatedLayout)
The resulting graph looks mostly like the desired result:
You can use this link to play around and improve it at your wish: https://codepen.io/anon/pen/agzKBV?editors=1010
Summary
This example is still to be considered a workaround and is not tested beyond the given data. There is also room for improvement regarding the reusability and code efficiency and it is all written down in a sequencial manner to make this code understandable as possible.
Please also keep in mind, that displaying the same data on two different axis can be misleading to be interpreted as two different sets of data.
Any suggestions for improvement are allowed, code is free to use.

Peg.js distinguish between missing values and white space

I have the following peg.js script:
start = name*
name = '** name ' var ws 'var:' vr:var ws 'len:' n:num? ws 'label:' lb:label? 'type:' ws t:type? '**\n'
{return {NAME: vr,
LENGTH: n,
LABEL:lb,
TYPE: t
}}
type = 'CHAR'/'NUM'
var = $([a-zA-Z_][a-zA-Z0-9_]*)
label = p:labChar* { return p.join('')}
labChar = [^'"<>|\*\/]
ws = [\\t\\r ]
num = n:[0-9]+ {return n.join('')}
to parse:
** name a1 var:a1 len:9 label:The is the label for a1 type:NUM **
** name a2 var:a2 len: label:The is the label for a2 type:CHAR **
** name a3 var:a3 len:67 label: type: **
and I'm encountering a couple of issues.
Firstly, within the text that I am parsing I expect certain value labels such as 'var:', 'len:', 'label:' & 'type:'. I would like to use these labels, as I know they're fixed, to delineate between the values.
Secondly, I need to allow for missing values.
Am I going about this the correct way? Currently my script merges the value of the label with the type and then I get an error at :
Line 1, column 64: Expected "type:" or [^'"<>|*/] but "*" found.
Also, Can I do this with blocks of text too? I tried parsing :
** name a1 var:a1 len:9 label:The is the label for a1 type:NUM **
** name a2 var:a2 len: label:The is the label for a2 type:CHAR **
randomly created text ()= that I would like to keep
** name b1 var:b1 len:9 label:This is the label for b1 type:NUM **
** name b2 var:b2 len: label:This is the label for b2 type:CHAR **
more text
by amending the first line an adding the following:
start = (name/random)*
random = r:.+ (!'** name')
{return {RANDOM: r.join('')}}
I'm after a final result of:
[
[{
"NAME": "a1",
"LENGTH": "9",
"LABEL": "The is the label for a1",
"TYPE": "NUM"
},
{
"NAME": "a2",
"LENGTH": null,
"LABEL": "The is the label for a2",
"TYPE": "CHAR"
},
{"RANDOM":"randomly created text ()= that I would like to keep"}]
[{
"NAME": "b1",
"LENGTH": "9",
"LABEL": "This is the label for b1",
"TYPE": "NUM"
},
{
"NAME": "b2",
"LENGTH": null,
"LABEL": "This is the label for b2",
"TYPE": "CHAR"
},
{"RANDOM":"more text "}]
]
You'll want a negative lookahead !(ws 'type:') otherwise, the label rule will be too greedy and consume all the input to the end of the line.
As a side note, you can use the $() syntax to join the text of elements instead of {return n.join('')}.
start = name*
name = '** name ' var ws 'var:' vr:var ws 'len:' n:num? ws 'label:' lb:label? ws 'type:' t:type? ws '**' '\n'?
{return {NAME: vr,
LENGTH: n,
LABEL:lb,
TYPE: t
}}
var = $([a-zA-Z_][a-zA-Z0-9_]*)
num = $([0-9]+)
label = $((!(ws 'type:') [^'"<>|\*\/])*)
type = 'CHAR'/'NUM'
ws = [\\t\\r ]
Output:
[
{
"NAME": "a1",
"LENGTH": "9",
"LABEL": "The is the label for a1",
"TYPE": "NUM"
},
{
"NAME": "a2",
"LENGTH": null,
"LABEL": "The is the label for a2",
"TYPE": "CHAR"
},
{
"NAME": "a3",
"LENGTH": "67",
"LABEL": "",
"TYPE": null
}
]
Finally got the following to work:
random = r: $(!('** name').)+ {return {"RANDOM": r}}
I'm not sure I completely understand the syntax, but it works.

Javascript Splitting a string into multiple object properties

I am working on a Shopify app and part of the order info that I need to get into Mongo is coming as a property that contains a single string via their API. As an example:
"note": "Child 1 First Name: Ali\nChild 1 Gender: Female\nChild 1 Hair Color: Blonde\nChild 1 Hair Style: Wavy\nChild 1 Skin Tone: Tan\nChild 2 First Name: Morgan \nChild 2 Gender: Female\nChild 2 Hair Color: Brown\nChild 2 Hair Style: Ponytail\nChild 2 Skin Tone: Light\nRelationship 1 to 2: Brother\nRelationship 2 to 1: Brother\n",
I actually need this string to look something like this in Mongo:
mongoExDoc: {
child1FirstName: "Ali",
child1Gender: "Female",
child1HairColor: "Blonde",
child1HairStyle: "Wavy",
child1SkinTone: "Tan",
child2FirstName: "Morgan",
child2Gender: "Female",
child2HairColor: "Brown",
child2HairStyle: "Ponytail",
child2SkinTone: "Light",
relationship1To2: "Brother",
relationship2To1: "Brother"
}
Or something along these lines. The property values themselves will NOT change. As you can see each value is separated by \n and each actual value is preceded by a:. I would really appreciate suggestions!
At a glance:
var data = {"note": "Child 1 First Name: Ali\nChild 1 Gender: Female\nChild 1 Hair Color: Blonde\nChild 1 Hair Style: Wavy\nChild 1 Skin Tone: Tan\nChild 2 First Name: Morgan \nChild 2 Gender: Female\nChild 2 Hair Color: Brown\nChild 2 Hair Style: Ponytail\nChild 2 Skin Tone: Light\nRelationship 1 to 2: Brother\nRelationship 2 to 1: Brother\n"};
var mongoExDoc = data.note.split("\n").reduce(function(obj, str, index) {
var strParts = str.split(":");
obj[strParts[0].replace(/\s+/g, '')] = strParts[1];
return obj;
}, {})
console.log(mongoExDoc);

Bind JQuery filter bar to sorted list

I have a JSON array loaded into a JQuery mobile list. I have 4 radio buttons to sort the list. All working.
Plus, I have a filter bar to search through the sorted results. So let's say I sorted my list by price ascending, I can search the list with the filter bar. But if I click price descending, the list automaticaly refresh to default.
So what i'm trying to do is sort by name ascending, filter through the results with the search bar, and if I click another sort button, the list doesnt refresh.
I hope you can understand me, I explained this as clearly as I can. Heres my code :
var productList = {"products": [
{"brand": "brand1", "description": "Product 1", "price": "03.25 "},
{"brand": "brand2", "description": "Product 4", "price": "01.10 "},
{"brand": "brand3", "description": "Product 3", "price": "04.21 "},
{"brand": "brand4", "description": "Product 2", "price": "15.24 "},
{"brand": "brand5", "description": "Product 5", "price": "01.52 "},
{"brand": "brand6", "description": "Product 6", "price": "12.01 "},
{"brand": "brand7", "description": "Product 7", "price": "05.24 "}
]
};
$(document).ready(function() {
console.debug('ready');
$('#sort > input[type = "radio"]').next('label').click( function(event, el) {
console.debug($(event.currentTarget).prev('input').attr('id'));
sortID = $(event.currentTarget).prev('input').attr('id');
refresh(sortID);
});
});
function refresh(sortID) {
var list = $("#productList").listview();
$(list).empty();
var prods = productList.products.sort(function(a, b) {
switch (sortID) {
case 'sort-a-z':
return a.description > b.description;
case 'sort-z-a':
return a.description < b.description;
case 'sort-price-up':
return parseInt(a.price) > parseInt(b.price);
case 'sort-price-down':
return parseInt(a.price) < parseInt(b.price);
default:
return a.description > b.description;
}
});
$.each(prods, function() {
list.append("<li>" + this.description + "  :       " + this.price + "</li>");
});
$(list).listview("refresh");
}
I am not sure what is exactly the problem but I do know you shouldn't be using parseInt to compare the prices like that.
parseInt stops when it sees a nondigit.
If the first character passed to parseInt is 0, then the string is evaluated in base 8 instead of base 10. In base 8, 8 and 9 are not digits, so parseInt("08") and parseInt("09") produce 0 as their result.
parseInt can take a radix parameter, so that parseInt("08", 10) produces 8.
for the best result use parseFloat and then multiply the result by 100 to produce a whole number - then when you'll compare the prices you'll avoid all quircks of the javascript floating point system.
beyond that the code you shared looks okay to me.

Categories