d3.js Checking/Count series chart - javascript

I am working on an application that uses foursquare data.
//this is the series chart that has had some delving into - but there are some bugs still running around here.
So we have a batch of data - Health & Beauty, Restaurants, Cafe, Public Houses.
-- there would be a COUNT of them -- and a SUMMATION of checkout information.
So I want this chart to be able to show the NUMBER of venues, but also indicate how POPULAR they are.. so for example the number of pubs may be smaller, but the number of checkins higher as they are more popular. So in that instance want to reverse the colors of the circles.
There are some bugs with the current code attempts.
the swapping of the circles/circle spacing causes tears in black paths and odd behaviors
with the lines I would like to have a black line under the blue circle, but inside the blue circle show a cropped circle path orange line -- so a kind of masking ability.
_latest jsfiddle
phase1
using "V" instead of "L" but couldn't make it work properly for the time being.
phase 2
I think it works more consistently but there are some issues. Also, I am not sure about the data and the scaling of the circles. (I've added extra labels so that it is visible what the value of the circles are)
phase 3
changed the getCircleSize a bit even though I believe a more consistent thing to do would be something like this layerSet.push(parseInt(getPercentage(layerArray[i], meansPerGroup[0])*60, 10));
so here the first step draws the circles by size order first... so in this case by count.. but maybe there is a bug here reversing the color to indicate the checkin count instead - so maybe we need to sort by count,checkin order - that way the first circle to get painted follows correctly.
// Create Circles
function setCircles(items) {
// sort elements in order to draw them by size
items.sort(function(a, b) {
return parseFloat(b.value) - parseFloat(a.value);
});
var circlelayer = svg.append("g")
.attr("class", "circlelayer");
var circle = circlelayer.selectAll("circle")
.data(items);
circle.enter().append("circle")
.attr("class", function(d, i) {
if (d.l == 0) {
return "blue";
}
return "gold";
})
.attr("cy", 60)
.attr("cx", function(d, i) {
var distance = calculateDistance(d, items);
if (d.l == 1) {
distancesL1.push(distance);
} else {
distancesL0.push(distance);
}
return distance;
})
.attr("r", function(d, i) {
return Math.sqrt(d.value);
})
.attr("filter", function(d) {
return "url(#drop-shadow)";
});
circle.exit().remove();
}
json structure to look something like this
[{
"label": "Health and Beauty",
"count": 30,
"checkin": 100
}, {
"label": "Restaurants",
"count": 23,
"checkin": 200
}, {
"label": "Cafes",
"count": 11,
"checkin": 900
}, {
"label": "Public Houses",
"count": 5,
"checkin": 1000
}]

I'm not sure I understand what is your problem but I've decided to try to create that chart from you screenshot with sample data from your plunker. Here is my result:
My script is making sure that smaller circle is always on top of the rage one so both circles are always visible.
Here you can find my code:
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v5.min.js"></script>
<style>
html {
font-family: sans-serif;
}
</style>
</head>
<body>
<div id="container">
</div>
<script>
const data = [{
"name": "Twitter",
"vists": "15 billion",
"unit": "per day",
"layer1value": 15000000,
"layer2value": 450
}, {
"name": "Facebook",
"vists": "5 billion",
"unit": "per day",
"layer1value": 4000000,
"layer2value": 5000000
}, {
"name": "Google",
"vists": "5 billion",
"unit": "per day",
"layer1value": 5000000,
"layer2value": 25000
}, {
"name": "Netflix",
"vists": "10 billion",
"unit": "per day",
"layer1value": 3000000,
"layer2value": 2200
}, {
"name": "Ebay",
"vists": "8 billion",
"unit": "per day",
"layer1value": 2500000,
"layer2value": 4900000
}, {
"name": "Klout",
"vists": "2 billion",
"unit": "per day",
"layer1value": 1000000,
"layer2value": 45
}];
/*
* Finding max and min layer size
*/
const values = data.reduce((acumulator, datum) => [...acumulator, datum.layer1value, datum.layer2value], []);
const maxValue = Math.max(...values);
const minValue = Math.min(...values);
/*
* Creating scale based on the smallest and largest layer1value or layer2value
*/
const radiusScale = d3.scaleLinear()
.domain([minValue, maxValue])
.range([10, 150]); // min and max value of the circle
const width = 900;
const height = 500;
const orangeColour = '#ffb000';
const blueColour = '#00a1ff';
// Creating svg element
const svg = d3.select('#container').append('svg').attr('width', width).attr('height', height);
let xPos = 0; // position of circle
/*
* iterate over each datum and render all associated elements: two circles, and two labels with pointer lines
*/
for (let i = 0; i < data.length; i++) {
const d = data[i]; // current data point
const currMaxRadius = radiusScale(Math.max(d.layer1value, d.layer2value)); // get largest radius within current group of two layers
xPos += currMaxRadius; // add that radius to xPos
// create group element containing all view elements for current datum
const group = svg.append('g')
.attr('transform', `translate(${xPos}, ${height / 2})`);
group.append('circle')
.attr('r', radiusScale(d.layer1value))
.style('fill', blueColour);
group.insert('circle', d.layer2value > d.layer1value ? ':first-child' : null) // if layer2value is larger than layer1value then insert this circle before the previous one
.attr('r', radiusScale(d.layer2value))
.style('fill', orangeColour);
xPos += currMaxRadius * 0.9;
/*
* ADDING LABEL UNDERNEATH THE CIRCLES
*/
group.append('text')
.text(d.name)
.attr('dy', radiusScale(maxValue) + 40) // add 40px of padding so label is not just bellow the circle
.style('text-anchor', 'middle');
group.append('line')
.attr('y1', radiusScale(d.layer2value))
.attr('y2', radiusScale(maxValue) + 20) // add 20px of padding so the pointer line is not overlapping with label
.style('stroke', orangeColour);
/*
* ADDING LABEL AT THE ANGLE OF 45deg RELATIVE TO THE CIRCLES
*/
// we are creating new group element so we can easily rotate both line and label by -45deg
const rotatedLabelGroup = group.append('g').style('transform', 'rotate(-45deg)');
rotatedLabelGroup.append('line')
.attr('x1', radiusScale(d.layer2value))
.attr('x2', radiusScale(maxValue) + 20)
.style('stroke', orangeColour);
rotatedLabelGroup.append('text')
.text(d.vists)
.attr('dx', radiusScale(maxValue))
.attr('dy', -5); // this way label is slightly above the line
}
</script>
</body>

Related

Offset Line stroke-weight d3.js

I'm using d3.js to plot a highway network over a map SVG. I'd like to be able to vary the stroke-weight of the line to illustrate demand based on a value.
Highway links are define as one way, so for example a two way road would have two overlapping line elements (with separate id's). I can use stroke-weight to edit the thickness of the line based on a variable (as below), but on a two way road, the larger of the two stroke weights will always cover the smaller rendering it invisible.
Is there an easy way to offset a line by half its stroke-weight to the left hand side of the direction the line is drawn? (direction denoted by x1,y1 x2,y2)
d3.csv("links.csv", function (error, data) {
d3.select("#lines").selectAll("line")
.data(data)
.enter()
.append("line")
.each(function (d) {
d.p1 = projection([d.lng1, d.lat1]);
d.p2 = projection([d.lng2, d.lat2]);
})
.attr("x1", function (d) { return d.p1[0]; })
.attr("y1", function (d) { return d.p1[1]; })
.attr("x2", function (d) { return d.p2[0]; })
.attr("y2", function (d) { return d.p2[1]; })
.on('mouseover', tip_link.show)
.on('mouseout', tip_link.hide)
.style("stroke", "black")
.style("stroke-width", lineweight)
});
One option would be to just create new start/end points when drawing your lines and use those:
var offset = function(start,destination,distance) {
// find angle of line
var dx = destination[0] - start[0];
var dy = destination[1] - start[1];
var angle = Math.atan2(dy,dx);
// offset them:
var newStart = [
start[0] + Math.sin(angle-Math.PI)*distance,
start[1] + Math.cos(angle)*distance
];
var newDestination = [
destination[0] + Math.sin(angle-Math.PI)*distance,
destination[1] + Math.cos(angle)*distance
];
// return the new start/end points
return [newStart,newDestination]
}
This function takes two points and offsets them by a particular amount given the angle between the two points. Negative values shift to the other side, swapping the start and destination points will shift to the other side.
In action, this looks like, with the original line in black:
var offset = function(start,destination,distance) {
// find angle of line
var dx = destination[0] - start[0];
var dy = destination[1] - start[1];
var angle = Math.atan2(dy,dx);
// offset them:
var newStart = [
start[0] + Math.sin(angle-Math.PI)*distance,
start[1] + Math.cos(angle)*distance
];
var newDestination = [
destination[0] + Math.sin(angle-Math.PI)*distance,
destination[1] + Math.cos(angle)*distance
];
// return the new start/end points
return [newStart,newDestination]
}
var line = [
[10,10],
[200,100]
];
var svg = d3.select("svg");
// To avoid repetition:
function draw(selection) {
selection.attr("x1",function(d) { return d[0][0]; })
.attr("x2",function(d) { return d[1][0]; })
.attr("y1",function(d) { return d[0][1]; })
.attr("y2",function(d) { return d[1][1]; })
}
svg.append("line")
.datum(line)
.call(draw)
.attr("stroke","black")
.attr("stroke-width",1)
svg.append("line")
.datum(offset(...line,6))
.call(draw)
.attr("stroke","orange")
.attr("stroke-width",10)
svg.append("line")
.datum(offset(...line,-4))
.call(draw)
.attr("stroke","steelblue")
.attr("stroke-width",5)
<svg width="500" height="300"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
You will need to adapt this to your data structure, and it requires twice as many lines as before, because you aren't using stroke width, your using lines. This is advantageous if you wanted to use canvas.

How to show full text when zoom in & truncate it when zoom out

I am creating a tree chart with d3.js, it works fine... but I want text to react to zooming, Here is the JSFiddle.
Please look at first node... it has lots of characters (in my case max will be 255)
When zoomed in or out, my text remains same, but I want to see all on zoom in.
var json = {
"name": "Maude Charlotte Licia Fernandez Maude Charlotte Licia Fernandez Maude Charlotte Licia Fernandez Maude Charlotte Licia FernandezMaude Charlotte Licia Fernandez Maude Charlotte Licia Fernandez Maude Charlotte Licia Fernandez Maude asdlkhkjh asd asdsd",
"id": "06ada7cd-3078-54bc-bb87-72e9d6f38abf",
"_parents": [{
"name": "Janie Clayton Norton",
"id": "a39bfa73-6617-5e8e-9470-d26b68787e52",
"_parents": [{
"name": "Pearl Cannon",
"id": "fc956046-a5c3-502f-b853-d669804d428f",
"_parents": [{
"name": "Augusta Miller",
"id": "fa5b0c07-9000-5475-a90e-b76af7693a57"
}, {
"name": "Clayton Welch",
"id": "3194517d-1151-502e-a3b6-d1ae8234c647"
}]
}, {
"name": "Nell Morton",
"id": "06c7b0cb-cd21-53be-81bd-9b088af96904",
"_parents": [{
"name": "Lelia Alexa Hernandez",
"id": "667d2bb6-c26e-5881-9bdc-7ac9805f96c2"
}, {
"name": "Randy Welch",
"id": "104039bb-d353-54a9-a4f2-09fda08b58bb"
}]
}]
}, {
"name": "Helen Donald Alvarado",
"id": "522266d2-f01a-5ec0-9977-622e4cb054c0",
"_parents": [{
"name": "Gussie Glover",
"id": "da430aa2-f438-51ed-ae47-2d9f76f8d831",
"_parents": [{
"name": "Mina Freeman",
"id": "d384197e-2e1e-5fb2-987b-d90a5cdc3c15"
}, {
"name": "Charlotte Ahelandro Martin",
"id": "ea01728f-e542-53a6-acd0-6f43805c31a3"
}]
}, {
"name": "Jesus Christ Pierce",
"id": "bfd1612c-b90d-5975-824c-49ecf62b3d5f",
"_parents": [{
"name": "Donald Freeman Cox",
"id": "4f910be4-b827-50be-b783-6ba3249f6ebc"
}, {
"name": "Alex Fernandez Gonzales",
"id": "efb2396d-478a-5cbc-b168-52e028452f3b"
}]
}]
}]
};
var boxWidth = 250,
boxHeight = 100;
// Setup zoom and pan
var zoom = d3.behavior.zoom()
.scaleExtent([.1, 1])
.on('zoom', function() {
svg.attr("transform", "translate(" + d3.event.translate + ") scale(" + d3.event.scale + ")");
})
// Offset so that first pan and zoom does not jump back to the origin
.translate([600, 600]);
var svg = d3.select("body").append("svg")
.attr('width', 1000)
.attr('height', 500)
.call(zoom)
.append('g')
// Left padding of tree so that the whole root node is on the screen.
// TODO: find a better way
.attr("transform", "translate(150,200)");
var tree = d3.layout.tree()
// Using nodeSize we are able to control
// the separation between nodes. If we used
// the size parameter instead then d3 would
// calculate the separation dynamically to fill
// the available space.
.nodeSize([100, 200])
// By default, cousins are drawn further apart than siblings.
// By returning the same value in all cases, we draw cousins
// the same distance apart as siblings.
.separation(function() {
return .9;
})
// Tell d3 what the child nodes are. Remember, we're drawing
// a tree so the ancestors are child nodes.
.children(function(person) {
return person._parents;
});
var nodes = tree.nodes(json),
links = tree.links(nodes);
// Style links (edges)
svg.selectAll("path.link")
.data(links)
.enter().append("path")
.attr("class", "link")
.attr("d", elbow);
// Style nodes
var node = svg.selectAll("g.person")
.data(nodes)
.enter().append("g")
.attr("class", "person")
.attr("transform", function(d) {
return "translate(" + d.y + "," + d.x + ")";
});
// Draw the rectangle person boxes
node.append("rect")
.attr({
x: -(boxWidth / 2),
y: -(boxHeight / 2),
width: boxWidth,
height: boxHeight
});
// Draw the person's name and position it inside the box
node.append("text")
.attr("text-anchor", "start")
.attr('class', 'name')
.text(function(d) {
return d.name;
});
// Text wrap on all nodes using d3plus. By default there is not any left or
// right padding. To add padding we would need to draw another rectangle,
// inside of the rectangle with the border, that represents the area we would
// like the text to be contained in.
d3.selectAll("text").each(function(d, i) {
d3plus.textwrap()
.container(d3.select(this))
.valign("middle")
.draw();
});
/**
* Custom path function that creates straight connecting lines.
*/
function elbow(d) {
return "M" + d.source.y + "," + d.source.x + "H" + (d.source.y + (d.target.y - d.source.y) / 2) + "V" + d.target.x + "H" + d.target.y;
}
body {
text-align: center;
}
svg {
margin-top: 32px;
border: 1px solid #aaa;
}
.person rect {
fill: #fff;
stroke: steelblue;
stroke-width: 1px;
}
.person {
font: 14px sans-serif;
}
.link {
fill: none;
stroke: #ccc;
stroke-width: 1.5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3plus/1.8.0/d3plus.min.js"></script>
I made a sample of your requirement in this fiddle
It may need some more tweaking to position the text vertical middle; but this can be the base for you to work on. Calculations are done in the function wrap() and call on page load and zooming.
function wrap() {
var texts = d3.selectAll("text"),
lineHeight = 1.1, // ems
padding = 2, // px
fSize = scale > 1 ? fontSize / scale : fontSize,
// find how many lines can be included
lines = Math.floor((boxHeight - (2 * padding)) / (lineHeight * fSize)) || 1;
texts.each(function(d, i) {
var text = d3.select(this),
words = d.name.split(/\s+/).reverse(),
word,
line = [],
lineNumber = 0,
tspan = text.text(null).append("tspan").attr("dy", "-0.5em").style("font-size", fSize + "px");
while ((word = words.pop())) {
line.push(word);
tspan.text(line.join(" "));
// check if the added word can fit in the box
if ((tspan.node().getComputedTextLength() + (2 * padding)) > boxWidth) {
// remove current word from line
line.pop();
tspan.text(line.join(" "));
lineNumber++;
// check if a new line can be placed
if (lineNumber > lines) {
// left align text of last line
tspan.attr("x", (tspan.node().getComputedTextLength() - boxWidth) / 2 + padding);
--lineNumber;
break;
}
// create new line
tspan.text(line.join(" "));
line = [word]; // place the current word in new line
tspan = text.append("tspan")
.style("font-size", fSize + "px")
.attr("dy", "1em")
.text(word);
}
// left align text
tspan.attr("x", (tspan.node().getComputedTextLength() - boxWidth) / 2 + padding);
}
// align vertically inside the box
text.attr("text-anchor", "middle").attr("y", padding - (lineHeight * fSize * lineNumber) / 2);
});
}
Also note that I've added the style dominant-baseline: hanging; to .person class
The code in this jsfiddle is an attempt to address the performance issues that you have with very large tree charts. A delay is set with setTimeout in the zoom event handler to allow zooming at "full speed", without text resizing. Once the zooming stops for a short time, the text is rearranged according to the new scaling:
var scaleValue = 1;
var refreshTimeout;
var refreshDelay = 0;
var zoom = d3.behavior.zoom()
.scaleExtent([.1, 1.5])
.on('zoom', function () {
svg.attr("transform", "translate(" + d3.event.translate + ") scale(" + d3.event.scale + ")");
scaleValue = d3.event.scale;
if (refreshTimeout) {
clearTimeout(refreshTimeout);
}
refreshTimeout = setTimeout(function () {
wrapText();
}, refreshDelay);
})
The delay (in milliseconds) depends on the number of nodes in the tree. You can experiment with the mathematical expression to find the best parameters for the wide range of node counts that you expect in your tree.
// Calculate the refresh delay
refreshDelay = Math.pow(node.size(), 0.5) * 2.0;
You can also set the parameters in calcFontSize to fit your needs:
// Calculate the font size for the current scaling
var calcFontSize = function () {
return Math.min(24, 10 * Math.pow(scaleValue, -0.25))
}
The initialization of the nodes has been slightly modified:
node.append("rect")
.attr({
x: 0,
y: -(boxHeight / 2),
width: boxWidth,
height: boxHeight
});
node.append("text")
.attr("text-anchor", "start")
.attr("dominant-baseline", "middle")
.attr('class', 'name')
.text(function (d) {
return d.name;
});
And the text is processed in wrapText:
// Adjust the font size to the zoom level and wrap the text in the container
var wrapText = function () {
d3.selectAll("text").each(function (d, i) {
var $text = d3.select(this);
if (!$text.attr("data-original-text")) {
// Save original text in custom attribute
$text.attr("data-original-text", $text.text());
}
var content = $text.attr("data-original-text");
var tokens = content.split(/(\s)/g);
var strCurrent = "";
var strToken = "";
var box;
var lineHeight;
var padding = 4;
$text.text("").attr("font-size", calcFontSize());
var $tspan = $text.append("tspan").attr("x", padding).attr("dy", 0);
while (tokens.length > 0) {
strToken = tokens.shift();
$tspan.text((strCurrent + strToken).trim());
box = $text.node().getBBox();
if (!lineHeight) {
lineHeight = box.height;
}
if (box.width > boxWidth - 2 * padding) {
$tspan.text(strCurrent.trim());
if (box.height + lineHeight < boxHeight) {
strCurrent = strToken;
$tspan = $text.append("tspan").attr("x", padding).attr("dy", lineHeight).text(strCurrent.trim());
} else {
break;
}
}
else {
strCurrent += strToken;
}
}
$text.attr("y", -(box.height - lineHeight) / 2);
});
}
Text wrapping can be process intensive if we have a lot of text. To address those issues, present in my first answer, this new version has improved performance, thanks to pre-rendering.
This script creates an element outside of the DOM, and stores all nodes and edges into it. Then it checks which elements would be visible, removing them from the DOM, and adding them back when appropriate.
I'm making use of jQuery for data(), and for selecting elements. In my example on the fiddle, there are 120 nodes. But it should work similarly for much more, as the only nodes rendered are the ones on screen.
I changed the zoom behaviour, so that the zoom is centered on the mouse cursor, and was surprised to see that the pan / zoom works on iOS as well.
See it in action.
UPDATE
I applied the timeout (ConnorsFan's solution), as it makes a big difference. In addition, I added a minimum scale for which text should be re-wrapped.
$(function() {
var viewport_width = $(window).width(),
viewport_height = $(window).height(),
node_width = 120,
node_height = 60,
separation_width = 100,
separation_height = 55,
node_separation = 0.78,
font_size = 20,
refresh_delay = 200,
refresh_timeout,
zoom_extent = [0.5, 1.15],
// Element outside DOM, to calculate pre-render
buffer = $("<div>");
// Parse "transform" attribute
function parse_transform(input_string) {
var transformations = {},
matches, seek;
for (matches in input_string = input_string.match(/(\w+)\(([^,)]+),?([^)]+)?\)/gi)) {
seek = input_string[matches].match(/[\w.\-]+/g), transformations[seek.shift()] = seek;
}
return transformations;
}
// Adapted from ConnorsFan's answer
function get_font_size(scale) {
fs = ~~Math.min(font_size, 15 * Math.pow(scale, -0.25));
fs = ~~(((font_size / scale) + fs) / 2)
return [fs, fs]
}
// Use d3plus to wrap the text
function wrap_text(scale) {
if (scale > 0.75) {
$("svg > g > g").each(function(a, b) {
f = $(b);
$("text", f)
.text(f.data("text"));
});
d3.selectAll("text").each(function(a, b) {
d3_el = d3.select(this);
d3plus.textwrap()
.container(d3_el)
.align("center")
.valign("middle")
.width(node_width)
.height(node_height)
.valign("middle")
.resize(!0)
.size(get_font_size(scale))
.draw();
});
}
}
// Handle pre-render (remove elements that leave viewport, add them back when appropriate)
function pre_render() {
buffer.children("*")
.each(function(i, el) {
d3.transform(d3.select(el).attr("transform"));
var el_path = $(el)[0],
svg_wrapper = $("svg"),
t = parse_transform($("svg > g")[0].getAttribute("transform")),
element_data = $(el_path).data("coords"),
element_min_x = ~~element_data.min_x,
element_max_x = ~~element_data.max_x,
element_min_y = ~~element_data.min_y,
element_max_y = ~~element_data.max_y,
svg_wrapper_width = svg_wrapper.width(),
svg_wrapper_height = svg_wrapper.height(),
s = parseFloat(t.scale),
x = ~~t.translate[0],
y = ~~t.translate[1];
if (element_min_x * s + x <= svg_wrapper_width &&
element_min_y * s + y <= svg_wrapper_height &&
0 <= element_max_x * s + x &&
0 <= element_max_y * s + y) {
if (0 == $("#" + $(el).prop("id")).length) {
if (("n" == $(el).prop("id").charAt(0))) {
// insert nodes above edges
$(el).clone(1).appendTo($("svg > g"));
wrap_text(scale = t.scale);
} else {
// insert edges
$(el).clone(1).prependTo($("svg > g"));
}
}
} else {
id = $(el).prop("id");
$("#" + id).remove();
}
});
}
d3.scale.category20();
var link = d3.select("body")
.append("svg")
.attr("width", viewport_width)
.attr("height", viewport_height)
.attr("pointer-events", "all")
.append("svg:g")
.call(d3.behavior.zoom().scaleExtent(zoom_extent)),
layout_tree = d3.layout.tree()
.nodeSize([separation_height * 2, separation_width * 2])
.separation(function() {
return node_separation;
})
.children(function(a) {
return a._parents;
}),
nodes = layout_tree.nodes(json),
edges = layout_tree.links(nodes);
// Style links (edges)
link.selectAll("path.link")
.data(edges)
.enter()
.append("path")
.attr("class", "link")
.attr("d", function(a) {
return "M" + a.source.y + "," + a.source.x + "H" + ~~(a.source.y + (a.target.y - a.source.y) / 2) + "V" + a.target.x + "H" + a.target.y;
});
// Style nodes
var node = link.selectAll("g.person")
.data(nodes)
.enter()
.append("g")
.attr("transform", function(a) {
return "translate(" + a.y + "," + a.x + ")";
})
.attr("class", "person");
// Draw the rectangle person boxes
node.append("rect")
.attr({
x: -(node_width / 2),
y: -(node_height / 2),
width: node_width,
height: node_height
});
// Draw the person's name and position it inside the box
node_text = node.append("text")
.attr("text-anchor", "start")
.text(function(a) {
return a.name;
});
// Text wrap on all nodes using d3plus. By default there is not any left or
// right padding. To add padding we would need to draw another rectangle,
// inside of the rectangle with the border, that represents the area we would
// like the text to be contained in.
d3.selectAll("text")
.each(function(a, b) {
d3plus.textwrap()
.container(d3.select(this))
.valign("middle")
.resize(!0)
.size(get_font_size(1))
.draw();
});
// START Create off-screen render
// Append node edges to memory, to allow pre-rendering
$("svg > g > path")
.each(function(a, b) {
el = $(b)[0];
if (d = $(el)
.attr("d")) {
// Parse d parameter from rect, in the format found in the d3 tree dom: M0,0H0V0V0
for (var g = d.match(/([MLQTCSAZVH])([^MLQTCSAZVH]*)/gi), c = g.length, h, k, f, l, m = [], e = [], n = 0; n < c; n++) {
command = g[n], void 0 !== command && ("M" == command.charAt(0) ? (coords = command.substring(1, command.length), m.push(~~coords.split(",")[0]), e.push(~~coords.split(",")[1])) : "V" == command.charAt(0) ? e.push(~~command.substring(1, command.length)) : "H" == command.charAt(0) && m.push(~~command.substring(1, command.length)));
}
0 < m.length && (h = Math.min.apply(this, m), f = Math.max.apply(this, m));
0 < e.length && (k = Math.min.apply(this, e), l = Math.max.apply(this, e));
$(el).data("position", a);
$(el).prop("id", "e" + a);
$(el).data("coords", {
min_x: h,
min_y: k,
max_x: f,
max_y: l
});
// Store element coords in memory
hidden_element = $(el).clone(1);
buffer.append(hidden_element);
}
});
// Append node elements to memory
$("svg > g > g").each(function(a, b) {
el = $("rect", b);
transform = b.getAttribute("transform");
null !== transform && void 0 !== transform ? (t = parse_transform(transform), tx = ~~t.translate[0], ty = ~~t.translate[1]) : ty = tx = 0;
// Calculate element area
el_min_x = ~~el.attr("x");
el_min_y = ~~el.attr("y");
el_max_x = ~~el.attr("x") + ~~el.attr("width");
el_max_y = ~~el.attr("y") + ~~el.attr("height");
$(b).data("position", a);
$(b).prop("id", "n" + a);
$(b).data("coords", {
min_x: el_min_x + tx,
min_y: el_min_y + ty,
max_x: el_max_x + tx,
max_y: el_max_y + ty
});
text_el = $("text", $(b));
0 < text_el.length && $(b).data("text", d3.select(text_el[0])[0][0].__data__.name);
// Store element coords in memory
hidden_element = $(b).clone(1);
// store node in memory
buffer.append(hidden_element);
});
// END Create off-screen render
d3_svg = d3.select("svg");
svg_group = d3.select("svg > g");
// Setup zoom and pan
zoom = d3.behavior.zoom()
.on("zoom", function() {
previous_transform = $("svg > g")[0].getAttribute("transform");
svg_group.style("stroke-width", 1.5 / d3.event.scale + "px");
svg_group.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
pre_render();
if (previous_transform !== null) {
previous_transform = parse_transform(previous_transform);
if (previous_transform.scale != d3.event.scale) {
// ConnorsFan's solution
if (refresh_timeout) {
clearTimeout(refresh_timeout);
}
scale = d3.event.scale;
refresh_timeout = setTimeout(function() {
wrap_text(scale = scale);
}, refresh_delay, scale);
}
}
});
// Apply initial zoom / pan
d3_svg.call(zoom);
});

venn.js selecting Set[A] except Set[B]

I have this code:
var sets = [
{sets: ['A'], size: 10},
{sets: ['B'], size: 10},
{sets: ['A','B'], size: 5}
];
var chart = venn.VennDiagram();
var div = d3.select("#venn").datum(sets).call(chart);
using excellent venn.js library, my venn diagram is drawn and works perfectly.
using this code:
div.selectAll("g")
.on("mouseover", function (d, i) {
// sort all the areas relative to the current item
venn.sortAreas(div, d);
// Display a tooltip with the current size
tooltip.transition().duration(400).style("opacity", .9);
tooltip.text(d.size + " items");
// highlight the current path
var selection = d3.select(this).transition("tooltip").duration(400);
selection.select("path")
.style("stroke-width", 3)
.style("fill-opacity", d.sets.length == 1 ? .4 : .1)
.style("stroke-opacity", 1)
.style("cursor", "pointer");
})
.on("mousemove", function () {
tooltip.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
})
.on("click", function (d, i) {
window.location.href = "/somepage"
})
.on("mouseout", function (d, i) {
tooltip.transition().duration(400).style("opacity", 0);
var selection = d3.select(this).transition("tooltip").duration(400);
selection.select("path")
.style("stroke-width", 1)
.style("fill-opacity", d.sets.length == 1 ? .25 : .0)
.style("stroke-opacity", 0);
});
I'm able to add Click, mouseover,... functionality to my venn.
Here is the problem:
Adding functionality to Circles (Sets A or B) works fine.
Adding functionality to Intersection (Set A intersect Set B) works fine.
I need to add some functionality to Except Area (set A except set B)
This question helped a little: 2D Polygon Boolean Operations with D3.js SVG
But I had no luck making this work.
Tried finding out Except area using: clipperjs or Greiner-Hormann polygon clipping algorithm but couldn't make it work.
Update 1:
The code in this question is copied from venn.js sample: http://benfred.github.io/venn.js/examples/intersection_tooltip.html
Other samples:
https://github.com/benfred/venn.js/
Perhaps you can do something like this....
Given 2 overlapping circles,
Find the two intersection points, and
Manually create a path that arcs from IP1 to IP2 along circle A and then from IP2 back to IP1 along circle B.
After that path is created (that covers A excluding B), you can style it however you want and add click events (etc.) to that SVG path element.
FIND INTERSECTION POINTS (IPs)
Circle-circle intersection points
var getIntersectionPoints = function(circleA, circleB){
var x1 = circleA.cx,
y1 = circleA.cy,
r1 = circleA.r,
x2 = circleB.cx,
y2 = circleB.cy,
r2 = circleB.r;
var d = Math.sqrt(Math.pow(x2-x1,2)+Math.pow(y2-y1,2)),
a = (Math.pow(r1,2)-Math.pow(r2,2)+Math.pow(d,2))/(2*d),
h = Math.sqrt(Math.pow(r1,2)-Math.pow(a,2));
var MPx = x1 + a*(x2-x1)/d,
MPy = y1 + a*(y2-y1)/d,
IP1x = MPx + h*(y2-y1)/d,
IP1y = MPy - h*(x2-x1)/d,
IP2x = MPx - h*(y2-y1)/d,
IP2y = MPy + h*(x2-x1)/d;
return [{x:IP1x,y:IP1y},{x:IP2x,y:IP2y}]
}
MANUALLY CREATE PATH
var getExclusionPath = function(keepCircle, excludeCircle){
IPs = getIntersectionPoints(keepCircle, excludeCircle);
var start = `M ${IPs[0].x},${IPs[0].y}`,
arc1 = `A ${keepCircle.r},${keepCircle.r},0,1,0,${IPs[1].x},${IPs[1].y}`,
arc2 = `A ${excludeCircle.r},${excludeCircle.r},0,0,1,${IPs[0].x},${IPs[0].y}`,
pathStr = start+' '+arc1+' '+arc2;
return pathStr;
}
var height = 900;
width = 1600;
d3.select(".plot-div").append("svg")
.attr("class", "plot-svg")
.attr("width", "100%")
.attr("viewBox", "0 0 1600 900")
var addCirc = function(circ, color){
d3.select(".plot-svg").append("circle")
.attr("cx", circ.cx)
.attr("cy", circ.cy)
.attr("r", circ.r)
.attr("fill", color)
.attr("opacity", "0.5")
}
var getIntersectionPoints = function(circleA, circleB){
var x1 = circleA.cx,
y1 = circleA.cy,
r1 = circleA.r,
x2 = circleB.cx,
y2 = circleB.cy,
r2 = circleB.r;
var d = Math.sqrt(Math.pow(x2-x1,2)+Math.pow(y2-y1,2)),
a = (Math.pow(r1,2)-Math.pow(r2,2)+Math.pow(d,2))/(2*d),
h = Math.sqrt(Math.pow(r1,2)-Math.pow(a,2));
var MPx = x1 + a*(x2-x1)/d,
MPy = y1 + a*(y2-y1)/d,
IP1x = MPx + h*(y2-y1)/d,
IP1y = MPy - h*(x2-x1)/d,
IP2x = MPx - h*(y2-y1)/d,
IP2y = MPy + h*(x2-x1)/d;
return [{x:IP1x,y:IP1y},{x:IP2x,y:IP2y}]
}
var getExclusionPath = function(keepCircle, excludeCircle){
IPs = getIntersectionPoints(keepCircle, excludeCircle);
var start = `M ${IPs[0].x},${IPs[0].y}`,
arc1 = `A ${keepCircle.r},${keepCircle.r},0,1,0,${IPs[1].x},${IPs[1].y}`,
arc2 = `A ${excludeCircle.r},${excludeCircle.r},0,0,1,${IPs[0].x},${IPs[0].y}`,
pathStr = start+' '+arc1+' '+arc2;
return pathStr;
}
var circleA = {cx: 600, cy: 500, r: 400};
var circleB = {cx: 900, cy: 400, r: 300};
var pathStr = getExclusionPath(circleA, circleB)
addCirc(circleA, "steelblue");
addCirc(circleB, "darkseagreen");
d3.select(".plot-svg").append("text")
.text("Hover over blue circle")
.attr("font-size", 70)
.attr("x", 30)
.attr("y", 70)
d3.select(".plot-svg").append("path")
.attr("class","exlPath")
.attr("d", pathStr)
.attr("stroke","steelblue")
.attr("stroke-width","10")
.attr("fill","white")
.attr("opacity",0)
.plot-div{
width: 50%;
display: block;
margin: auto;
}
.plot-svg {
border-style: solid;
border-width: 1px;
border-color: green;
}
.exlPath:hover {
opacity: 0.7;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div class="plot-div">
</div>
If you have more complex overlapping in your Venn diagrams (3+ region overlap) then this obviously gets more complicated, but I think you could still extend this approach for those situations.
Quick (sort of) note on to handle 3 set intersections ie. A∩B\C or A∩B∩C
There are 3 "levels" of overlap between A∩B and circle C...
Completely contained in C | Both AB IPs are in C
Partial overlap; C "cuts through" A∩B | Only one AB IP is in C
A∩B is completely outside C | No AB IPs are in C
Note: This is assuming C is not a subset of or fully contained by A or B -- otherwise, for example, both BC IPs could be contained in A
In total, you'll need 3 points to create the path for the 3 overlapping circles. The first 2 are along C where it "cuts through" A∩B. Those are...
The BC intersection point contained in A
The AC intersection point contained in B
For the 3rd point of the path, it depends if you want (i)A∩B∩C or (ii)A∩B\C...
(i) A∩B∩C: The AB intersection point contained in C
(ii) A∩B\C: The AB intersection point NOT contained in C
With those the points you can draw the path manually with the appropriate arcs.
Bonus -- Get ANY subsection for 2 circles
It's worth noting as well that you can get any subsection by choosing the right large-arc-flag and sweep-flag. Picked intelligently and you'll can get...
Circle A (as path)
Circle B (as path)
A exclude B --in shown example
B exclude A
A union B
A intersect B
... as well as a few more funky ones that won't match anything useful.
Some resources...
W3C site for elliptical curve commands
Good explanation for arc flags
Large-arc-flag: A value of 0 means to use the smaller arc, while a value of 1 means use the larger arc.
Sweep-flag: The sweep-flag determines whether to use an arc (0) or its reflection around the axis (1).

Creating variable number of elements with D3.js

I am pretty new to d3.js and maybe my question is very basic, but I haven't been able to find an answer...
I am trying to achieve the following, based on an array of data like this:
var data = [
{
"items": 10,
"selected": 8
},
{
"items": 12,
"selected": 4
}];
I would like to create a row of circles for every element of the array. The number of circles should be the equal to the items property and in every row, the circle in selected position should be special (like a different color). For the example above, it should display something similar to:
OOOOOOO*OO
OOO*OOOOOOOO
For the first step, any tips on how to create a variable number of SVG elements based on data values would be a great help.
Here's an example I made on codepen.
Check out the code below and/or fork the codepen and have a play.
Essentially, what is happening here is that I add a g element for each item in your data array. Normally we might be-able to create a circle for each data element, but since that is contained within a property and variable, I've used an each loop (d3 each). This creates a loop of the number of items, and creates a circle for each. If the element is selected, the fill color changes.
things to note:
The g element has a transform of 30 * i on the y axis. This means that each group will stack down the page.
In the for loop we get access to the g element using this, but we must use the d3 select function to reference it as a d3 object so we can append.
The code:
//add an svg canvas to the page
var svg = d3.select("body")
.append("svg")
.attr("transform", "translate(" + 20 + "," + 20 + ")"); //this will give it a bottom and left margin
//set up some basic data
var data = [
{
"items": 10,
"selected": 8
},
{
"items": 12,
"selected": 4
}];
var groups = svg.selectAll("g")
.data(data)
.enter()
.append("g").attr("transform", function(d, i) {
return "translate(0," + i * 30 + ")";
});
groups.each(function(d,i) {
for (var x = 1; x <= d.items; x++) {
d3.select(this).append('circle').attr({cx: function(d,i) { return (x + 1) * 22; } ,
cy: 10 ,
r: 10 ,
fill: function(d,i) { return x == d.selected ? "yellow" : "blue" }
});
}
});

d3: position text element dependent on length of element before

I'm stuck in d3 (or JavaScript in general).
I want to make a legend with d3. The position of the 9 items should be dependent on each other. More specifically:
This is my simplified array:
var dataset = ["Africa","Asia", "Caribbean", "Central America", "Europe", "Middle East", "North America", "Oceania", "South America"];
On the x-axis, I want to draw the next text 40px futher (to the right) then the last text lable ended. My intention is to have the same space between the circles every time. So the next text is always dependent on the length of the last country name.
I tried this:
.attr("x", function(d, i) {return i * 40 + d[i-1].length + 7;})
but the console says d[i-1] is undefined.
What am I missing? How would you solve this?
Many thanks in advance! Your help is very much appreciated!
Ewa
UPDATE:
Actually the legend I want to draw not only consists of the text, but also little circles.
Here is the array (with hard coded x_pos as d[2]: var dataset = [
["Africa", "#4B7985", 5], ["Asia", "#58AB86", 55], ["Caribbean", "#63A4B5", 100], ["Central America", "#818181", 165], ["Europe", "#E9726C", 255], ["Middle East", "#E3AC73", 310], ["North America", "#B65856", 383], ["Oceania", "#287E5C", 470], ["South America", "#AC8358", 530]
];
How do I draw the circles dependent on the length of the country names and get the same spacing between the cirlces?
You can draw text element to get bounding box on canvas. Then adjust position based on the last element's width:
svg.selectAll("text")
.data(data).enter()
.append("text")
.attr("x", function(d) {
return x_pos;
})
.attr("y", 50)
.style("display", "none")
.text(function(d) { return d; });
svg.selectAll("text")
.style("display", "block")
.attr("x", function(d) {
var c_pos = x_pos;
x_pos = x_pos + this.getBBox().width + distance;
return c_pos;
});
Full example: https://vida.io/documents/C5CSjbWLhoJ8rQmhF
This is how I would do it.
//This will be your array of legends
var legendItems = []
var legendCount = legendItems.length;
var legendSectionWidth = width / (legendCount+1);
//This will be your "y" value going forward. You can assign anything you want. I have used the following if else case, you should replace it with your logic to calculate y.
var vert = 0;
if(legendPosition == "bottom"){
if(showAxes)
vert = height + legendVerticalPad + containerHeight*0.04;
else
vert = height + legendVerticalPad + containerHeight*0.02;
}
for(var i = 0; i < legendCount; i++){
var text = svg.append('text')
.attr('x', (i+1)*legendSectionWidth)
.attr('y', vert)
.attr('class', 'legend-text '+legendItems[i])
.style('text-anchor', 'middle')
.style('dominant-baseline', 'central')
.text(function() {
return legendItems[i];
});
var len = text[0][0].getComputedTextLength();
// you could use circles here, just change the width n height to rx and x and y to cx and cy// you could use circles here, just change the width n height to rx and x and y to cx and cy`enter code here`
//The value 5 is your choice, i use 5px between my rect and text
svg.append('rect')
.attr('x', (i+1)*legendSectionWidth - len/2 - 5 - legendRectSize)
.attr('y', vert - legendRectSize/2)
.attr('width', legendRectSize)
.attr('height', legendRectSize)
.attr('class', function () { return 'legend '+ legendItems[i];} )
.attr('label', function() {
return legendItems[i];
})
}
The result is something like this
The following images prove that the legends(combo of rect and text) are equi-distant from each and place right in the center of the provided width. And with this logic, no matter what is the no of legends you need, all will be placed equi-distant from each other and show up right in the middle of the screen
I hope this helps.
First off, d refers to an individual, bound data point, while i refers to its index in the dataset. To look at the previous data point, you would need to reference the original dataset, rather than the provided datapoint.
Assuming you had:
var dataset = ["Africa","Asia", "Caribbean", "Central America", "Europe", "Middle East", "North America", "Oceania", "South America"];
d3.select('.foo').data(dataset)....
You would want to change your d[i - 1] references in your position handler to dataset[i - 1]
With that fixed, your first element will still blow up, since it's at dataset[0]. dataset[i - 1] is dataset[-1] in that case.
You could change your return statement to:
return i ? (i * 40 + dataset[i-1].length + 7) : i;

Categories