D3 Map XML Polygon ids to JSON data - d3.js

I have an svg/xml file that I'm loading into d3 using d3.xml(...)
This is the output of the SVG
I also have a small JSON dataset
const data = [{
"id": "polygon5256", //roof
"value": 39.5
},
{
"id": "polygon1628", //grass
"value": 3.93
},
{
"id": "polygon5254", //left wall
"value": 3.14
},
{
"id": "path5894", //door step
"value": 20.98
}
]
I'm trying to join the id's from the JSON dataset to the respective id's (polygons and paths) in the XML data. Do you know how I could also limit the selectable paths/polygons to only those that are in the JSON data? So only the available IDs will be affected by the mouse events?
Here is my jsfiddle
Any help would be much appreciated

In your fiddle you have d3.select('#polygon5256') but you can add events to any polygon with selectAll and a different CSS selector:
d3.selectAll("svg polygon")
You want path too (for the door step) so the CSS selector you need is:
d3.selectAll("svg polygon, svg path")
In the handler you can get the id of the polygon with (since you use d3 v5.9.2):
d3.event.target.id
Note that if you're using a later version of the D3 library this handling will need to change (see D3 v6 changes)
I've changed your fiddle to have a single visible div where the mouseover and mouseout handlers just populate labelling if available in data or clear the text.
Working example:
const data = [
{ "id": "polygon5256", "value": 39.5 }, // roof
{ "id": "polygon1628", "value": 3.93 }, // grass
{ "id": "polygon5254", "value": 3.14 }, // left wall
{ "id": "path5894", "value": 20.98 } // door step
];
// just make one visible div and leave text till later
const tooltip = d3.select("body")
.append("div")
.style("position", "absolute")
.style("z-index", "10")
.style("visibility", "visible");
const svgUrl = "https://raw.githubusercontent.com/gangrel11/samplefiles/main/house.svg";
d3.xml(svgUrl).then(render);
// on render set an event for any polygon of the svg
function render(data) {
d3.select("body").node().append(data.documentElement)
d3.selectAll("svg polygon, svg path")
.on("mouseover", mouseover)
.on("mouseout", mouseout);
}
// set the text of the div if it's in the data set
function mouseover(d) {
const id = d3.event.target.id;
const obj = data.find(item => item.id === id);
const text = obj ? `${id} - ${obj.value}` : "";
tooltip.text(text);
d3.select(`#${id}`).style("opacity", 0.5);
}
// clear the text of the div
function mouseout(d) {
const id = d3.event.target.id;
tooltip.text("");
d3.select(`#${id}`).style("opacity", 1);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
Edit
If you want to limit the paths/polygons to only those that are in the JSON data you can swap out this:
function render(data) {
d3.select("body").node().append(data.documentElement)
d3.selectAll("svg polygon, svg path")
.on("mouseover", mouseover)
.on("mouseout", mouseout);
}
For this (note the function signature of render has to change as we refer to data array within the function):
function render(svg) {
// add svg
d3.select("body").node().append(svg.documentElement)
// add events for ids
const ids = data.map(d => d.id);
ids.forEach(id => d3.select(`#${id}`)
.on("mouseover", mouseover)
.on("mouseout", mouseout)
);
}
Working example:
const data = [
{ "id": "polygon5256", "value": 39.5 }, // roof
{ "id": "polygon1628", "value": 3.93 }, // grass
{ "id": "polygon5254", "value": 3.14 }, // left wall
{ "id": "path5894", "value": 20.98 } // door step
];
// just make one visible div and leave text till later
const tooltip = d3.select("body")
.append("div")
.style("position", "absolute")
.style("z-index", "10")
.style("visibility", "visible");
// load external svg and render
const svgUrl = "https://raw.githubusercontent.com/gangrel11/samplefiles/main/house.svg";
d3.xml(svgUrl).then(render);
// on render set an event for anything with an id within data
function render(svg) {
// add svg
d3.select("body").node().append(svg.documentElement)
// add events for ids
const ids = data.map(d => d.id);
ids.forEach(id => d3.select(`#${id}`)
.on("mouseover", mouseover)
.on("mouseout", mouseout)
);
}
// set the text of the div if it's in the data set
function mouseover(d) {
const id = d3.event.target.id;
const obj = data.find(item => item.id === id);
const text = obj ? `${id} - ${obj.value}` : "";
tooltip.text(text);
d3.select(`#${id}`).style("opacity", 0.5);
}
// clear the text of the div
function mouseout(d) {
const id = d3.event.target.id;
tooltip.text("");
d3.select(`#${id}`).style("opacity", 1);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

Related

How to create a decision tree / flow chart in D3/dagre-D3/javascript?

So I would like to create a question flowchart like below:
Not sure where the best place to start is... Is this a Directed Graph?
Some of those end up being really spaced out and not looking great for 'flows' like so: https://observablehq.com/#d3/force-directed-graph
The best example I've seen is a non-D3 library (yworks) but it seems to cost $15k:
This is the only related StackOverflow I've seen which just references yworks: Can I create a flow chart (no tree chart) using D3.js
Maybe this dagre-d3 example as well:
http://jsfiddle.net/armyofda12mnkeys/9L50of2c/2/
var g = new dagreD3.graphlib.Graph().setGraph({});
Some cool optional stuff I'd like to add:
*I also want to be able to control the css on the Circles, like some will green some red in certain circumstances based on that node's data.
*Each Edge arrow I'd also like to add onHovers events, so a tooltip comes up to show the actual rule like 'if(Question1 == A || B)'
*Make the nodes/edges draggable or 'bouncy' (where they pop back to orig location if dragged). Sounds gimmicky, but sometimes users may use this feature if the rules get too cramped together (because of the smart auto-layout) and they wanna drag stuff to see what arrows point where.
I think I got it with dagre-d3.
Here is my initial jsfiddle:
http://jsfiddle.net/armyofda12mnkeys/4gv90qhx/2/
Also here is the same example with popups also on the edges (although I don't like the implementation as much as the node popups)
http://jsfiddle.net/armyofda12mnkeys/4gv90qhx/37/
and here is a full example of how I'm using in my project for a Diabetes Questionnaire (I upgraded the code to latest d3.v5+dagre, and made the nodes+edges draggable... lots of initial JSON parsing code to get into a format I can actually loop over, sorry bout that):
https://jsfiddle.net/armyofda12mnkeys/1burht5j/44/
Note: this last link may not work if 'cors-anywhere' website Im using is down. Try downloading it then.
// Create a new directed graph
var g = new dagreD3.graphlib.Graph().setGraph({});
var nodes = [
{'qs_code':"QS1", 'hovertext': 'This is QS1', 'proto_logic_type': 'none' },
{'qs_code':"QS2", 'hovertext': 'This is QS2', 'proto_logic_type': 'disqualify'},
{'qs_code':"QS3", 'hovertext': 'This is QS3', 'proto_logic_type': 'qualify'},
{'qs_code':"QS4", 'hovertext': 'This is QS4', 'proto_logic_type': 'both'},
{'qs_code':"QS5", 'hovertext': 'This is QS5', 'proto_logic_type': 'none'},
{'qs_code':"QS6", 'hovertext': 'This is QS6', 'proto_logic_type': 'none'}
];
// Automatically label each of the nodes
nodes.forEach(function(node) {
g.setNode(node.qs_code, { label: node.qs_code, shape: "circle", class: [node.proto_logic_type], hovertext: node.hovertext }); //style: 'fill: red'
});
// Set up the edges
g.setEdge("QS1", "QS2", { label: "<u onmouseover='(function(){ return $(\"#tooltip_template\").css(\"visibility\", \"visible\"); })()' onmouseout='(function(){ return $(\"#tooltip_template\").css(\"visibility\", \"hidden\"); })()' onmousemove='(function(){ $(\"#tooltip_template\").html(\"AAA&gt;BBB\").css(\"top\", (event.pageY-10)+\"px\").css(\"left\",(event.pageX+10)+\"px\"); })()'>Rule1</u>", hovertext:"A>B", labelType: "html" });
g.setEdge("QS1", "QS3", { label: "<u onmouseover='(function(){ return $(\"#tooltip_template\").css(\"visibility\", \"visible\"); })()' onmouseout='(function(){ return $(\"#tooltip_template\").css(\"visibility\", \"hidden\"); })()' onmousemove='(function(){ $(\"#tooltip_template\").html(\"AAA&lt;BBB\").css(\"top\", (event.pageY-10)+\"px\").css(\"left\",(event.pageX+10)+\"px\"); })()'>Rule2</u>", hovertext:"A<B", labelType: "html" });
g.setEdge("QS1", "QS4", { label: "<u onmouseover='(function(){ return $(\"#tooltip_template\").css(\"visibility\", \"visible\"); })()' onmouseout='(function(){ return $(\"#tooltip_template\").css(\"visibility\", \"hidden\"); })()' onmousemove='(function(){ $(\"#tooltip_template\").html(\"AAA==BBB\").css(\"top\", (event.pageY-10)+\"px\").css(\"left\",(event.pageX+10)+\"px\"); })()'>Rule3</u>", hovertext:"A==B", labelType: "html" });
g.setEdge("QS2", "QS5", { label: "Rule1", arrowhead: "vee", hovertext:"(A+B)>1" });
g.setEdge("QS3", "QS5", { label: "Rule1", hovertext:"(A-B)<2" });
g.setEdge("QS3", "QS6", { label: "Rule2", hovertext:"(A*B)>=3" });
g.setEdge("QS4", "QS6", { label: "Rule2", arrowhead: "vee", hovertext:"(A>10)||(B<20)" });
var svg = d3.select("svg"),
inner = svg.select("g");
// Set the rankdir
g.graph().rankdir = 'TB';//'LR';
g.graph().nodesep = 50;
// Set up zoom support
var zoom = d3.behavior.zoom().on("zoom", function() {
inner.attr("transform", "translate(" + d3.event.translate + ")" +
"scale(" + d3.event.scale + ")");
});
svg.call(zoom);
// Create the renderer
var render = new dagreD3.render();
// Run the renderer. This is what draws the final graph.
render(inner, g);
var tooltip = d3.select("body")
.append("div")
.attr('id', 'tooltip_template')
.style("position", "absolute")
.style("background-color", "white")
.style("border", "solid")
.style("border-width", "2px")
.style("border-radius", "5px")
.style("padding", "5px")
.style("z-index", "10")
.style("visibility", "hidden")
.text("Simple Tooltip...");
inner.selectAll('g.node')
.attr("data-hovertext", function(v) {
return g.node(v).hovertext
})
.on("mouseover", function(){return tooltip.style("visibility", "visible");})
.on("mousemove", function(){
tooltip.text( this.dataset.hovertext)
.style("top", (event.pageY-10)+"px")
.style("left",(event.pageX+10)+"px");
})
.on("mouseout", function(){return tooltip.style("visibility", "hidden");});
inner.selectAll('g.edgePath')
//inner.selectAll('path')
.append('title').text('This is a line.');
// Center the graph
var initialScale = 0.75;
zoom
.translate([(svg.attr("width") - g.graph().width * initialScale) / 2, 20])
.scale(initialScale)
.event(svg);
svg.attr('height', g.graph().height * initialScale + 40);

d3 static Cubism chart - can't get data input right

I'm trying to make a static cubism chart like this http://bl.ocks.org/bae25/10797393
The csv file ("cubism_test.csv") looks something like this:
date,one,two,three,four,five
2018-06-01,132.54,18.44,68.36,0,56.63
2018-06-02,146.64,19.18,71.74,0,59.66
2018-06-03,160.77,117.98,75.15,0,62.71
2018-06-04,193.29,171.53,78.59,0,65.76
2018-06-05,275.92,78.64,82.05,0,68.82
<script>
// create context and horizon
var context = cubism.context()
.size(30)
.stop();
var horizon = context.horizon()
.extent([0,2]);
d3.csv("cubism_test.csv", function(data)
{
var format = d3.time.format("%Y-%m-%d");
data.forEach(function(d, i)
{
d.date = format.parse(d.date);
d.one= +d.one;
d.two= +d.two;
d.three= +d.three;
d.four= +d.four;
d.five= +d.five;
})
console.log(data);
// define metric accessor
context.metric(function(start,stop,step,callback)
{
var values = data;
console.log(values);
callback(null, values);
}, name);
d3.select("#graph").selectAll(".horizon")
.data(data)
.enter()
.append("div")
.attr("class", "horizon")
.call(horizon);
// set rule
d3.select("#body").append("div")
.attr("class", "rule")
.call(context.rule());
// set focus
context.on("focus", function(i) {
d3.selectAll(".value")
.style( "right", i == null ? null : context.size() - i + "px");
});
// set axis
var axis = context.axis()
d3.select("#graph").append("div").attr("class", "axis").append("g").call(axis);
});
</script>
Obviously this isn't working, but I don't know to fix it. I can't find a proper recourse on how to work with d3 data. The ones I've found are very basic and tell you how to use data to make simple circles, but not time series.
I don't know how to tell d3 to use the column headers as names or get it to use the values in the columns as the values for each cubism/horizon chart.
Your advice would be highly appreciated.

Custom legend based on color of bars in a dc.js composite chart

I implemented a composite chart with two bar charts in which one bar chart consists of bars with different colored bars.
Now, I want to create a custom legend that represents each color bar (similar to https://dc-js.github.io/dc.js/examples/pie-external-labels.html used for pie chart).
Below is the code snippet of what I've done so far:
var buttonPress = dc.barChart(composite)
.dimension(joyTimeDimension)
//.renderlet(colorRenderlet)
//.colors('red')
.colors(colorbrewer.Set1[5])
.colorDomain([101, 105])
.colorAccessor(function (d) {
return d.value;
})
.group(btnGroup, "Button Press")
.keyAccessor(function(d) {return d.key[0];})
.valueAccessor(function (d) {
return d.value;
})
.title( function(d){
return [
"Time: "+d.key[0],
"button Name: "+d.key[1],
"button: "+ d.value
].join('\n')
});
var joyStick = dc.barChart(composite)
.dimension(joyTimeDimension)
.colors('blue')
.group(stepperGroup,"Joy Stick Movement")
.keyAccessor(function(d) {return d.key[0];})
.title( function(d){
return [
"Time: "+d.key[0],
"Stepper Position: "+ d.value
].join('\n')
});
composite
.width(1200)
.transitionDuration(500)
.margins({top: 30, right: 50, bottom: 25, left: 40})
.x(d3.time.scale().domain([startDate,currDate]))
.xUnits(function(){return 150;})
//.xUnits(d3.time.second)
.elasticY(true)
.legend(dc.legend().x(1000).y(4).itemHeight(13).gap(5))
.renderHorizontalGridLines(true)
.renderTitle(true)
.shareTitle(false)
.compose([buttonPress, joyStick])
.brushOn(false)
Is there a way to create a custom legend for this scenario?
Thanks in advance.
Let me provide a little bit of background about how the legend is built.
The legend in dc.js is really not all that sophisticated. It just calls .legendables() on the chart, and the chart decides what items to display in the legend.
Each chart has its own special-purpose code for this.
If we look at the source for compositeChart.legendables(), it's just recursively getting the legendables for each child chart and concatenating them:
_chart.legendables = function () {
return _children.reduce(function (items, child) {
if (_shareColors) {
child.colors(_chart.colors());
}
items.push.apply(items, child.legendables());
return items;
}, []);
};
The pie chart creates a legendable for each pie slice:
_chart.legendables = function () {
return _chart.data().map(function (d, i) {
var legendable = {name: d.key, data: d.value, others: d.others, chart: _chart};
legendable.color = _chart.getColor(d, i);
return legendable;
});
};
The legendables for the bar chart come from the stack mixin, which creates a legendable for each stack:
_chart.legendables = function () {
return _stack.map(function (layer, i) {
return {
chart: _chart,
name: layer.name,
hidden: layer.hidden || false,
color: _chart.getColor.call(layer, layer.values, i)
};
});
};
Given that there's currently no way to get a bar chart to display a pie chart's legend, I think the easiest thing to do is override legendables for your bar chart with its custom colors:
buttonPress.legendables = function() {
return btnGroup.all().map(function(kv) {
return {
chart: buttonPress,
// display the value as the text (not sure what you want here)
name: kv.value,
// apply the chart's color scale to get the color
color: buttonPress.colors()(kv.value)
};
})
};
There are probably some more details to be worked out, such as what if the same value occurs twice? I am assuming you can just read the input data from the group and .map() it, but you might need to generate your data a different way.
But this should give the general idea. Lmk if it doesn't work and I'll be glad to follow up.

How can I use 2 range sliders at the same time?

I want to filter data in the table based on the age and height at the same time using 2 range sliders.
I have implemented 2 range sliders (Age and Height) using d3.slider.js and a dc.dataTable. I want to use these 2 range sliders at the same time, but it seems that they are not working properly.
Also, under the table, there is the text "49 selected out of 49 records". The numbers are not changing while using the sliders.
Code:
var dataTable = dc.dataTable("table#list");
var dispatch = d3.dispatch('load','filter');
d3.json('data.json',function(json){
dispatch.load(json)
});
dispatch.on('load',function(json) {
var formatNumber = d3.format( ",d");
var facts = crossfilter(json);
var dimensionAge = facts.dimension(function(d) {
return +d.age;
});
var accessorAge = function(d) {
return d.age;
};
var dimensionHeight = facts.dimension(function(d) {
return +d.height;
});
var accessorHeight = function(d) {
return d.height;
};
var range = d3.extent(json, accessorAge);
var range2 = d3.extent(json, accessorHeight);
var all = facts.groupAll();
d3.select("div#slider3")
.call(d3.slider().axis(true).min(range[0]).max(range[1]).value(range)
.on("slide", function(evt,value) {
dispatch.filter(value);
d3.select("#slider3textmin").text(Math.floor(value[0]));
d3.select("#slider3textmax").text(Math.floor(value[1]))
}))
d3.select("div#slider4")
.call(d3.slider().axis(true).min(range2[0]).max(range2[1]).value(range2)
.on("slide", function(evt,value) {
dispatch.filter(value);
d3.select("#slider4textmin").text(Math.floor(value[0]));
d3.select("#slider4textmax").text(Math.floor(value[1]))
}))
FieldNames = [
"",
"Age",
"Weight",
"Height",
"Eye Color",
"Hair Color",
"Race",
"Sex",
"Annual Income"
];
d3.select("tr#FieldNames").selectAll("th")
.data(FieldNames)
.enter()
.append("th")
.append("text")
.text(function(d){
return d;
});
dataTable
.dimension(dimensionAge)
.group(function(d) {
return d.sex;
})
.columns([
function(d) {return "";},
function(d) {return d.age;},
function(d) {return d.weight;},
function(d) {return d.height;},
function(d) {return d.eyeColor;},
function(d) {return d.hairColor;},
function(d) {return d.race;},
function(d) {return d.sex;},
function(d) {return formatNumber(d.annualIncome);}
]);
dispatch.on('filter',function(value){
dataTable.replaceFilter(dc.filters.RangedFilter(value[0], value[1]));
dataTable.redraw();
})
dc.dataCount(".dc-data-count")
.dimension(facts)
.group(all);
dc.renderAll();
});
Link to the website
Plunker
Original response on the dc.js users group.
Nice use of d3.slider.js - I haven't seen that used with dc.js before.
At a quick glance, I see two problems here. First, you're using one
dispatch for both sliders, so both sliders are filtering the age,
since that's the dimension of the table. You'd probably want to create
another dimension for filtering by height, and you don't really need
to attach that to a chart.
Second, instead of just redrawing the chart with dataTable.redraw(),
you probably want to call dataTable.redrawGroup() so that all charts
in its chart group get redrawn, including the dataCount.
Specifically:
you'll need two filter events in your dispatch
var dispatch = d3.dispatch('load','filterAge','filterHeight');
the age slider will call filterAge
dispatch.filterAge(value);
and the height slider will call filterHeight
dispatch.filterHeight(value);
the current filter event handler will now handle filterAge and it will call redrawGroup
dispatch.on('filterAge',function(value){
dataTable.replaceFilter(dc.filters.RangedFilter(value[0], value[1]));
dataTable.redrawGroup();
})
we add another filterHeight handler which directly filters dimensionHeight and also redraws the chart group
dispatch.on('filterHeight',function(value){
dimensionHeight.filter([value[0], value[1]]);
dataTable.redrawGroup();
})
Reset All will also have to clear dimensionHeight. (Since this dimension isn't used by any chart, dc.filterAll() won't find it.)
Reset All
Fork of your plunker.
this for reset all, the 49 selected out of 49 records already change correcly
replace this
Reset All
to this
Reset All
add this after dispatch on load
dispatch.on('load',function(json) {
//your code
})
function sololo(){
//table
dispatch.filterAge([0,100]);
dispatch.filterHeight([0,100]);
//text slider
d3.select("#slider4textmin").text(0)
d3.select("#slider4textmax").text(0)
d3.select("#slider3textmin").text(0);
d3.select("#slider3textmax").text(0)
//slider
d3.select('#slider3').select('#handle-one').style('left','0%')
d3.select('#slider3').select('#handle-two') .style('right','0%')
d3.select('#slider3').select('div').style('left','0%').style('right','0%')
d3.select('#slider4').select('#handle-one').style('left','0%')
d3.select('#slider4').select('#handle-two') .style('right','0%')
d3.select('#slider4').select('div').style('left','0%').style('right','0%')
}

How to add a title to C3 Gauge Chart?

I am using RStudio to deal with C3 Gauge Chart. Since I am not much informed about javascript. I am having a trouble to do some small things such as adding a title.
I would like to add a title on it. However, I am having a trouble with it. Please help! Here is the code below.
output$gauge1 <- renderC3Gauge({
PTable <- Projects
if (input$accountname != "All") {
Projects <- Projects[Projects$AccountName == input$accountname,]
}
if (input$sponsorname != "All") {
Projects <- Projects[Projects$SponsorName == input$sponsorname,]
}
if (input$typename != "All") {
Projects <- Projects[Projects$TypeName == input$typename,]
}
Projects
C3Gauge(mean(Projects$PercentComplete))
})
}
shinyApp(ui=ui,server=server)
--------------------------------------------------------
HTMLWidgets.widget({
name: 'C3Gauge',
type: 'output',
factory: function(el, width, height) {
// TODO: define shared variables for this instance
return {
renderValue: function(x) {
// create a chart and set options
// note that via the c3.js API we bind the chart to the element with id equal to chart1
var chart = c3.generate({
bindto: el,
data: {
json: x,
type: 'gauge',
},
gauge: {
label:{
//returning here the value and not the ratio
format: function(value, ratio){ return value;}
},
min: 0,
max: 100,
width: 15,
units: 'value' //this is only the text for the label
}
});
},
resize: function(width, height) {
// TODO: code to re-render the widget with a new size
}
};
}
});
By deafult C3.js cannot add title to gauge chart, but you can do it with D3.js, which C3 is based on.
You have to add oninit callback into param object:
var chart = c3.generate({
oninit: function()
{
d3
.select(el)
.select("svg")
.append("text")
.attr("x", "50%" )
.attr("y", "100%")
.style("text-anchor", "middle")
.text("Your chart title goes here");
},
gauge chart title example.

Resources